Sichtschutz, schwebende Stufen und Hoehenprofil nach Plandurchgang
Deploy App / deploy (push) Successful in 28s

Nach erneutem Detailstudium des Gartenplans und der Referenzbilder darin:

- Sichtschutz ist nicht durchgehend: 28 aufrechte Travertinstelen (50x6 cm,
  Dreiergruppen, Positionen direkt aus dem Plan) wechseln sich mit
  Eibengruppen ab, alle in einem Dekorkiesbett. Hoehe 1.80 m gegen den
  Suednachbarn, 1.50 m gegen Zollet auf der unteren Ebene.
- Zwei schwebende Travertinstufen fuehren vom gedeckten Sitzplatz (-0.02)
  auf den Hauptsitzplatz hinunter, als auskragende Platten mit Schattenfuge.
- Hoehenprofil auf zwei klare Ebenen gebracht: untere Ebene -0.33,
  Travertinsitzmauer mit 31 cm Ansicht, obere Ebene -0.02. Die Niveaus stehen
  fest in LEVELS statt aus streuenden Koten geschaetzt zu werden.
- Jede Hoehenkote gehoert neu zu genau einer Flaeche, der kleinsten, die sie
  enthaelt - sonst kippen benachbarte Ebenen ineinander.
- Der schmale Streifen am Sichtschutz ist Kiesbett, nicht Plattenbelag.
- Travertin nach den Referenzbildern deutlich heller, eigene Steintextur fuer
  die Stelen; Flammengrill als Stahlkubus mit Holzfach und Feueroeffnung.
- Stirnflaechen der Hecken hatten entartete UVs und zeigten Strahlenmuster.
- tools/plan-bilder.mjs zieht die Referenzfotos aus dem Plan-PDF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 07:40:36 +02:00
co-authored by Claude Opus 5
parent d3732bfed6
commit 5b1cf9d0d9
11 changed files with 474 additions and 127 deletions
+84 -37
View File
@@ -4,11 +4,11 @@
* Die Mauern stehen in walls.ts.
*/
import {
BoxGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial, PointLight,
type MeshStandardMaterialParameters,
BoxGeometry, BufferGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial,
PointLight, type MeshStandardMaterialParameters,
} from 'three';
import type { Lamp, Prop, Stele } from '../site';
import { boxGeometry } from './geometry';
import { boxGeometry, mergeGeometries } from './geometry';
import * as tex from './textures';
function material(belag: tex.Belag, extra: MeshStandardMaterialParameters = {}) {
@@ -21,15 +21,28 @@ function material(belag: tex.Belag, extra: MeshStandardMaterialParameters = {})
...extra,
});
}
/**
* Travertinstelen: aufrecht gestellte Platten, 50 cm lang und 6 bis 9 cm dick.
* Sie stehen im Dekorkiesbett und wechseln sich mit den Eibengruppen ab - das
* ist der Sichtschutz des Gartens. Hoehe je nach Seite 1.50 oder 1.80 m.
*/
export function buildStelen(stelen: Stele[], height: (x: number, z: number) => number): Group {
const group = new Group();
group.name = 'Travertinstelen';
const stein = tex.travertinMauer();
const mat = material(stein, { roughness: 0.66 });
const stein = tex.travertinStele();
const mat = material(stein, { roughness: 0.62 });
const parts: BufferGeometry[] = [];
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, stein.tile);
const m = new Mesh(geo, mat);
const y0 = height(s.x, s.z) - 0.25;
// 2.5 cm Stossfuge: die Dreiergruppen des Plans sollen als einzelne
// Platten lesbar bleiben und nicht zu einer glatten Wand verschmelzen.
const len = s.len - 0.025;
const w = s.rot === 0 ? len : s.t;
const d = s.rot === 0 ? s.t : len;
parts.push(boxGeometry(s.x, s.z, w, d, y0, y0 + 0.25 + s.height, 0, stein.tile));
}
if (parts.length) {
const m = new Mesh(mergeGeometries(parts), mat);
m.castShadow = true;
m.receiveShadow = true;
group.add(m);
@@ -37,6 +50,32 @@ export function buildStelen(stelen: Stele[], height: (x: number, z: number) => n
return group;
}
/**
* Schwebende Travertinstufe: dicke Platte, die frei auskragt. Der Schattenfuge
* darunter verdankt sie ihre Wirkung, deshalb sitzt sie auf einem deutlich
* schmaleren Sockel.
*/
function schwebendeStufe(p: Prop, y: number): Group {
const g = new Group();
const belag = tex.travertinSlab();
const platte = material(belag, { roughness: 0.68 });
const top = p.top ?? y;
const slab = new Mesh(
boxGeometry(p.x, p.z, p.w, p.d, top - p.h, top, p.rot, belag.tile), platte,
);
slab.castShadow = true;
slab.receiveShadow = true;
g.add(slab);
// Unterbau, ringsum 12 cm eingezogen: erzeugt die Schattenfuge
const sockel = new Mesh(
boxGeometry(p.x, p.z, p.w - 0.24, p.d - 0.24, top - p.h - 0.30, top - p.h - 0.01, p.rot, 1),
new MeshStandardMaterial({ color: 0x3a3833, roughness: 0.9 }),
);
sockel.receiveShadow = true;
g.add(sockel);
return g;
}
// ---------------------------------------------------------------------------
// Freistehende Objekte
// ---------------------------------------------------------------------------
@@ -96,45 +135,52 @@ function spielhaus(p: Prop, y: number): Group {
}
/**
* Flammengrill: Cortenstahl-Kubus mit umlaufender Grillplatte und offener
* Feuerkammer, wie auf dem Referenzfoto des Plans.
* Flammengrill nach dem Referenzbild des Planers: anthrazitfarbener Stahlkubus,
* unten ein offenes Holzfach, oben ein perforiertes Band, in der Deckplatte die
* runde Feueroeffnung.
*/
function flammengrill(p: Prop, y: number): Group {
const g = new Group();
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 });
const stahl = new MeshStandardMaterial({ color: 0x4a4c50, roughness: 0.55, metalness: 0.45 });
const dunkel = new MeshStandardMaterial({ color: 0x25272a, roughness: 0.85, metalness: 0.2 });
const deckel = new MeshStandardMaterial({ color: 0x3f4144, roughness: 0.38, metalness: 0.68 });
const scheiter = material(tex.holzscheiter(), { roughness: 0.92 });
// 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 hw = p.w / 2, hd = p.d / 2;
const holzTop = y + p.h * 0.42; // Oberkante Holzfach
const bandTop = y + p.h - 0.04; // Unterkante Deckplatte
const bandBot = bandTop - 0.16; // perforiertes Kopfband
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);
// Holzfach: sichtbare Scheiter zwischen zwei Stahlwangen
const holz = new Mesh(boxGeometry(p.x, p.z, p.w - 0.06, p.d - 0.06, y + 0.03, holzTop, p.rot, 0.5), scheiter);
holz.receiveShadow = true;
g.add(holz);
for (const s of [-1, 1]) {
const wange = new Mesh(boxGeometry(p.x + s * hw, p.z, 0.04, p.d, y, holzTop, p.rot, 1), stahl);
wange.castShadow = true;
g.add(wange);
}
const rueck = new Mesh(boxGeometry(p.x, p.z + hd, p.w, 0.04, y, holzTop, p.rot, 1), stahl);
g.add(rueck);
// Geschlossener Korpus darueber
const body = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, holzTop, bandBot, p.rot, 1), stahl);
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);
// Perforiertes Band: einzelne Loecher waeren zu teuer, deshalb ein dunkleres,
// leicht zurueckversetztes Band mit rauer Oberflaeche
const band = new Mesh(boxGeometry(p.x, p.z, p.w - 0.01, p.d - 0.01, bandBot, bandTop, p.rot, 1), dunkel);
band.castShadow = true;
g.add(band);
// Grillplatte mit Ausschnitt fuer das Feuer
const plate = new Mesh(
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;
// Deckplatte mit runder Feueroeffnung
const plate = new Mesh(boxGeometry(p.x, p.z, p.w + 0.05, p.d + 0.05, bandTop, bandTop + 0.04, p.rot, 1), deckel);
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);
const loch = new Mesh(new CylinderGeometry(p.w * 0.19, p.w * 0.19, 0.06, 24), dunkel);
loch.position.set(p.x, bandTop + 0.035, p.z);
g.add(loch);
return g;
}
@@ -232,6 +278,7 @@ 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 'stufe': obj = schwebendeStufe(p, y); break;
case 'roboter': obj = roboterGarage(p, y); break;
case 'wasserhahn': obj = wasserhahn(p, y); break;
case 'schacht': obj = schachtdeckel(p, y); break;