Qualitaetssprung: ebene Belaege, echte Bauteile, Sichtschutz
Deploy App / deploy (push) Successful in 49s

Nach der Plankontrolle ueberarbeitet:

- Belaege sind eben. Je Belag wird aus den Hoehenkoten eine Ebene bestimmt
  (Gefaelle <= 5 %, am Kotenschwerpunkt aufgehaengt, nur Koten derselben
  Gelaendezone). Das Raster wird darauf gezwungen und dort nicht geglaettet.
  Jeder Belag bekommt eine senkrechte Sichtkante zum Gelaende.
- Mauern sind Bauteile: Travertin-Schichtmauerwerk mit versetzten Stossfugen
  und Abdeckplatte, einzelne Winkelelemente mit Fussplatte, Ortbeton in
  Schaltafelfeldern.
- Sichtschutz: die Eibenreihen des Plans werden zu geschnittenen Hecken
  verkettet, ebenso die Ligusterhecke. Lamellenwand im Planrhythmus.
- Der Plan zeichnet die Travertinplatten 40x60 einzeln im Dekorkies - jetzt
  30 erhabene Platten an den Planpositionen statt geschlossenem Belag.
- Normalmaps aus dem Fugenbild fuer Platten, Pflaster, Holzschalung und Laub.
- Nachbarhaus Zollet richtig platziert (stand auf dem Nachbargarten) und
  wie Haus 9a ausgebildet.

Behobene Fehler: boxGeometry erzeugte Seiten und Deckel mit umgekehrtem
Wickelsinn - saemtliche Boxen waren innen nach aussen gekehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 07:47:47 +02:00
co-authored by Claude Opus 5
parent f2b7b1b236
commit 8979775895
24 changed files with 1452 additions and 319 deletions
+98 -101
View File
@@ -1,104 +1,34 @@
/**
* Bauteile: Stuetz- und Sitzmauern, Travertinstelen, Gartenhaus, Spielhaus,
* Flammengrill, Baenkli, Leuchten.
* Freistehende Bauteile: Travertinstelen, Gartenhaus, Spielhaus,
* Flammengrill, Baenkli, Maehroboter-Garage, Gartentor und die Leuchten.
* Die Mauern stehen in walls.ts.
*/
import {
BoxGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial, PointLight,
type MeshStandardMaterialParameters,
} from 'three';
import type { Lamp, Prop, Stele, Wall } from '../site';
import { boxGeometry, wallGeometry } from './geometry';
import type { Lamp, Prop, Stele } from '../site';
import { boxGeometry } from './geometry';
import * as tex from './textures';
function material(belag: tex.Belag, extra: MeshStandardMaterialParameters = {}) {
return new MeshStandardMaterial({
map: belag.map, roughness: belag.roughness, metalness: 0, ...extra,
map: belag.map,
normalMap: belag.normalMap ?? null,
normalScale: belag.normalScale,
roughness: belag.roughness,
metalness: 0,
...extra,
});
}
const WALL_LOOK: Record<Wall['type'], () => tex.Belag> = {
travertin: tex.travertinMauer,
beton: tex.beton,
holzscheiter: tex.holzscheiter,
lamellen: tex.fassadeHolz,
};
/** Wertet ein Hoehenprofil (Stuetzelemente) an der Stelle t aus. */
function profileAt(profile: number[] | undefined, t: number, fallback: number): number {
if (!profile || !profile.length) return fallback;
const f = t * (profile.length - 1);
const i = Math.max(0, Math.min(profile.length - 2, Math.floor(f)));
const u = f - i;
return profile[i] * (1 - u) + profile[i + 1] * u;
}
export function buildWalls(walls: Wall[], height: (x: number, z: number) => number): Group {
const group = new Group();
group.name = 'Mauern und Wände';
for (const w of walls) {
const belag = WALL_LOOK[w.type]();
const top = (t: number, x: number, z: number) =>
w.crestProfile ? profileAt(w.crestProfile, t, 0) : (w.top ?? height(x, z) + 0.4);
const base = (t: number, x: number, z: number) =>
w.footProfile ? profileAt(w.footProfile, t, -1) : (w.base ?? height(x, z) - 0.5);
if (w.openSlats) {
// Lamellenwand: senkrechte Bretter im Plan-Rhythmus (10 cm breit,
// 35 cm tief, rund 90 cm Achsabstand), dazu das schmale Randband.
const slats = new Group();
const [a, b] = [w.path[0], w.path[w.path.length - 1]];
const len = Math.hypot(b[0] - a[0], b[1] - a[1]);
const n = Math.max(2, Math.round(len / 0.9));
const mat = material(belag, { roughness: 0.85 });
const dirX = (b[0] - a[0]) / len, dirZ = (b[1] - a[1]) / len;
const angle = (Math.atan2(dirZ, dirX) * 180) / Math.PI;
for (let i = 0; i <= n; i++) {
const t = i / n;
const x = a[0] + (b[0] - a[0]) * t;
const z = a[1] + (b[1] - a[1]) * t;
const g = boxGeometry(x, z, 0.10, w.t, base(t, x, z), top(t, x, z), angle, 1.2);
const m = new Mesh(g, mat);
m.castShadow = true;
m.receiveShadow = true;
slats.add(m);
}
// durchlaufendes Randband an der Fassadenkante
const band = new Mesh(
wallGeometry(w.path, 0.10, top, base, { segments: 6, tile: belag.tile }),
mat,
);
band.castShadow = true;
band.receiveShadow = true;
slats.add(band);
slats.name = w.id;
slats.userData.note = w.note;
group.add(slats);
continue;
}
const geo = wallGeometry(w.path, w.t, top, base, {
segments: w.crestProfile ? w.crestProfile.length - 1 : 16,
tile: belag.tile,
});
const mesh = new Mesh(geo, material(belag));
mesh.name = w.id;
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.userData.note = w.note;
group.add(mesh);
}
return group;
}
export function buildStelen(stelen: Stele[], height: (x: number, z: number) => number): Group {
const group = new Group();
group.name = 'Travertinstelen';
const mat = material(tex.travertinMauer(), { roughness: 0.68 });
const stein = tex.travertinMauer();
const mat = material(stein, { roughness: 0.66 });
for (const s of stelen) {
const y0 = height(s.x, s.z) - 0.3;
const geo = boxGeometry(s.x, s.z, 0.22, 0.22, y0, y0 + 0.3 + s.h, 0, 1.6);
const geo = boxGeometry(s.x, s.z, 0.22, 0.22, y0, y0 + 0.3 + s.h, 0, stein.tile);
const m = new Mesh(geo, mat);
m.castShadow = true;
m.receiveShadow = true;
@@ -165,30 +95,59 @@ function spielhaus(p: Prop, y: number): Group {
return g;
}
/**
* Flammengrill: Cortenstahl-Kubus mit umlaufender Grillplatte und offener
* Feuerkammer, wie auf dem Referenzfoto des Plans.
*/
function flammengrill(p: Prop, y: number): Group {
const g = new Group();
const steel = new MeshStandardMaterial({ color: 0x3a3b3e, roughness: 0.5, metalness: 0.45 });
const body = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + 0.06, y + p.h, p.rot, 1), steel);
const corten = new MeshStandardMaterial({ color: 0x5c3a26, roughness: 0.62, metalness: 0.45 });
const dark = new MeshStandardMaterial({ color: 0x241d18, roughness: 0.75, metalness: 0.3 });
const plateMat = new MeshStandardMaterial({ color: 0x3b3a37, roughness: 0.34, metalness: 0.72 });
// Sockel etwas eingezogen, damit der Kubus zu schweben scheint
const base = new Mesh(boxGeometry(p.x, p.z, p.w - 0.12, p.d - 0.12, y, y + 0.08, p.rot, 1), dark);
g.add(base);
const bodyTop = y + p.h - 0.05;
const body = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + 0.08, bodyTop, p.rot, 1), corten);
body.castShadow = true; body.receiveShadow = true;
g.add(body);
// Feuerkammer: dunkle Nische in der Vorderseite
const a = (p.rot * Math.PI) / 180;
const fx = p.x - Math.sin(a) * (p.d / 2 - 0.02);
const fz = p.z + Math.cos(a) * (p.d / 2 - 0.02);
const fire = new Mesh(
boxGeometry(fx, fz, p.w * 0.62, 0.06, y + 0.24, y + 0.52, p.rot, 1), dark,
);
g.add(fire);
// Grillplatte mit Ausschnitt fuer das Feuer
const plate = new Mesh(
boxGeometry(p.x, p.z, p.w + 0.1, p.d + 0.1, y + p.h, y + p.h + 0.03, p.rot, 1),
new MeshStandardMaterial({ color: 0x3a3a3c, roughness: 0.35, metalness: 0.7 }),
boxGeometry(p.x, p.z, p.w + 0.14, p.d + 0.14, bodyTop, bodyTop + 0.035, p.rot, 1), plateMat,
);
plate.castShadow = true;
plate.receiveShadow = true;
g.add(plate);
const ring = new Mesh(
new CylinderGeometry(p.w * 0.24, p.w * 0.24, 0.05, 20), dark,
);
ring.position.set(p.x, bodyTop + 0.04, p.z);
g.add(ring);
return g;
}
function bank(p: Prop, y: number): Group {
const g = new Group();
const stein = material(tex.travertinMauer(), { roughness: 0.65 });
const seat = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + p.h - 0.09, y + p.h, p.rot, 1.6), stein);
const belag = tex.travertinMauer();
const stein = material(belag, { roughness: 0.65 });
const seat = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + p.h - 0.09, y + p.h, p.rot, belag.tile), stein);
seat.castShadow = true; seat.receiveShadow = true;
g.add(seat);
for (const s of [-1, 1]) {
const m = new Mesh(
boxGeometry(p.x, p.z + (s * (p.d / 2 - 0.22)), p.w - 0.08, 0.14, y, y + p.h - 0.09, p.rot, 1.6),
boxGeometry(p.x, p.z + (s * (p.d / 2 - 0.22)), p.w - 0.08, 0.14, y, y + p.h - 0.09, p.rot, belag.tile),
stein,
);
m.castShadow = true;
@@ -197,14 +156,52 @@ function bank(p: Prop, y: number): Group {
return g;
}
function simpleBox(p: Prop, y: number, color: number, rough = 0.7): Mesh {
const m = new Mesh(
boxGeometry(p.x, p.z, p.w, p.d, y, y + p.h, p.rot, 1),
new MeshStandardMaterial({ color, roughness: rough }),
/** Garage fuer den Maehroboter: flaches Pultdach auf zwei Seitenwangen. */
function roboterGarage(p: Prop, y: number): Group {
const g = new Group();
const holz = material(tex.fassadeHolz(), { color: 0x9aa08e, roughness: 0.82 });
const dach = new MeshStandardMaterial({ color: 0x4e5348, roughness: 0.6, metalness: 0.2 });
const hw = p.w / 2;
for (const s of [-1, 1]) {
const m = new Mesh(boxGeometry(p.x + s * (hw - 0.03), p.z, 0.06, p.d, y, y + p.h, p.rot, 1), holz);
m.castShadow = true; m.receiveShadow = true;
g.add(m);
}
const back = new Mesh(boxGeometry(p.x, p.z + p.d / 2 - 0.03, p.w, 0.06, y, y + p.h, p.rot, 1), holz);
back.castShadow = true;
g.add(back);
const roof = new Mesh(
boxGeometry(p.x, p.z, p.w + 0.1, p.d + 0.12, y + p.h, y + p.h + 0.05, p.rot, 1), dach,
);
m.castShadow = true;
m.receiveShadow = true;
return m;
roof.castShadow = true;
g.add(roof);
return g;
}
/** Wasserhahn: verzinkter Standrohrsockel mit Auslauf. */
function wasserhahn(p: Prop, y: number): Group {
const g = new Group();
const metal = new MeshStandardMaterial({ color: 0x8d9095, roughness: 0.38, metalness: 0.8 });
const post = new Mesh(new CylinderGeometry(0.045, 0.05, p.h, 12), metal);
post.position.set(p.x, y + p.h / 2, p.z);
post.castShadow = true;
g.add(post);
const spout = new Mesh(new CylinderGeometry(0.016, 0.016, 0.16, 8), metal);
spout.rotation.z = Math.PI / 2;
spout.position.set(p.x + 0.08, y + p.h - 0.08, p.z);
g.add(spout);
return g;
}
/** Schlaufschacht D = 30 cm: buendiger Deckel im Belag. */
function schachtdeckel(p: Prop, y: number): Group {
const g = new Group();
const metal = new MeshStandardMaterial({ color: 0x6f6c66, roughness: 0.55, metalness: 0.6 });
const lid = new Mesh(new CylinderGeometry(p.w / 2, p.w / 2, 0.03, 24), metal);
lid.position.set(p.x, y + 0.012, p.z);
lid.receiveShadow = true;
g.add(lid);
return g;
}
function gartentor(p: Prop, y: number): Group {
@@ -235,9 +232,9 @@ export function buildProps(props: Prop[], height: (x: number, z: number) => numb
case 'flammengrill': obj = flammengrill(p, y); break;
case 'bank': obj = bank(p, y); break;
case 'tor': obj = gartentor(p, y); break;
case 'roboter': obj = simpleBox(p, y, 0x33413a, 0.6); break;
case 'wasserhahn': obj = simpleBox(p, y, 0x8a8d90, 0.4); break;
case 'schacht': obj = simpleBox(p, y - 0.01, 0x77746e, 0.9); break;
case 'roboter': obj = roboterGarage(p, y); break;
case 'wasserhahn': obj = wasserhahn(p, y); break;
case 'schacht': obj = schachtdeckel(p, y); break;
default: continue;
}
obj.name = p.id;