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
+9 -1
View File
@@ -13,7 +13,7 @@ import './style.css';
import { loadSite, makeHeightSampler, type SiteData } from './site';
import { buildTerrain } from './scene/terrain';
import { buildSurfaces } from './scene/surfaces';
import { buildStelen, buildProps, buildLamps } from './scene/structures';
import { buildStelen, buildProps, buildLamps, buildLinear } from './scene/structures';
import { buildWalls } from './scene/walls';
import { buildVegetation } from './scene/vegetation';
import { buildBuilding, buildNeighbourBuildings } from './scene/building';
@@ -59,6 +59,7 @@ async function main() {
scene.add(buildWalls(site.walls, height));
scene.add(buildStelen(site.stelen, height));
scene.add(buildProps(site.props, height));
scene.add(buildLinear(site.linear, height));
await progress(66, 'Haus und Nachbarschaft …');
scene.add(buildBuilding(site.building));
@@ -104,6 +105,13 @@ async function main() {
obstacles.push(obstacleFromSegment(w.path[i - 1], w.path[i], w.t + 0.1));
}
}
// Gelaender begrenzen den Rundgang, Treppen und Rinnen bleiben begehbar.
for (const f of site.linear) {
if (f.kind !== 'gelaender') continue;
for (let i = 1; i < f.path.length; i++) {
obstacles.push(obstacleFromSegment(f.path[i - 1], f.path[i], 0.14));
}
}
for (const p of site.props) {
if (p.type === 'schacht' || p.type === 'wasserhahn') continue;
obstacles.push({ cx: p.x, cz: p.z, hw: p.w / 2, hd: p.d / 2, rot: (p.rot * Math.PI) / 180 });
+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;
}
+14
View File
@@ -32,6 +32,11 @@ interface BelagDef {
edging?: boolean;
/** Material der Sichtkante, falls abweichend. */
edge?: () => tex.Belag;
/**
* Randplatte: folgt dem Gelaende wie eine gewachsene Flaeche, steht aber
* mit einer sichtbaren Kante darueber. Fuer die Rasenrandplatten.
*/
kerb?: boolean;
}
const BELAG: Record<SurfaceType, BelagDef> = {
@@ -45,6 +50,7 @@ const BELAG: Record<SurfaceType, BelagDef> = {
rasen: { belag: tex.rasen, label: 'Rasen', lift: 0.012, built: false },
schotterwiese: { belag: tex.schotterwiese, label: 'Schotterwiese', lift: 0.010, built: false },
beet: { belag: tex.beet, label: 'Pflanzbeet', lift: 0.008, built: false, edging: true },
randplatte: { belag: tex.travertinMauer, label: 'Rasenrandplatten', lift: 0.045, built: false, kerb: true },
};
const STAHL = () => new MeshStandardMaterial({ color: 0x8a877f, roughness: 0.55, metalness: 0.62 });
@@ -100,6 +106,14 @@ export function buildSurfaces(
tops.push(surfaceGeometryMulti([s.polygon], height, {
offset: def.lift, tile: belag.tile, maxEdge: 0.28,
}));
if (def.kerb) {
sides.push(surfaceEdgeGeometry(
s.polygon,
(x, z) => height(x, z) + def.lift,
(x, z) => height(x, z) - 0.22,
belag.tile,
));
}
if (def.edging) {
edgingParts.push(edgingGeometry(s.polygon, (x, z) => height(x, z) + def.lift));
}
+23 -1
View File
@@ -11,7 +11,8 @@ export type Vec2 = [number, number];
export type SurfaceType =
| 'travertin80' | 'dekorkies' | 'travertinstell' | 'keramik'
| 'verbundstein' | 'tegula' | 'rasen' | 'schotterwiese' | 'beet' | 'spielsand';
| 'verbundstein' | 'tegula' | 'rasen' | 'schotterwiese' | 'beet' | 'spielsand'
| 'randplatte';
export interface Surface {
type: SurfaceType;
@@ -105,6 +106,26 @@ export interface Stele {
side: 'sued' | 'nord' | 'west';
}
/**
* Lineares Bauteil entlang einer Achse: Treppe mit Rampe, Gelaender oder
* Entwaesserungsrinne. Der Plan zeichnet diese nur als Linienbuendel.
*/
export interface Linear {
id: string;
kind: 'treppe' | 'gelaender' | 'rinne';
note?: string;
path: Vec2[];
/** Breite quer zur Achse (Treppe, Rinne). */
width?: number;
/** Hoehe an Anfang und Ende der Achse. */
fromY?: number;
toY?: number;
/** Anzahl Blockstufen. */
steps?: number;
/** Gelaenderhoehe ueber dem Lauf. */
height?: number;
}
export interface Opening {
face: 'north' | 'south' | 'east' | 'west';
a: number;
@@ -174,6 +195,7 @@ export interface SiteData {
lamps: Lamp[];
walls: Wall[];
props: Prop[];
linear: Linear[];
stelen: Stele[];
building: Building;
neighbourBuildings: NeighbourBuilding[];
+7 -1
View File
@@ -90,6 +90,7 @@ export function setupHud(opts: HudOptions): Hud {
travertin80: 'Travertinplatten 80×80×3 cm',
dekorkies: 'Dekorkies',
travertinstell: 'Travertinstellplatten',
randplatte: 'Rasenrandplatten',
keramik: 'Keramikplatten 60×60×2 cm',
verbundstein: 'Verbundsteine (Nachbar)',
tegula: 'Tegula-Pflastersteine grau',
@@ -140,7 +141,12 @@ export function setupHud(opts: HudOptions): Hud {
<li>${site.walls.length} Mauern und Wände</li>
<li>${site.lamps.length} Leuchten &ndash; sie schalten sich abends automatisch ein</li>
<li>${site.slabs.length} lose Travertinplatten 40 × 60 × 3 cm im Dekorkies</li>
<li>Gartenhaus 1,8 × 1,8 m, Spielhaus, Flammengrill mit Holzscheiterwand</li>
<li>Rasenrandplatten als 6 cm schmale Stellplatten entlang der Rasenkante</li>
<li>Gartenhaus 1,8 × 1,8 m, Spielhaus, Flammengrill mit zwei Holzscheiterwänden
(je 150 × 200 cm)</li>
<li>Zwei schwebende Travertinstufen am Gartentor, Stabgittertor 100 cm</li>
<li>${site.linear.filter((l) => l.kind === 'treppe').length} Treppen mit Rampe
(Ost- und Westzugang), Geländer und Entwässerungsrinnen</li>
</ul>
<h3>Bedienung</h3>