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
+19
View File
@@ -130,6 +130,12 @@ aus (`src/scene/walls.ts`):
- **Lamellenwand** – senkrechte Bretter im Rhythmus des Plans (10 cm breit, - **Lamellenwand** – senkrechte Bretter im Rhythmus des Plans (10 cm breit,
35 cm tief, 90 cm Achsabstand) vor dem gedeckten Sitzplatz. 35 cm tief, 90 cm Achsabstand) vor dem gedeckten Sitzplatz.
- **Holzscheiterwand** – Stahlrahmen, gefüllt mit gestapelten Scheitern. - **Holzscheiterwand** – Stahlrahmen, gefüllt mit gestapelten Scheitern.
- **Treppen mit Rampe, Geländer und Rinnen** – die linearen Bauteile aus
`LINEAR` in `tools/authoring.mjs`, gerendert in `src/scene/structures.ts`:
Blockstufen aus Travertin, Stahlrohrgeländer mit Hand- und Knieholm entlang
des Gefälles, Entwässerungsrinnen mit bündigem Rost.
- **Gartentor** – Stabgitterflügel von 100 cm mit zwei Pfosten und senkrechten
Stäben, nicht als geschlossenes Blatt.
Die **Eibengruppen** entstehen nicht aus Einzelkugeln: die Pflanzstandorte des Die **Eibengruppen** entstehen nicht aus Einzelkugeln: die Pflanzstandorte des
Plans werden gruppenweise verkettet, begradigt, an scharfen Knicken getrennt und Plans werden gruppenweise verkettet, begradigt, an scharfen Knicken getrennt und
@@ -139,6 +145,19 @@ Abschnitt „Der Sichtschutz". Ebenso die Ligusterhecke (170 cm, 75 cm breit).
Die **Travertinplatten 40 × 60 × 3 cm** liegen laut Plan einzeln im Dekorkies – Die **Travertinplatten 40 × 60 × 3 cm** liegen laut Plan einzeln im Dekorkies –
30 Platten, exakt an den Planpositionen, als erhabene Platten auf dem Kiesbett. 30 Platten, exakt an den Planpositionen, als erhabene Platten auf dem Kiesbett.
Die **Rasenrandplatten** sind im Plan ein durchgehendes Band von 6 cm Breite
zwischen Rasen und Kiesbett des Sichtschutzes. Sie werden nicht als Belag,
sondern als hochkant stehende Randplatte mit sichtbarer Kante aufgebaut
(Belagstyp `randplatte`).
Am **Gartentor** liegen ebenfalls zwei schwebende Travertinstufen: der Plan
zeichnet dort ein Feld von 0.80 × 1.60 m mit den Koten −0.13 (Podest am Tor)
und −0.32 (Anschluss an den Hauptsitzplatz).
Der Plan zeigt **zwei** orange markierte Felder von 1.50 × 0.46 m in der
Sichtschutzlinie; die Beschriftung „Holzscheiterwand 150 cm x 200 cm" steht
zwischen ihnen und gilt für beide.
Materialien und Ausbildung folgen den **Referenzbildern im Plan-PDF**: heller, Materialien und Ausbildung folgen den **Referenzbildern im Plan-PDF**: heller,
geschichteter Travertin für Mauern, Stelen und Platten, die schwebenden Stufen geschichteter Travertin für Mauern, Stelen und Platten, die schwebenden Stufen
als auskragende Platten mit Schattenfuge, der Flammengrill als anthrazitfarbener als auskragende Platten mit Schattenfuge, der Flammengrill als anthrazitfarbener
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -13,7 +13,7 @@ import './style.css';
import { loadSite, makeHeightSampler, type SiteData } from './site'; import { loadSite, makeHeightSampler, type SiteData } from './site';
import { buildTerrain } from './scene/terrain'; import { buildTerrain } from './scene/terrain';
import { buildSurfaces } from './scene/surfaces'; 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 { buildWalls } from './scene/walls';
import { buildVegetation } from './scene/vegetation'; import { buildVegetation } from './scene/vegetation';
import { buildBuilding, buildNeighbourBuildings } from './scene/building'; import { buildBuilding, buildNeighbourBuildings } from './scene/building';
@@ -59,6 +59,7 @@ async function main() {
scene.add(buildWalls(site.walls, height)); scene.add(buildWalls(site.walls, height));
scene.add(buildStelen(site.stelen, height)); scene.add(buildStelen(site.stelen, height));
scene.add(buildProps(site.props, height)); scene.add(buildProps(site.props, height));
scene.add(buildLinear(site.linear, height));
await progress(66, 'Haus und Nachbarschaft …'); await progress(66, 'Haus und Nachbarschaft …');
scene.add(buildBuilding(site.building)); 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)); 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) { for (const p of site.props) {
if (p.type === 'schacht' || p.type === 'wasserhahn') continue; 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 }); 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, BoxGeometry, BufferGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial,
PointLight, type MeshStandardMaterialParameters, PointLight, type MeshStandardMaterialParameters,
} from 'three'; } from 'three';
import type { Lamp, Prop, Stele } from '../site'; import type { Lamp, Linear, Prop, Stele } from '../site';
import { boxGeometry, mergeGeometries } from './geometry'; import { boxGeometry, mergeGeometries } from './geometry';
import * as tex from './textures'; import * as tex from './textures';
@@ -250,19 +250,40 @@ function schachtdeckel(p: Prop, y: number): Group {
return g; 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 { function gartentor(p: Prop, y: number): Group {
const g = new Group(); const g = new Group();
const mat = new MeshStandardMaterial({ color: 0x4a4a4c, roughness: 0.5, metalness: 0.4 }); const mat = new MeshStandardMaterial({ color: 0x43464a, roughness: 0.45, metalness: 0.55 });
const frame = new Mesh(boxGeometry(p.x, p.z, p.w, 0.05, y, y + p.h, p.rot, 1), mat); const a = (p.rot * Math.PI) / 180;
g.add(frame); // Laengsrichtung des Fluegels in der Ebene, quer dazu die Blattdicke.
for (let i = 1; i < 6; i++) { const ux = Math.cos(a), uz = Math.sin(a);
const bar = new Mesh( const at = (t: number): [number, number] => [p.x + ux * t, p.z + uz * t];
boxGeometry(p.x - p.w / 2 + (i * p.w) / 6, p.z, 0.03, 0.03, y, y + p.h, p.rot, 1), const parts: BufferGeometry[] = [];
mat, const half = p.w / 2;
); const top = y + p.h;
g.add(bar);
// 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; 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; edging?: boolean;
/** Material der Sichtkante, falls abweichend. */ /** Material der Sichtkante, falls abweichend. */
edge?: () => tex.Belag; 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> = { 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 }, rasen: { belag: tex.rasen, label: 'Rasen', lift: 0.012, built: false },
schotterwiese: { belag: tex.schotterwiese, label: 'Schotterwiese', lift: 0.010, 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 }, 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 }); 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, { tops.push(surfaceGeometryMulti([s.polygon], height, {
offset: def.lift, tile: belag.tile, maxEdge: 0.28, 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) { if (def.edging) {
edgingParts.push(edgingGeometry(s.polygon, (x, z) => height(x, z) + def.lift)); 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 = export type SurfaceType =
| 'travertin80' | 'dekorkies' | 'travertinstell' | 'keramik' | 'travertin80' | 'dekorkies' | 'travertinstell' | 'keramik'
| 'verbundstein' | 'tegula' | 'rasen' | 'schotterwiese' | 'beet' | 'spielsand'; | 'verbundstein' | 'tegula' | 'rasen' | 'schotterwiese' | 'beet' | 'spielsand'
| 'randplatte';
export interface Surface { export interface Surface {
type: SurfaceType; type: SurfaceType;
@@ -105,6 +106,26 @@ export interface Stele {
side: 'sued' | 'nord' | 'west'; 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 { export interface Opening {
face: 'north' | 'south' | 'east' | 'west'; face: 'north' | 'south' | 'east' | 'west';
a: number; a: number;
@@ -174,6 +195,7 @@ export interface SiteData {
lamps: Lamp[]; lamps: Lamp[];
walls: Wall[]; walls: Wall[];
props: Prop[]; props: Prop[];
linear: Linear[];
stelen: Stele[]; stelen: Stele[];
building: Building; building: Building;
neighbourBuildings: NeighbourBuilding[]; neighbourBuildings: NeighbourBuilding[];
+7 -1
View File
@@ -90,6 +90,7 @@ export function setupHud(opts: HudOptions): Hud {
travertin80: 'Travertinplatten 80×80×3 cm', travertin80: 'Travertinplatten 80×80×3 cm',
dekorkies: 'Dekorkies', dekorkies: 'Dekorkies',
travertinstell: 'Travertinstellplatten', travertinstell: 'Travertinstellplatten',
randplatte: 'Rasenrandplatten',
keramik: 'Keramikplatten 60×60×2 cm', keramik: 'Keramikplatten 60×60×2 cm',
verbundstein: 'Verbundsteine (Nachbar)', verbundstein: 'Verbundsteine (Nachbar)',
tegula: 'Tegula-Pflastersteine grau', 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.walls.length} Mauern und Wände</li>
<li>${site.lamps.length} Leuchten &ndash; sie schalten sich abends automatisch ein</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>${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> </ul>
<h3>Bedienung</h3> <h3>Bedienung</h3>
+105 -9
View File
@@ -25,7 +25,7 @@ export const SURFACE_BY_COLOR = {
'#bbbbbb': 'verbundstein', // Anpassung Verbundsteine Nachbar '#bbbbbb': 'verbundstein', // Anpassung Verbundsteine Nachbar
'#ddba99': 'beet', '#ddba99': 'beet',
'#c0bab4': 'spielsand', '#c0bab4': 'spielsand',
'#ffaa00': 'tegula', // Tegula-Pflastersteine grau (im Plan orange markiert) '#ffaa00': 'holzscheiter', // orange markiert: die beiden Holzscheiterwaende im Sichtschutz
'#ddd299': 'travertinstell', '#ddd299': 'travertinstell',
'#73e900': 'ligusterhecke', '#73e900': 'ligusterhecke',
'#cccccc': 'winkelelement', '#cccccc': 'winkelelement',
@@ -137,6 +137,22 @@ export const WALLS = [
note: 'Schenkel der Travertinsitzmauer laengs des Rasens, gleicher Hoehensprung', note: 'Schenkel der Travertinsitzmauer laengs des Rasens, gleicher Hoehensprung',
path: [[-18.44, 0.55], [-18.44, 3.94]], t: 0.40, top: -0.02, base: -0.55, seating: true, path: [[-18.44, 0.55], [-18.44, 3.94]], t: 0.40, top: -0.02, base: -0.55, seating: true,
}, },
{
// Der Plan beschriftet die Linien an der Ostgrenze mit "Winkelelemente":
// sie stuetzen den Garten gegen den tiefer liegenden Fussweg ab.
id: 'winkelelemente-ost', type: 'beton',
note: 'Winkelelemente aus Beton an der Ostgrenze, gegen den Fussweg',
path: [[3.735, -6.90], [3.735, 4.90]],
t: 0.14, top: null, base: null, retains: 'ost',
},
{
// Der Plan beschriftet die Linie bei z 4.96 mit "Winkelelemente": sie
// fasst das hoeher liegende Nachbargrundstueck hinter dem Sichtschutz.
id: 'winkelelemente-sued', type: 'beton',
note: 'Winkelelemente aus Beton entlang der Suedgrenze',
path: [[-21.20, 4.96], [1.99, 4.96]],
t: 0.14, top: null, base: null, retains: 'sued',
},
{ {
id: 'betonmauer-nachbar', type: 'beton', id: 'betonmauer-nachbar', type: 'beton',
note: 'Bestehende Betonmauer des Suednachbarn entlang der Grenze', note: 'Bestehende Betonmauer des Suednachbarn entlang der Grenze',
@@ -147,15 +163,23 @@ export const WALLS = [
note: 'Rueckspringender Schenkel der Nachbarmauer', note: 'Rueckspringender Schenkel der Nachbarmauer',
path: [[2.09, 6.07], [2.09, 9.05]], t: 0.20, top: 1.10, base: -0.60, path: [[2.09, 6.07], [2.09, 9.05]], t: 0.20, top: 1.10, base: -0.60,
}, },
// Der Plan zeigt zwei orange markierte Felder von 1.50 x 0.46 m in der
// Sichtschutzlinie; die Beschriftung "Holzscheiterwand 150 cm x 200 cm"
// steht zwischen ihnen und gilt fuer beide.
{ {
id: 'holzscheiterwand', type: 'holzscheiter', id: 'holzscheiterwand-west', type: 'holzscheiter',
note: 'Holzscheiterwand 150 cm breit, 200 cm hoch, hinter dem Flammengrill', note: 'Holzscheiterwand 150 cm breit, 200 cm hoch, hinter dem Flammengrill',
path: [[-5.75, 4.35], [-4.25, 4.35]], t: 0.45, top: 2.26, base: 0.20, path: [[-6.08, 4.18], [-4.58, 4.18]], t: 0.46, top: 2.32, base: 0.22,
}, },
{ {
id: 'stufe-gartentor', type: 'travertin', id: 'holzscheiterwand-ost', type: 'holzscheiter',
note: 'Blockstufen beim Gartentor an der Suedwestecke', note: 'Zweite Holzscheiterwand 150 cm breit, 200 cm hoch',
path: [[-21.05, 2.15], [-21.05, 3.75]], t: 0.80, top: -0.18, base: -0.80, path: [[-8.74, 4.18], [-7.24, 4.18]], t: 0.46, top: 2.28, base: 0.18,
},
{
id: 'stufe-nordwest', type: 'travertin',
note: 'Stufe an der Nordwestecke, vom Sitzplatz auf die Schotterwiese',
path: [[-20.90, -7.29], [-20.10, -7.29]], t: 0.42, top: -0.33, base: -0.95,
}, },
{ {
id: 'lamellenwand', type: 'lamellen', id: 'lamellenwand', type: 'lamellen',
@@ -179,7 +203,9 @@ export const PROPS = [
{ id: 'wasserhahn', type: 'wasserhahn', x: -13.90, z: -4.60, w: 0.12, d: 0.12, h: 0.85, rot: 0 }, { id: 'wasserhahn', type: 'wasserhahn', x: -13.90, z: -4.60, w: 0.12, d: 0.12, h: 0.85, rot: 0 },
{ id: 'schlaufschacht', type: 'schacht', x: -14.35, z: -3.95, w: 0.30, d: 0.30, h: 0.02, rot: 0, { id: 'schlaufschacht', type: 'schacht', x: -14.35, z: -3.95, w: 0.30, d: 0.30, h: 0.02, rot: 0,
note: 'Schlaufschacht D = 30 cm' }, note: 'Schlaufschacht D = 30 cm' },
{ id: 'gartentor', type: 'tor', x: -21.60, z: 3.30, w: 1.00, d: 0.06, h: 1.10, rot: 0 }, // Der Torfluegel liegt im Plan bei x -21.53 und laeuft ueber z 2.56 bis 3.56.
{ id: 'gartentor', type: 'tor', x: -21.53, z: 3.06, w: 1.00, d: 0.06, h: 1.20, rot: 90,
note: 'Gartentor 100 cm, Stabgitter aus Stahl' },
// Zwei schwebende Travertinplatten fuehren vom gedeckten Sitzplatz (-0.02) // Zwei schwebende Travertinplatten fuehren vom gedeckten Sitzplatz (-0.02)
// auf den Hauptsitzplatz (-0.33). Lage und Format stehen im Plan, die // auf den Hauptsitzplatz (-0.33). Lage und Format stehen im Plan, die
// Ausbildung als auskragende Platte zeigt das Referenzbild des Planers. // Ausbildung als auskragende Platte zeigt das Referenzbild des Planers.
@@ -187,6 +213,13 @@ export const PROPS = [
top: -0.17, note: 'Schwebende Travertinstufe, Tritthoehe -0.17' }, top: -0.17, note: 'Schwebende Travertinstufe, Tritthoehe -0.17' },
{ id: 'stufe-unten', type: 'stufe', x: -14.880, z: -1.480, w: 0.80, d: 3.20, h: 0.08, rot: 0, { id: 'stufe-unten', type: 'stufe', x: -14.880, z: -1.480, w: 0.80, d: 3.20, h: 0.08, rot: 0,
top: -0.31, note: 'Schwebende Travertinstufe, Tritthoehe -0.31' }, top: -0.31, note: 'Schwebende Travertinstufe, Tritthoehe -0.31' },
// Beim Gartentor liegen ebenfalls zwei schwebende Travertinstufen: der Plan
// zeichnet ein Feld 0.80 x 1.60 m mit den Koten -0.13 (Podest am Tor) und
// -0.32 (Anschluss an den Hauptsitzplatz).
{ id: 'stufe-tor-oben', type: 'stufe', x: -21.05, z: 3.35, w: 0.80, d: 0.80, h: 0.08, rot: 0,
top: -0.13, note: 'Schwebende Travertinstufe am Gartentor, Tritthoehe -0.13' },
{ id: 'stufe-tor-unten', type: 'stufe', x: -21.05, z: 2.55, w: 0.80, d: 0.80, h: 0.08, rot: 0,
top: -0.32, note: 'Schwebende Travertinstufe am Gartentor, Tritthoehe -0.32' },
]; ];
/** /**
@@ -201,8 +234,27 @@ export const EXTRA_SURFACES = [
}, },
{ {
type: 'tegula', type: 'tegula',
note: 'Feuerwehrzufahrt und Zugangsweg, Tegula-Pflastersteine grau', note: 'Feuerwehrzufahrt und Zugangsweg westlich der Grenze, Tegula-Pflastersteine grau',
polygon: [[-25.60, -9.00], [-22.10, -9.00], [-22.10, 6.40], [-25.60, 6.40]], polygon: [[-25.60, -13.50], [-22.10, -13.50], [-22.10, 6.40], [-25.60, 6.40]],
},
{
// Der Plan zeigt oestlich der Grenze einen gepflasterten Weg, erst dahinter
// beginnt die Wiese. Zwei Leuchten stehen daran.
type: 'tegula',
note: 'Fussweg an der Ostseite zwischen Verbundstein-Anpassung und Winkelelementen',
polygon: [[2.86, -7.00], [3.69, -7.00], [3.69, 4.90], [2.86, 4.90]],
},
{
// Schmaler gepflasterter Streifen jenseits der Suedgrenze, zwischen
// Grenze (z 4.49) und den Winkelelementen des Nachbarn (z 4.96).
type: 'tegula',
note: 'Tegula-Streifen des Suednachbarn entlang der Grenze',
polygon: [[-21.74, 4.49], [1.99, 4.49], [1.99, 4.96], [-21.74, 4.96]],
},
{
type: 'tegula',
note: 'Stellplatte und Sitzplatz des Nachbarn im Nordosten',
polygon: [[0.40, -11.20], [4.60, -11.20], [4.60, -7.30], [0.40, -7.30]],
}, },
{ {
// Die schwebenden Stufen im Plan: eine Zwischenebene zwischen dem // Die schwebenden Stufen im Plan: eine Zwischenebene zwischen dem
@@ -214,6 +266,50 @@ export const EXTRA_SURFACES = [
}, },
]; ];
/**
* Lineare Bauteile, die der Plan benennt, aber nur als Linienbuendel zeichnet:
* Treppen mit Rampe, die zugehoerigen Gelaender und die Entwaesserungsrinnen.
*
* `path` ist die Achse in Laufrichtung, `fromY`/`toY` die Hoehe an Anfang und
* Ende. Die Hoehen stammen aus den Koten beidseits des jeweiligen Bauteils.
*/
export const LINEAR = [
{
// Der Plan zeichnet acht Trittlinien bei z -4.18 bis -6.52 ueber die
// Breite x 2.17 bis 3.78; die Koten daneben lauten -0.37 und -1.57.
id: 'treppe-ost', kind: 'treppe',
note: 'Treppe und Rampe hinunter zum Stellplatz des Nachbarn (-0.37 auf -1.57)',
path: [[2.975, -4.18], [2.975, -6.52]], width: 1.61, fromY: -0.37, toY: -1.57, steps: 7,
},
{
id: 'gelaender-ost', kind: 'gelaender',
note: 'Gelaender laengs der Osttreppe',
path: [[2.17, -4.18], [2.17, -6.52]], fromY: -0.37, toY: -1.57, height: 0.95,
},
{
// Trittlinien bei x -21.94 bis -22.54 im Abstand von 20 cm, Block bis
// x -23.34; Koten -0.28 oben und -1.08 auf der Feuerwehrzufahrt.
id: 'treppe-west', kind: 'treppe',
note: 'Treppe und Rampe von der Feuerwehrzufahrt zum Gartentor (-0.28 auf -1.08)',
path: [[-21.84, 1.37], [-22.64, 1.37]], width: 1.70, fromY: -0.28, toY: -1.08, steps: 4,
},
{
id: 'gelaender-west', kind: 'gelaender',
note: 'Gelaender an der Treppe zur Feuerwehrzufahrt',
path: [[-21.84, 0.52], [-22.64, 0.52]], fromY: -0.28, toY: -1.08, height: 0.95,
},
{
id: 'rinne-gartentor', kind: 'rinne',
note: 'Entwaesserungsrinne vor dem Gartentor',
path: [[-23.34, 2.295], [-21.74, 2.295]], width: 0.15,
},
{
id: 'edelstahlrinne', kind: 'rinne',
note: 'Edelstahlrinne laengs der Fassade am Hauptsitzplatz',
path: [[-13.98, -6.55], [-13.98, -2.95]], width: 0.13,
},
];
/** /**
* Hoehe des Sichtschutzes. Er besteht aus aufrechten Travertinstelen * Hoehe des Sichtschutzes. Er besteht aus aufrechten Travertinstelen
* (50 x 6 cm im Grundriss) und Eibengruppen, die sich abwechseln und * (50 x 6 cm im Grundriss) und Eibengruppen, die sich abwechseln und
+17 -2
View File
@@ -8,7 +8,7 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { import {
SURFACE_BY_COLOR, PLANT_BY_COLOR, TREES, WALLS, PROPS, LEVELS, STELE_HEIGHTS, SURFACE_BY_COLOR, PLANT_BY_COLOR, TREES, WALLS, PROPS, LEVELS, STELE_HEIGHTS, LINEAR,
BUILDING, NEIGHBOUR_BUILDINGS, TERRAIN_ZONES, EXTRA_SPOTS, EXTRA_SURFACES, BUILDING, NEIGHBOUR_BUILDINGS, TERRAIN_ZONES, EXTRA_SPOTS, EXTRA_SURFACES,
START_VIEWS, SITE_LOCATION, START_VIEWS, SITE_LOCATION,
} from './authoring.mjs'; } from './authoring.mjs';
@@ -85,7 +85,7 @@ function fitPlane(points) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** Bauteile, die als Mauer modelliert werden - nicht als begehbare Flaeche. */ /** Bauteile, die als Mauer modelliert werden - nicht als begehbare Flaeche. */
const NOT_A_SURFACE = new Set(['travertinmauer', 'winkelelement', 'spielhaus', 'gartenhaus', 'roboter', 'ligusterhecke']); const NOT_A_SURFACE = new Set(['travertinmauer', 'winkelelement', 'spielhaus', 'gartenhaus', 'roboter', 'ligusterhecke', 'holzscheiter']);
const bboxOf = (pts) => { const bboxOf = (pts) => {
let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity; let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity;
@@ -133,6 +133,19 @@ for (const s of raw.shapes) {
} }
} }
// Rasenrandplatten: derselbe Ton wie die Stelen, aber als durchgehendes
// Band von 6 cm Breite entlang der Rasenkante. Sie stehen hochkant und
// trennen den Rasen vom Kiesbett des Sichtschutzes.
const laenge = Math.max(b.w, b.d);
if (s.hex === '#ddd299' && laenge > 0.8 && s.area / laenge < 0.15) {
const poly = polygonArea(s.pts) < 0 ? [...s.pts].reverse() : s.pts;
surfaces.push({
type: 'randplatte', area: round(s.area, 2),
polygon: poly.map((p) => [round(p[0]), round(p[1])]),
});
continue;
}
// Der Plan verwendet denselben hellen Ton fuer Dekorkies und fuer die // Der Plan verwendet denselben hellen Ton fuer Dekorkies und fuer die
// Keramikterrasse des Nachbarn; unterschieden wird ueber die Lage. // Keramikterrasse des Nachbarn; unterschieden wird ueber die Lage.
if (type === 'dekorkies' && cz < -6.6) type = 'keramik'; if (type === 'dekorkies' && cz < -6.6) type = 'keramik';
@@ -466,6 +479,7 @@ const site = {
lamps, lamps,
walls, walls,
props: PROPS, props: PROPS,
linear: LINEAR,
stelen, stelen,
building: BUILDING, building: BUILDING,
neighbourBuildings: NEIGHBOUR_BUILDINGS, neighbourBuildings: NEIGHBOUR_BUILDINGS,
@@ -487,6 +501,7 @@ console.log(`Lose Travertinplatten 40x60: ${slabs.length}`);
console.log(`Pflanzen ${plants.length}, Baeume ${trees.length}, Leuchten ${lamps.length}, Mauern ${walls.length}, Objekte ${PROPS.length}`); console.log(`Pflanzen ${plants.length}, Baeume ${trees.length}, Leuchten ${lamps.length}, Mauern ${walls.length}, Objekte ${PROPS.length}`);
const bySide = {}; const bySide = {};
for (const s of stelen) bySide[s.side] = (bySide[s.side] ?? 0) + 1; for (const s of stelen) bySide[s.side] = (bySide[s.side] ?? 0) + 1;
console.log(`Lineare Bauteile ${LINEAR.length}: ` + LINEAR.map((l) => l.kind).join(', '));
console.log(`Travertinstelen ${stelen.length}: ` + Object.entries(bySide).map(([k, v]) => `${k} ${v} Stk auf ${STELE_HEIGHTS.find((r) => r.id === k)?.height ?? '?'} m`).join(', ')); console.log(`Travertinstelen ${stelen.length}: ` + Object.entries(bySide).map(([k, v]) => `${k} ${v} Stk auf ${STELE_HEIGHTS.find((r) => r.id === k)?.height ?? '?'} m`).join(', '));
console.log(`Hoehenraster ${nx} x ${nz} (${CELL} m), Koten ${terrainSpots.length}`); console.log(`Hoehenraster ${nx} x ${nz} (${CELL} m), Koten ${terrainSpots.length}`);
const hs = Array.from(smoothed); const hs = Array.from(smoothed);