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
+11
View File
@@ -117,6 +117,17 @@ async function main() {
for (const t of site.trees) {
obstacles.push({ cx: t.x, cz: t.z, hw: 0.22, hd: 0.22, rot: 0 });
}
// Der Sichtschutz ist mannshoch und damit undurchdringlich: aufrechte
// Travertinstelen und die Eibengruppen dazwischen.
for (const s of site.stelen) {
const hw = s.rot === 0 ? s.len / 2 : s.t / 2;
const hd = s.rot === 0 ? s.t / 2 : s.len / 2;
obstacles.push({ cx: s.x, cz: s.z, hw, hd, rot: 0 });
}
for (const p of site.plants) {
if (p.type !== 'eibe' || p.x === undefined || p.z === undefined) continue;
obstacles.push({ cx: p.x, cz: p.z, hw: 0.26, hd: 0.26, rot: 0 });
}
const walker = new Walker(camera, { height, obstacles, bounds: BOUNDS });
walker.attach(canvas);
+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;
+43 -6
View File
@@ -223,7 +223,7 @@ function travertinFinish(ctx: CanvasRenderingContext2D, seed: number) {
const rnd = mulberry32(seed + 7);
// Poren und Adern, typisch fuer Travertin
for (let i = 0; i < 900; i++) {
ctx.fillStyle = `rgba(120,105,84,${0.05 + rnd() * 0.10})`;
ctx.fillStyle = `rgba(146,130,104,${0.05 + rnd() * 0.09})`;
ctx.beginPath();
ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 1 + rnd() * 5, 0.6 + rnd() * 1.6, rnd() * 0.6, 0, Math.PI * 2);
ctx.fill();
@@ -242,7 +242,7 @@ function travertinFinish(ctx: CanvasRenderingContext2D, seed: number) {
/** Travertinplatten 80 x 80 x 3 cm, Hauptsitzplatz. */
export const travertin80 = () => makeRelief('travertin80', 3.2, 0.70, 3.2,
(ctx) => {
slabs(ctx, 3.2, 0.8, 0.8, 'rgb(209,195,168)', 'rgb(158,145,124)', 0.010, 11, 7);
slabs(ctx, 3.2, 0.8, 0.8, 'rgb(224,214,192)', 'rgb(176,166,146)', 0.010, 11, 6);
travertinFinish(ctx, 21);
},
(ctx) => slabHeight(ctx, 3.2, 0.8, 0.8, 0.010, 0.015));
@@ -250,7 +250,7 @@ export const travertin80 = () => makeRelief('travertin80', 3.2, 0.70, 3.2,
/** Einzelne Travertinplatte 40 x 60 x 3 cm - Textur fuer die losen Platten. */
export const travertinSlab = () => makeRelief('travertinSlab', 0.8, 0.70, 2.2,
(ctx) => {
ctx.fillStyle = 'rgb(213,199,171)';
ctx.fillStyle = 'rgb(228,218,196)';
ctx.fillRect(0, 0, SIZE, SIZE);
travertinFinish(ctx, 26);
},
@@ -259,7 +259,7 @@ export const travertinSlab = () => makeRelief('travertinSlab', 0.8, 0.70, 2.2,
/** Travertinstellplatten als schmaler Randstreifen. */
export const travertinstell = () => makeRelief('travertinstell', 2.4, 0.73, 3.0,
(ctx) => {
slabs(ctx, 2.4, 0.4, 0.4, 'rgb(206,192,166)', 'rgb(155,144,124)', 0.012, 13, 8);
slabs(ctx, 2.4, 0.4, 0.4, 'rgb(221,210,188)', 'rgb(172,162,142)', 0.012, 13, 7);
travertinFinish(ctx, 23);
},
(ctx) => slabHeight(ctx, 2.4, 0.4, 0.4, 0.012, 0.012));
@@ -422,7 +422,7 @@ export const spielsand = () => make('spielsand', 1.5, 1.0, (ctx) => {
*/
export const travertinMauer = () => makeRelief('travertinMauer', 0.7, 0.68, 1.9,
(ctx) => {
ctx.fillStyle = 'rgb(203,183,148)';
ctx.fillStyle = 'rgb(221,208,182)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(61);
// liegende Sedimentbaender, bewusst zurueckhaltend - sonst wirkt der Stein
@@ -430,7 +430,7 @@ export const travertinMauer = () => makeRelief('travertinMauer', 0.7, 0.68, 1.9,
for (let i = 0; i < 34; i++) {
const y = rnd() * SIZE;
const hgt = 3 + rnd() * 12;
ctx.fillStyle = `rgba(${(188 + rnd() * 20) | 0},${(168 + rnd() * 18) | 0},${(134 + rnd() * 16) | 0},0.35)`;
ctx.fillStyle = `rgba(${(206 + rnd() * 18) | 0},${(192 + rnd() * 16) | 0},${(166 + rnd() * 14) | 0},0.32)`;
ctx.beginPath();
ctx.moveTo(0, y);
for (let x = 0; x <= SIZE; x += 24) ctx.lineTo(x, y + Math.sin(x / 55 + i) * 4);
@@ -654,3 +654,40 @@ function laub(key: string, base: string, seed: number, leaf: number): Belag {
export const laubEibe = () => laub('laubEibe', 'rgb(64,92,52)', 201, 3.4);
/** Liguster: mittelgruen, groesseres Blatt. */
export const laubLiguster = () => laub('laubLiguster', 'rgb(85,118,55)', 211, 5.2);
/**
* Travertin fuer die aufrechten Stelen. Der Stein ist gesaegt und zeigt die
* Schichtung als feine, liegende Baender - so wie auf dem Referenzbild des
* Planers. Die Kachel ist klein, weil eine Stele nur 50 cm breit ist.
*/
export const travertinStele = () => makeRelief('travertinStele', 0.5, 0.62, 1.7,
(ctx) => {
ctx.fillStyle = 'rgb(226,214,190)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(91);
for (let i = 0; i < 46; i++) {
const y = rnd() * SIZE;
const hgt = 2 + rnd() * 9;
ctx.fillStyle = `rgba(${(204 + rnd() * 22) | 0},${(190 + rnd() * 20) | 0},${(162 + rnd() * 18) | 0},0.42)`;
ctx.beginPath();
ctx.moveTo(0, y);
for (let x = 0; x <= SIZE; x += 32) ctx.lineTo(x, y + Math.sin(x / 70 + i) * 2.5);
for (let x = SIZE; x >= 0; x -= 32) ctx.lineTo(x, y + hgt + Math.sin(x / 62 + i * 1.4) * 2.5);
ctx.closePath();
ctx.fill();
}
travertinFinish(ctx, 92);
grain(ctx, 93, 0.04, 1);
},
(ctx) => {
ctx.fillStyle = 'rgb(196,196,196)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(94);
for (let i = 0; i < 1800; i++) {
ctx.fillStyle = `rgba(70,70,70,${0.15 + rnd() * 0.3})`;
ctx.beginPath();
ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 0.7 + rnd() * 2.0, 0.5 + rnd() * 0.9, 0, 0, Math.PI * 2);
ctx.fill();
}
noiseTile(ctx, 95, 120, 0.06);
});
+41 -15
View File
@@ -156,6 +156,19 @@ function simplifyLine(points: Vec2[], tol: number): Vec2[] {
];
}
/**
* Hoehe des Sichtschutzes je nach Lage - dieselbe Regel wie fuer die
* Travertinstelen, damit Hecke und Stele auf einer Linie abschliessen:
* gegen den Suednachbarn 1.80 m, gegen Zollet und die Zufahrt 1.50 m.
* Alles andere sind Beetpflanzungen und bleiben niedrig.
*/
function hedgeHeight(x: number, z: number): number {
if (z > 3.0) return 1.80;
if (z < -5.0) return 1.50;
if (x < -19.4) return 1.50;
return 0.95;
}
/** Trennt einen Polyzug an Knicken ueber `maxTurnDeg` in eigene Abschnitte. */
function splitAtCorners(line: Vec2[], maxTurnDeg: number): Vec2[][] {
if (line.length <= 2) return [line];
@@ -273,16 +286,26 @@ function hedgeMesh(
pos.push(...A, ...D, ...C, ...A, ...C, ...B);
uv.push(U0, v0, U0, v1, U1, v1, U0, v0, U1, v1, U1, v0);
}
// Stirnflaechen an den Enden
// Stirnflaechen an den Enden. Die UVs folgen dem Querschnitt in Metern,
// sonst faechern sich die Blaetter vom Mittelpunkt aus strahlenfoermig auf.
if (i === 0 || i === stations.length - 2) {
const s = i === 0 ? s0 : s1;
const y = i === 0 ? y0 : y1;
const c = at(s, y, [0, 0.5]);
const mid: Vec2 = [0, 0.5];
const c = at(s, y, mid);
const capUv = (p: Vec2): [number, number] => [(p[0] * s.w) / 0.55, (p[1] * s.h) / 0.55];
const cu = capUv(mid);
for (let k = 0; k < profile.length - 1; k++) {
const A = at(s, y, profile[k]);
const B = at(s, y, profile[k + 1]);
if (i === 0) pos.push(...c, ...A, ...B); else pos.push(...c, ...B, ...A);
uv.push(0.5, 0.5, 0, 0, 1, 1);
const ua = capUv(profile[k]), ub = capUv(profile[k + 1]);
if (i === 0) {
pos.push(...c, ...A, ...B);
uv.push(cu[0], cu[1], ua[0], ua[1], ub[0], ub[1]);
} else {
pos.push(...c, ...B, ...A);
uv.push(cu[0], cu[1], ub[0], ub[1], ua[0], ua[1]);
}
}
}
run += seg;
@@ -339,29 +362,32 @@ export function buildVegetation(
const r = rng(4242);
// --- Eibenhecke `Hilli` 80/100 cm ---------------------------------------
// Der Plan setzt die Eiben in dichten Reihen: das ist eine geschnittene
// Hecke und der eigentliche Sichtschutz im Garten, keine Einzelkugeln.
// --- Eibenhecke `Hilli` als Sichtschutz ----------------------------------
// Der Sichtschutz ist bewusst **nicht** durchgehend: im Plan wechseln sich
// Eibengruppen und aufrechte Travertinstelen ab, beide stehen im Dekorkies.
// Der geringe Verkettungsabstand haelt die Gruppen deshalb getrennt - dort,
// wo im Plan eine Stele steht, bleibt die Luecke offen.
const eiben = plants.filter((p) => p.type === 'eibe' && p.x !== undefined)
.map((p) => [p.x!, p.z!] as Vec2);
// Reihen begradigen und an scharfen Knicken trennen: eine gemiterte Hecke
// um eine 90-Grad-Ecke herum verdreht sich sonst.
const rows = chainRows(eiben, 2.2).flatMap((row) => splitAtCorners(simplifyLine(row, 0.22), 32));
const rows = chainRows(eiben, 0.95).flatMap((row) => splitAtCorners(simplifyLine(row, 0.22), 32));
const eibeLaub = tex.laubEibe();
const eibeMat = new MeshStandardMaterial({
map: eibeLaub.map, normalMap: eibeLaub.normalMap ?? null,
normalScale: eibeLaub.normalScale, roughness: eibeLaub.roughness, metalness: 0,
normalScale: eibeLaub.normalScale?.clone().multiplyScalar(0.6),
roughness: eibeLaub.roughness, metalness: 0,
side: DoubleSide,
});
for (const row of rows) {
const [mx, mz] = row.reduce((a, p) => [a[0] + p[0] / row.length, a[1] + p[1] / row.length], [0, 0]);
const h = hedgeHeight(mx, mz);
if (row.length >= 2) {
group.add(hedgeMesh(row, 0.95, 0.52, eibeMat, height, r, 'Eibenhecke `Hilli`'));
group.add(hedgeMesh(row, h, 0.55, eibeMat, height, r, 'Eibenhecke `Hilli`'));
} else {
// Einzelstand: als Solitaerpolster
// Einzelstand: als schmales, aufrechtes Polster
const [x, z] = row[0];
const geo = blob(0.26, 2, 0.05, 3, 1.8);
const geo = blob(0.28, 2, 0.05, 3, h / 0.56);
const m = new Mesh(geo, eibeMat);
m.position.set(x, height(x, z) + 0.47, z);
m.position.set(x, height(x, z) + h / 2, z);
m.castShadow = true;
m.receiveShadow = true;
group.add(m);
+21 -2
View File
@@ -19,6 +19,8 @@ export interface Surface {
polygon: Vec2[];
/** Ebene a*x + b*z + c, sofern es sich um einen gebauten Belag handelt. */
plane?: [number, number, number];
/** Zugehoerige Hauptebene, sofern fest vorgegeben. */
level?: string;
note?: string;
}
@@ -74,17 +76,34 @@ export interface Wall {
export interface Prop {
id: string;
type: 'gartenhaus' | 'spielhaus' | 'flammengrill' | 'bank' | 'roboter' | 'wasserhahn' | 'schacht' | 'tor';
type: 'gartenhaus' | 'spielhaus' | 'flammengrill' | 'bank' | 'roboter' | 'wasserhahn' | 'schacht' | 'tor' | 'stufe';
x: number;
z: number;
w: number;
d: number;
h: number;
rot: number;
/** Absolute Oberkante, sofern das Bauteil nicht auf dem Gelaende aufsitzt. */
top?: number;
note?: string;
}
export interface Stele { x: number; z: number; h: number }
/**
* Aufrechte Travertinstele, 50 x 6 cm im Grundriss. Zusammen mit den
* Eibengruppen bildet sie den Sichtschutz; beide stehen im Dekorkies.
*/
export interface Stele {
x: number;
z: number;
/** Laenge in Ansicht (Plan: 0.50 m). */
len: number;
/** Dicke (Plan: 0.06 bis 0.09 m). */
t: number;
/** Drehung um Y in Grad: 0 = laengs X, 90 = laengs Z. */
rot: number;
height: number;
side: 'sued' | 'nord' | 'west';
}
export interface Opening {
face: 'north' | 'south' | 'east' | 'west';
+15 -3
View File
@@ -88,7 +88,7 @@ export function setupHud(opts: HudOptions): Hud {
for (const s of site.surfaces) totals.set(s.type, (totals.get(s.type) ?? 0) + s.area);
const NAMES: Record<string, string> = {
travertin80: 'Travertinplatten 80×80×3 cm',
travertin4060: 'Travertinplatten 40×60×3 cm',
dekorkies: 'Dekorkies',
travertinstell: 'Travertinstellplatten',
keramik: 'Keramikplatten 60×60×2 cm',
verbundstein: 'Verbundsteine (Nachbar)',
@@ -100,6 +100,8 @@ export function setupHud(opts: HudOptions): Hud {
};
const heights = site.terrain.spots.filter((s) => s.zone === 'garten').map((s) => s.y);
const eibenCount = site.plants.filter((p) => p.type === 'eibe').length;
const hSued = Math.max(0, ...site.stelen.filter((s) => s.side === 'sued').map((s) => s.height)).toFixed(2);
const hNord = Math.min(9, ...site.stelen.filter((s) => s.side !== 'sued').map((s) => s.height)).toFixed(2);
$<HTMLElement>('info-body').innerHTML = `
<p>Alle Flächen, Mauern und Pflanzstandorte sind massstäblich aus dem Gartenplan
@@ -115,6 +117,13 @@ export function setupHud(opts: HudOptions): Hud {
<dt>Höhen im Garten</dt><dd>${Math.min(...heights).toFixed(2)} bis ${Math.max(...heights).toFixed(2)} m</dd>
</dl>
<h3>Die beiden Ebenen</h3>
<p>Der Garten liegt auf zwei Niveaus. Der Hauptsitzplatz aus Travertinplatten
liegt auf <strong>&minus;0.33 m</strong>. Die Travertinsitzmauer überwindet
<strong>31 cm</strong>, danach geht es eben weiter auf <strong>&minus;0.02 m</strong>
mit Rasen, Kiesfeld und Flammengrill. Vom gedeckten Sitzplatz führen
<strong>zwei schwebende Travertinstufen</strong> hinunter.</p>
<h3>Beläge</h3>
<dl>${[...totals.entries()]
.sort((a, b) => b[1] - a[1])
@@ -124,10 +133,13 @@ export function setupHud(opts: HudOptions): Hud {
<h3>Bepflanzung und Ausstattung</h3>
<ul>
<li>${site.trees.length} Solitärgehölze und Bäume, inkl. Apfelbaum</li>
<li>${eibenCount} Eiben &bdquo;Hilli&ldquo; 80/100 cm als Heckenpflanzung</li>
<li>Sichtschutz aus ${site.stelen.length} aufrechten Travertinstelen (50 × 6 cm)
und ${eibenCount} Eiben &bdquo;Hilli&ldquo; im Wechsel, alle im Dekorkiesbett &ndash;
${hSued} m gegen den Südnachbarn, ${hNord} m gegen Zollet</li>
<li>Ligusterhecke 170 cm hoch, 75 cm breit</li>
<li>${site.walls.length} Mauern und Wände, ${site.stelen.length} Travertinstelen</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.slabs.length} lose Travertinplatten 40 × 60 × 3 cm im Dekorkies</li>
<li>Gartenhaus 1,8 × 1,8 m, Spielhaus, Flammengrill mit Holzscheiterwand</li>
</ul>