Planabgleich Etappe A bis C: Zugaenge, Rasenrandplatten, Torstufen
Deploy App / deploy (push) Successful in 28s

Systematischer Abgleich Plan gegen Szene, Bereich fuer Bereich.

Neu gebaut
- Lineare Bauteile (LINEAR): Treppe und Rampe am Ost- und Westzugang,
  Stahlrohrgelaender laengs des Gefaelles, Entwaesserungsrinnen mit
  buendigem Rost. Geometrie aus den Trittlinien des Plans abgegriffen
  (Ost: 8 Trittlinien z -4.18 bis -6.52, Koten -0.37/-1.57; West:
  Trittlinien alle 20 cm ab x -21.94, Koten -0.28/-1.08).
- Rasenrandplatten: das durchgehende Band von 6 cm Breite entlang der
  Rasenkante wird als hochkant stehende Randplatte mit Sichtkante
  aufgebaut, nicht mehr als Belag (neuer Typ randplatte, 1.2 m2).
- Zwei schwebende Travertinstufen am Gartentor, Koten -0.13 und -0.32.
- Winkelelemente entlang der Suedgrenze, Tegula-Streifen des Nachbarn.

Korrigiert
- Holzscheiterwand: der Plan zeigt zwei Felder 1.50 x 0.46 m, nicht eines.
  Farbe #ffaa00 war faelschlich als Tegula klassiert.
- Gartentor: Fluegel laeuft ueber z, nicht ueber x - das Tor stand um 90
  Grad verdreht. Jetzt Stabgitter mit Pfosten und senkrechten Staeben
  statt geschlossenem schwarzem Blatt.
- Winkelelemente Ost auf x 3.735 (Plan: Doppellinie 3.69/3.78), Fussweg
  auf x 2.86 bis 3.69 zwischen Verbundstein-Anpassung und Winkelelementen.
- Rinne beim Gartentor auf die Schraffur des Plans (z 2.295, x -23.34
  bis -21.74).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 17:56:58 +02:00
co-authored by Claude Opus 5
parent ba9ce4ed21
commit 32ceba7de0
9 changed files with 364 additions and 26 deletions
+169 -11
View File
@@ -7,7 +7,7 @@ import {
BoxGeometry, BufferGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial,
PointLight, type MeshStandardMaterialParameters,
} from 'three';
import type { Lamp, Prop, Stele } from '../site';
import type { Lamp, Linear, Prop, Stele } from '../site';
import { boxGeometry, mergeGeometries } from './geometry';
import * as tex from './textures';
@@ -250,19 +250,40 @@ function schachtdeckel(p: Prop, y: number): Group {
return g;
}
/**
* Gartentor: Stabgitterfluegel aus Stahl. Zwei Pfosten, umlaufender Rahmen aus
* Flachstahl und senkrechte Staebe im Abstand von rund 11 cm - das Tor bleibt
* durchsichtig, es ist kein geschlossenes Blatt.
*/
function gartentor(p: Prop, y: number): Group {
const g = new Group();
const mat = new MeshStandardMaterial({ color: 0x4a4a4c, roughness: 0.5, metalness: 0.4 });
const frame = new Mesh(boxGeometry(p.x, p.z, p.w, 0.05, y, y + p.h, p.rot, 1), mat);
g.add(frame);
for (let i = 1; i < 6; i++) {
const bar = new Mesh(
boxGeometry(p.x - p.w / 2 + (i * p.w) / 6, p.z, 0.03, 0.03, y, y + p.h, p.rot, 1),
mat,
);
g.add(bar);
const mat = new MeshStandardMaterial({ color: 0x43464a, roughness: 0.45, metalness: 0.55 });
const a = (p.rot * Math.PI) / 180;
// Laengsrichtung des Fluegels in der Ebene, quer dazu die Blattdicke.
const ux = Math.cos(a), uz = Math.sin(a);
const at = (t: number): [number, number] => [p.x + ux * t, p.z + uz * t];
const parts: BufferGeometry[] = [];
const half = p.w / 2;
const top = y + p.h;
// Pfosten links und rechts, etwas hoeher als der Fluegel
for (const t of [-half - 0.05, half + 0.05]) {
const [x, z] = at(t);
parts.push(boxGeometry(x, z, 0.08, 0.08, y - 0.35, top + 0.06, p.rot, 1));
}
g.children.forEach((c) => { (c as Mesh).castShadow = true; });
// Rahmen: unterer und oberer Riegel
for (const [y0, y1] of [[y + 0.08, y + 0.14], [top - 0.08, top]] as const) {
parts.push(boxGeometry(p.x, p.z, p.w, 0.035, y0, y1, p.rot, 1));
}
// Senkrechte Staebe
const n = Math.max(4, Math.round(p.w / 0.11));
for (let i = 0; i <= n; i++) {
const [x, z] = at(-half + (i * p.w) / n);
parts.push(boxGeometry(x, z, 0.016, 0.016, y + 0.08, top, p.rot, 1));
}
const m = new Mesh(mergeGeometries(parts), mat);
m.castShadow = true;
g.add(m);
return g;
}
@@ -344,3 +365,140 @@ export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => numb
},
};
}
// ---------------------------------------------------------------------------
// Lineare Bauteile: Treppen mit Rampe, Gelaender, Entwaesserungsrinnen
// ---------------------------------------------------------------------------
/**
* Winkel fuer boxGeometry, so dass die lokale v-Achse (Tiefe d) entlang der
* Richtung (dx, dz) liegt. Aus world = (cx + u*cos - v*sin, y, cz + u*sin + v*cos)
* folgt v-Achse = (-sin, cos), also sin = -dx und cos = dz.
*/
function axisAngle(dx: number, dz: number): number {
return (Math.atan2(-dx, dz) * 180) / Math.PI;
}
/**
* Blockstufentreppe. Der Pfad laeuft von oben nach unten; jede Stufe ist ein
* Travertinblock, dessen Trittflaeche eine Steigung tiefer liegt als die
* vorige. Die Bloecke reichen bis unter das untere Niveau, damit die Treppe
* auch von der Seite geschlossen wirkt.
*/
function blockTreppe(f: Linear): Group {
const g = new Group();
const belag = tex.travertinSlab();
const mat = material(belag, { roughness: 0.6 });
const [[x0, z0], [x1, z1]] = [f.path[0], f.path[f.path.length - 1]];
const dx = x1 - x0, dz = z1 - z0;
const len = Math.hypot(dx, dz) || 1;
const rot = axisAngle(dx / len, dz / len);
const steps = f.steps ?? 4;
const width = f.width ?? 1.2;
const fromY = f.fromY ?? 0, toY = f.toY ?? -1;
const riser = (fromY - toY) / steps;
const tread = len / steps;
const parts: BufferGeometry[] = [];
for (let i = 0; i < steps; i++) {
const t = (i + 0.5) / steps;
const top = fromY - (i + 1) * riser;
parts.push(boxGeometry(
x0 + dx * t, z0 + dz * t, width, tread + 0.01,
toY - 0.45, top, rot, belag.tile,
));
}
const m = new Mesh(mergeGeometries(parts), mat);
m.castShadow = true;
m.receiveShadow = true;
g.add(m);
return g;
}
/**
* Gelaender aus Stahlrohr: Pfosten alle rund 1.1 m, Handlauf und Knieholm
* folgen dem Gefaelle des Laufs.
*/
function gelaender(f: Linear): Group {
const g = new Group();
const mat = new MeshStandardMaterial({ color: 0x4c4f52, roughness: 0.42, metalness: 0.65 });
const [[x0, z0], [x1, z1]] = [f.path[0], f.path[f.path.length - 1]];
const dx = x1 - x0, dz = z1 - z0;
const len = Math.hypot(dx, dz) || 1;
const rot = axisAngle(dx / len, dz / len);
const fromY = f.fromY ?? 0, toY = f.toY ?? -1;
const h = f.height ?? 0.95;
const parts: BufferGeometry[] = [];
const n = Math.max(2, Math.round(len / 1.1) + 1);
for (let i = 0; i < n; i++) {
const t = i / (n - 1);
const y = fromY + (toY - fromY) * t;
parts.push(boxGeometry(x0 + dx * t, z0 + dz * t, 0.05, 0.05, y - 0.35, y + h, rot, 1));
}
// Holme: als flache Bloecke ueber die ganze Laenge, um die Neigung gelegt.
for (const [level, thick] of [[h, 0.055], [h * 0.52, 0.04]] as const) {
const yA = fromY + level, yB = toY + level;
const rise = yA - yB;
const holmLen = Math.hypot(len, rise);
const geo = boxGeometry(0, 0, thick, holmLen, -thick / 2, thick / 2, 0, 1);
geo.rotateX(Math.atan2(rise, len));
geo.rotateY((rot * Math.PI) / 180);
geo.translate((x0 + x1) / 2, (yA + yB) / 2, (z0 + z1) / 2);
parts.push(geo);
}
const m = new Mesh(mergeGeometries(parts), mat);
m.castShadow = true;
g.add(m);
return g;
}
/**
* Entwaesserungsrinne: schmaler Kanal im Belag mit buendigem Stahlrost.
*/
function rinne(f: Linear, height: (x: number, z: number) => number): Group {
const g = new Group();
const beton = new MeshStandardMaterial({ color: 0x8e8a84, roughness: 0.85 });
const rost = new MeshStandardMaterial({ color: 0x6a6d70, roughness: 0.4, metalness: 0.7 });
const w = f.width ?? 0.15;
for (let i = 1; i < f.path.length; i++) {
const [ax, az] = f.path[i - 1], [bx, bz] = f.path[i];
const dx = bx - ax, dz = bz - az;
const len = Math.hypot(dx, dz) || 1;
const rot = axisAngle(dx / len, dz / len);
const cx = (ax + bx) / 2, cz = (az + bz) / 2;
const y = height(cx, cz);
const kasten = new Mesh(boxGeometry(cx, cz, w + 0.09, len, y - 0.30, y - 0.015, rot, 1), beton);
kasten.receiveShadow = true;
g.add(kasten);
// Rost in Segmenten von 50 cm, damit die Stossfugen sichtbar bleiben.
const seg = Math.max(1, Math.round(len / 0.5));
for (let s = 0; s < seg; s++) {
const t = (s + 0.5) / seg;
const r = new Mesh(
boxGeometry(ax + dx * t, az + dz * t, w, len / seg - 0.012, y - 0.035, y + 0.004, rot, 1),
rost,
);
r.receiveShadow = true;
g.add(r);
}
}
return g;
}
/** Baut Treppen, Gelaender und Rinnen aus dem Plan. */
export function buildLinear(features: Linear[], height: (x: number, z: number) => number): Group {
const group = new Group();
group.name = 'Treppen, Gelaender und Rinnen';
for (const f of features) {
let obj: Group;
switch (f.kind) {
case 'treppe': obj = blockTreppe(f); break;
case 'gelaender': obj = gelaender(f); break;
case 'rinne': obj = rinne(f, height); break;
default: continue;
}
obj.name = f.id;
obj.userData.note = f.note;
group.add(obj);
}
return group;
}