Sichtschutz, schwebende Stufen und Hoehenprofil nach Plandurchgang
Deploy App / deploy (push) Successful in 28s
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:
@@ -39,7 +39,8 @@ verifiziert: die Keramikplatten messen sich auf 0.600 m genau.
|
||||
|---|---|---|
|
||||
| Feuerwehrzufahrt, Nordwestecke | −2.08 | 648.09 m |
|
||||
| Nachbargarten Zollet (Haus 9, OK FB EG) | −1.70 | 649.47 m |
|
||||
| Hauptsitzplatz Travertin | −0.30 … −0.02 | 650.87 … 651.15 m |
|
||||
| Hauptsitzplatz Travertin (untere Ebene) | −0.33 | 650.84 m |
|
||||
| Rasen und Kiesfeld (obere Ebene) | −0.02 | 651.15 m |
|
||||
| Fixpunkt Hauseingang | 0.00 | 651.17 m |
|
||||
| Ostseite beim Apfelbaum | +0.63 | 651.80 m |
|
||||
| OK Fertigboden Obergeschoss | +2.90 | 654.07 m |
|
||||
@@ -71,15 +72,50 @@ Zur Kontrolle: `node tools/debug-svg.mjs` zeichnet die extrahierte Geometrie als
|
||||
SVG-Draufsicht, `node tools/inspect-colors.mjs` listet die Planfarben mit den
|
||||
nächstgelegenen Beschriftungen.
|
||||
|
||||
### Die beiden Ebenen
|
||||
|
||||
Der Garten liegt auf zwei Niveaus, und der Sprung dazwischen ist ein Bauteil:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Untere Ebene – Hauptsitzplatz Travertin | **−0.33 m** |
|
||||
| Travertinsitzmauer, Ansichtshöhe | **0.31 m** |
|
||||
| Obere Ebene – Rasen, Kiesfeld, Flammengrill | **−0.02 m** |
|
||||
| Gedeckter Sitzplatz | −0.02 m |
|
||||
| Zwei schwebende Travertinstufen dazwischen | −0.17 / −0.31 m |
|
||||
|
||||
Gegen Osten steigt der Garten laut Plan weiter an (0.26 bis 0.63 m); dort bleibt
|
||||
die aus den Koten ausgeglichene Ebene stehen. Die beiden Hauptniveaus stehen als
|
||||
`LEVELS` in `tools/authoring.mjs` und werden nicht geschätzt – Flächen, deren
|
||||
Schwerpunkt darin liegt, bekommen exakt dieses Niveau.
|
||||
|
||||
### Der Sichtschutz
|
||||
|
||||
Er ist bewusst **nicht durchgehend**. Der Plan zeichnet ihn als Wechsel von
|
||||
aufrechten **Travertinstelen** (50 × 6 cm, in Dreiergruppen) und
|
||||
**Eibengruppen**, alle zusammen in einem **Dekorkiesbett**. Die 28 Stelen kommen
|
||||
mit Position und Ausrichtung direkt aus dem Plan; die Eiben werden nur innerhalb
|
||||
einer Gruppe zu einem Heckenkörper verkettet, damit die Lücken für die Stelen
|
||||
offen bleiben.
|
||||
|
||||
Höhen nach Abschirmrichtung:
|
||||
|
||||
- gegen den Südnachbarn, entlang der langen Fläche: **1.80 m**
|
||||
- gegen Zollet und die Feuerwehrzufahrt, untere Ebene: **1.50 m**
|
||||
|
||||
### Ebene Beläge statt gewelltem Gelände
|
||||
|
||||
Gebaute Flächen sind eben. Für jeden Belag wird aus den Höhenkoten darin und
|
||||
knapp daneben eine Ebene bestimmt – Gefälle auf 5 % begrenzt, am Schwerpunkt der
|
||||
Koten aufgehängt, und nur Koten derselben Geländezone. Das Geländeraster wird in
|
||||
diesen Feldern auf die Ebene gezwungen und dort **nicht** geglättet. Im Rendering
|
||||
bekommt jeder Belag eine senkrechte Sichtkante zum angrenzenden Gelände – erst
|
||||
dadurch wirkt eine Terrasse gebaut und nicht aufgemalt. Wachsende Flächen
|
||||
(Rasen, Schotterwiese, Beet) folgen weiterhin dem Gelände.
|
||||
Gebaute Flächen sind eben. Wo kein festes Niveau vorgegeben ist, wird aus den
|
||||
Höhenkoten eine Ausgleichsebene bestimmt – Gefälle auf 4 % begrenzt, am
|
||||
Schwerpunkt der Koten aufgehängt. Jede Kote gehört dabei zu genau **einer**
|
||||
Fläche, nämlich der kleinsten, die sie enthält; sonst zieht etwa die Kote einer
|
||||
Stufe die darunterliegende Terrasse mit hoch und aus zwei sauberen Ebenen wird
|
||||
eine schiefe.
|
||||
|
||||
Das Geländeraster wird in diesen Feldern auf die Ebene gezwungen und dort
|
||||
**nicht** geglättet. Im Rendering bekommt jeder Belag eine senkrechte Sichtkante
|
||||
zum angrenzenden Gelände – erst dadurch wirkt eine Terrasse gebaut und nicht
|
||||
aufgemalt. Wachsende Flächen (Rasen, Schotterwiese, Beet) folgen dem Gelände.
|
||||
|
||||
### Bauteile statt hochgezogener Flächen
|
||||
|
||||
@@ -95,15 +131,20 @@ aus (`src/scene/walls.ts`):
|
||||
35 cm tief, 90 cm Achsabstand) vor dem gedeckten Sitzplatz.
|
||||
- **Holzscheiterwand** – Stahlrahmen, gefüllt mit gestapelten Scheitern.
|
||||
|
||||
Die **Eibenhecke** entsteht nicht aus Einzelkugeln: die Pflanzstandorte des Plans
|
||||
werden zu Reihen verkettet, begradigt, an scharfen Knicken getrennt und als
|
||||
geschnittener Heckenkörper mit gerundetem Querschnitt aufgezogen. Ebenso die
|
||||
Ligusterhecke (170 cm hoch, 75 cm breit). Das ist der eigentliche Sichtschutz
|
||||
im Garten.
|
||||
Die **Eibengruppen** entstehen nicht aus Einzelkugeln: die Pflanzstandorte des
|
||||
Plans werden gruppenweise verkettet, begradigt, an scharfen Knicken getrennt und
|
||||
als geschnittener Heckenkörper mit gerundetem Querschnitt aufgezogen – siehe
|
||||
Abschnitt „Der Sichtschutz". Ebenso die Ligusterhecke (170 cm, 75 cm breit).
|
||||
|
||||
Die **Travertinplatten 40 × 60 × 3 cm** liegen laut Plan einzeln im Dekorkies –
|
||||
30 Platten, exakt an den Planpositionen, als erhabene Platten auf dem Kiesbett.
|
||||
|
||||
Materialien und Ausbildung folgen den **Referenzbildern im Plan-PDF**: heller,
|
||||
geschichteter Travertin für Mauern, Stelen und Platten, die schwebenden Stufen
|
||||
als auskragende Platten mit Schattenfuge, der Flammengrill als anthrazitfarbener
|
||||
Stahlkubus mit Holzfach und runder Feueröffnung. Die Bilder lassen sich mit
|
||||
`node tools/plan-bilder.mjs` aus dem PDF ziehen.
|
||||
|
||||
### Warum Zonen statt eines einzigen Geländes
|
||||
|
||||
Das Grundstück fällt über rund 3.5 m ab, die Sprünge liegen aber nicht im
|
||||
|
||||
File diff suppressed because one or more lines are too long
+11
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>−0.33 m</strong>. Die Travertinsitzmauer überwindet
|
||||
<strong>31 cm</strong>, danach geht es eben weiter auf <strong>−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 „Hilli“ 80/100 cm als Heckenpflanzung</li>
|
||||
<li>Sichtschutz aus ${site.stelen.length} aufrechten Travertinstelen (50 × 6 cm)
|
||||
und ${eibenCount} Eiben „Hilli“ im Wechsel, alle im Dekorkiesbett –
|
||||
${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 – 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>
|
||||
|
||||
|
||||
+61
-15
@@ -71,6 +71,37 @@ export const TREES = [
|
||||
{ name: 'Solitärgehölz Zollet', x: -17.6, z: -10.2, crown: 1.34, height: 4.6, species: 'parrotia', neighbour: true },
|
||||
];
|
||||
|
||||
/**
|
||||
* Die beiden Hauptebenen des Gartens.
|
||||
*
|
||||
* Der Garten liegt auf zwei Niveaus: der Hauptsitzplatz aus Travertinplatten
|
||||
* liegt auf -0.33, dahinter steht die Travertinsitzmauer mit 31 cm Ansichtshoehe,
|
||||
* und direkt hinter ihr geht es eben weiter auf -0.02 mit Rasen, Kiesfeld und
|
||||
* Flammengrill. Flaechen, deren Schwerpunkt in einem dieser Bereiche liegt,
|
||||
* werden exakt auf das Niveau gelegt statt aus streuenden Koten geschaetzt.
|
||||
*
|
||||
* Gegen Osten steigt der Garten laut Plan weiter an (0.26 bis 0.63) - dort
|
||||
* bleibt die Ausgleichsebene aus den Koten stehen.
|
||||
*/
|
||||
export const LEVELS = [
|
||||
{
|
||||
id: 'unten',
|
||||
level: -0.33,
|
||||
note: 'Untere Ebene: Hauptsitzplatz Travertin mit Kiesstreifen und Eiben',
|
||||
polygon: [
|
||||
[-21.90, -6.65], [-13.75, -6.65], [-13.75, 0.90],
|
||||
[-18.42, 0.90], [-18.42, 4.15], [-21.90, 4.15],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'oben',
|
||||
level: -0.02,
|
||||
note: 'Obere Ebene beim Uebergang: gedeckter Sitzplatz und Rasen an der Mauer',
|
||||
polygon: [[-14.00, -3.20], [-10.20, -3.20], [-10.20, 1.00], [-14.00, 1.00]],
|
||||
types: ['keramik'],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Mauern, Stuetzelemente und Waende.
|
||||
* `path` = Mittellinie, `t` = Dicke, `top`/`base` = Hoehe der Krone bzw. des Fusses
|
||||
@@ -79,13 +110,13 @@ export const TREES = [
|
||||
export const WALLS = [
|
||||
{
|
||||
id: 'travertinmauer-nord', type: 'travertin',
|
||||
note: 'Travertinmauer an der Grenze zum Nachbargrundstueck Zollet, stuetzt rund 1.5 m ab',
|
||||
path: [[-18.17, -6.895], [-13.82, -6.895]], t: 0.55, top: -0.30, base: -1.90,
|
||||
note: 'Travertinmauer an der Grenze zum Nachbargrundstueck Zollet; Krone auf dem Niveau der unteren Ebene, stuetzt rund 1.5 m ab',
|
||||
path: [[-18.17, -6.895], [-13.82, -6.895]], t: 0.55, top: -0.33, base: -1.95,
|
||||
},
|
||||
{
|
||||
id: 'sitzmauer-nord', type: 'travertin',
|
||||
note: 'Sitzmauer 25 cm ueber der Travertinmauer (gelbe Kote "25" im Plan)',
|
||||
path: [[-20.08, -6.47], [-13.82, -6.47]], t: 0.30, top: -0.03, base: -0.45,
|
||||
note: 'Sitzmauer 25 cm ueber der unteren Ebene (gelbe Kote "25" im Plan)',
|
||||
path: [[-20.08, -6.47], [-13.82, -6.47]], t: 0.30, top: -0.08, base: -0.55,
|
||||
},
|
||||
{
|
||||
id: 'winkelelemente-west', type: 'beton',
|
||||
@@ -94,14 +125,17 @@ export const WALLS = [
|
||||
t: 0.22, top: null, base: null, retains: 'west',
|
||||
},
|
||||
{
|
||||
// Diese Mauer ist der Hoehensprung zwischen den beiden Ebenen: Krone
|
||||
// buendig mit der oberen Ebene (-0.02), Fuss auf der unteren (-0.33).
|
||||
// Ansichtshoehe damit genau 31 cm.
|
||||
id: 'travertinsitzmauer-west', type: 'travertin',
|
||||
note: 'Travertinsitzmauer als Abschluss des Hauptsitzplatzes gegen den Rasen',
|
||||
path: [[-18.64, 0.755], [-14.73, 0.755]], t: 0.41, top: 0.40, base: -0.45, seating: true,
|
||||
note: 'Travertinsitzmauer: 31 cm Hoehensprung vom Hauptsitzplatz zum Rasen',
|
||||
path: [[-18.64, 0.755], [-14.73, 0.755]], t: 0.41, top: -0.02, base: -0.55, seating: true,
|
||||
},
|
||||
{
|
||||
id: 'travertinsitzmauer-sued', type: 'travertin',
|
||||
note: 'Schenkel der Travertinsitzmauer laengs des Rasens',
|
||||
path: [[-18.44, 0.55], [-18.44, 3.94]], t: 0.40, top: 0.40, base: -0.40, seating: true,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 'betonmauer-nachbar', type: 'beton',
|
||||
@@ -121,7 +155,7 @@ export const WALLS = [
|
||||
{
|
||||
id: 'stufe-gartentor', type: 'travertin',
|
||||
note: 'Blockstufen beim Gartentor an der Suedwestecke',
|
||||
path: [[-21.05, 2.15], [-21.05, 3.75]], t: 0.80, top: -0.13, base: -0.75,
|
||||
path: [[-21.05, 2.15], [-21.05, 3.75]], t: 0.80, top: -0.18, base: -0.80,
|
||||
},
|
||||
{
|
||||
id: 'lamellenwand', type: 'lamellen',
|
||||
@@ -146,6 +180,13 @@ export const PROPS = [
|
||||
{ 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' },
|
||||
{ id: 'gartentor', type: 'tor', x: -21.60, z: 3.30, w: 1.00, d: 0.06, h: 1.10, rot: 0 },
|
||||
// Zwei schwebende Travertinplatten fuehren vom gedeckten Sitzplatz (-0.02)
|
||||
// auf den Hauptsitzplatz (-0.33). Lage und Format stehen im Plan, die
|
||||
// Ausbildung als auskragende Platte zeigt das Referenzbild des Planers.
|
||||
{ id: 'stufe-oben', type: 'stufe', x: -14.335, z: -1.065, w: 0.81, d: 3.25, h: 0.08, rot: 0,
|
||||
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,
|
||||
top: -0.31, note: 'Schwebende Travertinstufe, Tritthoehe -0.31' },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -174,13 +215,18 @@ export const EXTRA_SURFACES = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Travertinstelen: aufrechte Steinstelen als Raumteiler entlang der Beete.
|
||||
* Positionen aus den Planbeschriftungen "Travertinstelen".
|
||||
* Hoehe des Sichtschutzes. Er besteht aus aufrechten Travertinstelen
|
||||
* (50 x 6 cm im Grundriss) und Eibengruppen, die sich abwechseln und
|
||||
* gemeinsam in einem Dekorkiesbett stehen. Die Stelenpositionen kommen
|
||||
* direkt aus dem Plan, die Hoehe haengt davon ab, wohin abgeschirmt wird:
|
||||
*
|
||||
* - gegen den Suednachbarn, entlang der langen Flaeche: 1.80 m
|
||||
* - gegen Zollet und die Feuerwehrzufahrt, untere Ebene: 1.50 m
|
||||
*/
|
||||
export const STELEN = [
|
||||
{ x: -19.55, z: -3.10, h: 0.90 }, { x: -19.30, z: -2.30, h: 1.15 }, { x: -19.05, z: -1.55, h: 0.75 },
|
||||
{ x: -13.30, z: 4.45, h: 1.05 }, { x: -12.60, z: 4.45, h: 0.80 }, { x: -10.30, z: 4.60, h: 1.10 },
|
||||
{ x: -9.60, z: 4.60, h: 0.85 }, { x: -1.30, z: 4.55, h: 1.00 }, { x: -0.60, z: 4.55, h: 0.75 },
|
||||
export const STELE_HEIGHTS = [
|
||||
{ id: 'sued', height: 1.80, test: (x, z) => z > 3.0 },
|
||||
{ id: 'nord', height: 1.50, test: (x, z) => z < -5.0 },
|
||||
{ id: 'west', height: 1.50, test: (x) => x < -19.4 },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+105
-35
@@ -8,7 +8,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
SURFACE_BY_COLOR, PLANT_BY_COLOR, TREES, WALLS, PROPS, STELEN,
|
||||
SURFACE_BY_COLOR, PLANT_BY_COLOR, TREES, WALLS, PROPS, LEVELS, STELE_HEIGHTS,
|
||||
BUILDING, NEIGHBOUR_BUILDINGS, TERRAIN_ZONES, EXTRA_SPOTS, EXTRA_SURFACES,
|
||||
START_VIEWS, SITE_LOCATION,
|
||||
} from './authoring.mjs';
|
||||
@@ -100,24 +100,45 @@ const bboxOf = (pts) => {
|
||||
*/
|
||||
const slabs = [];
|
||||
const surfaces = [];
|
||||
/**
|
||||
* Travertinstelen: aufrecht gestellte Platten von 50 x 6 cm, im Plan in
|
||||
* Dreiergruppen. Zusammen mit den Eibengruppen bilden sie den Sichtschutz.
|
||||
*/
|
||||
const stelen = [];
|
||||
|
||||
for (const s of raw.shapes) {
|
||||
if (s.op !== 'f' || s.circle) continue;
|
||||
let type = SURFACE_BY_COLOR[s.hex];
|
||||
if (!type) continue;
|
||||
const b = bboxOf(s.pts);
|
||||
const cx = (b.x0 + b.x1) / 2, cz = (b.z0 + b.z1) / 2;
|
||||
|
||||
// Lose 40x60-Platten (Plan: 0.40 breit, 0.49 bis 0.80 lang)
|
||||
if (s.hex === '#ffffcc' && s.area < 0.6 && Math.abs(b.w - 0.4) < 0.06 && b.d > 0.4 && b.d < 0.9) {
|
||||
slabs.push({
|
||||
x: round((b.x0 + b.x1) / 2), z: round((b.z0 + b.z1) / 2),
|
||||
w: round(b.w, 2), d: round(b.d, 2),
|
||||
});
|
||||
slabs.push({ x: round(cx), z: round(cz), w: round(b.w, 2), d: round(b.d, 2) });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Aufrechte Stelen: 50 cm lang, 6 bis 9 cm dick, in beiden Achsrichtungen
|
||||
if (s.hex === '#ddd299' && s.area < 0.06) {
|
||||
const along = b.w > b.d ? 'x' : 'z';
|
||||
const len = Math.max(b.w, b.d);
|
||||
if (len > 0.35 && len < 0.65) {
|
||||
stelen.push({
|
||||
x: round(cx), z: round(cz), len: round(len, 2),
|
||||
t: round(Math.min(b.w, b.d), 2),
|
||||
rot: along === 'x' ? 0 : 90,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Der Plan verwendet denselben hellen Ton fuer Dekorkies und fuer die
|
||||
// Keramikterrasse des Nachbarn; unterschieden wird ueber die Lage.
|
||||
if (type === 'dekorkies' && (b.z0 + b.z1) / 2 < -6.6) type = 'keramik';
|
||||
if (type === 'dekorkies' && cz < -6.6) type = 'keramik';
|
||||
// Die schmalen Baender des Sichtschutzstreifens sind das Kiesbett, in dem
|
||||
// Stelen und Eiben stehen - kein Plattenbelag.
|
||||
if (type === 'travertinstell' && cz > 3.2 && Math.min(b.w, b.d) < 0.7) type = 'dekorkies';
|
||||
|
||||
if (s.area < 0.3) continue;
|
||||
if (NOT_A_SURFACE.has(type)) continue;
|
||||
@@ -130,6 +151,15 @@ for (const s of EXTRA_SURFACES) {
|
||||
}
|
||||
surfaces.sort((a, b) => b.area - a.area);
|
||||
|
||||
// Sichtschutzhoehe je nach Lage: gegen den Suednachbarn hoeher als gegen
|
||||
// Zollet auf der unteren Ebene.
|
||||
for (const st of stelen) {
|
||||
const rule = STELE_HEIGHTS.find((r) => r.test(st.x, st.z));
|
||||
st.height = rule ? rule.height : 1.5;
|
||||
st.side = rule ? rule.id : 'west';
|
||||
}
|
||||
stelen.sort((a, b) => a.z - b.z || a.x - b.x);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Hecken und Pflanzen aus den Plankreisen
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -212,43 +242,80 @@ function distToPolygon(x, z, poly) {
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jede Kote gehoert zu genau einer Flaeche: der kleinsten, die sie enthaelt.
|
||||
* Sonst zieht etwa die Kote einer Stufe die Ebene der darunterliegenden
|
||||
* Terrasse mit hoch, und aus zwei sauberen Ebenen wird eine schiefe.
|
||||
*/
|
||||
const owner = new Map();
|
||||
for (const sp of terrainSpots) {
|
||||
let best = null;
|
||||
for (const s of surfaces) {
|
||||
if (!FLAT_TYPES.has(s.type)) continue;
|
||||
if (!pointInPolygon(sp.x, sp.z, s.polygon)) continue;
|
||||
if (!best || s.area < best.area) best = s;
|
||||
}
|
||||
if (best) owner.set(sp, best);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Garten hat zwei Hauptebenen. Wo sie feststehen, werden sie fest
|
||||
* vorgegeben statt aus streuenden Koten geschaetzt - so bleiben die Flaechen
|
||||
* eben und der Hoehensprung an der Sitzmauer stimmt auf den Zentimeter.
|
||||
*/
|
||||
function levelFor(s) {
|
||||
const [cx, cz] = centroid(s.polygon);
|
||||
for (const lv of LEVELS) {
|
||||
if (lv.types && !lv.types.includes(s.type)) continue;
|
||||
if (pointInPolygon(cx, cz, lv.polygon)) return lv;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const flatSurfaces = [];
|
||||
for (const s of surfaces) {
|
||||
if (!FLAT_TYPES.has(s.type) || s.area < 0.6) continue;
|
||||
const inside = terrainSpots.filter((sp) => pointInPolygon(sp.x, sp.z, s.polygon));
|
||||
// Nur Koten derselben Gelaendezone heranziehen - ueber eine Stuetzmauer
|
||||
// hinweg gehoert die Nachbarkote nicht zu dieser Flaeche.
|
||||
const [pcx, pcz] = centroid(s.polygon);
|
||||
const zone = zoneOf(pcx, pcz);
|
||||
const near = terrainSpots.filter(
|
||||
(sp) => !inside.includes(sp) && sp.zone === zone && distToPolygon(sp.x, sp.z, s.polygon) < 1.1,
|
||||
);
|
||||
const pool = [...inside, ...near];
|
||||
if (!pool.length) continue;
|
||||
if (!FLAT_TYPES.has(s.type) || s.area < 0.4) continue;
|
||||
|
||||
const ys = pool.map((p) => p.y);
|
||||
const mean = ys.reduce((a, b) => a + b, 0) / ys.length;
|
||||
const px = pool.reduce((a, p) => a + p.x, 0) / pool.length;
|
||||
const pz = pool.reduce((a, p) => a + p.z, 0) / pool.length;
|
||||
const fixed = levelFor(s);
|
||||
let plane, ys;
|
||||
|
||||
let plane = pool.length >= 3 ? fitPlane(pool) : null;
|
||||
// Ein Belag hat hoechstens rund 5 % Gefaelle - genug fuer die Entwaesserung. Bei fast kollinearen Koten
|
||||
// kippt die Ausgleichsebene weg; dann nur das Gefaelle begrenzen und die
|
||||
// Ebene wieder am Schwerpunkt der Koten aufhaengen - nie am Polygonrand,
|
||||
// sonst wandert sie mit dem gekappten Gefaelle davon.
|
||||
if (plane) {
|
||||
const slope = Math.hypot(plane.a, plane.b);
|
||||
const k = slope > 0.05 ? 0.05 / slope : 1;
|
||||
const a = plane.a * k, b = plane.b * k;
|
||||
plane = { a, b, c: mean - (a * px + b * pz) };
|
||||
if (fixed) {
|
||||
plane = { a: 0, b: 0, c: fixed.level };
|
||||
ys = [fixed.level];
|
||||
s.level = fixed.id;
|
||||
} else {
|
||||
plane = { a: 0, b: 0, c: mean };
|
||||
const inside = terrainSpots.filter((sp) => owner.get(sp) === s);
|
||||
const [pcx, pcz] = centroid(s.polygon);
|
||||
const zone = zoneOf(pcx, pcz);
|
||||
const near = inside.length
|
||||
? []
|
||||
: terrainSpots.filter((sp) => !owner.has(sp) && sp.zone === zone && distToPolygon(sp.x, sp.z, s.polygon) < 1.2);
|
||||
const pool = [...inside, ...near];
|
||||
if (!pool.length) continue;
|
||||
|
||||
ys = pool.map((p) => p.y);
|
||||
const mean = ys.reduce((a, b) => a + b, 0) / ys.length;
|
||||
const px = pool.reduce((a, p) => a + p.x, 0) / pool.length;
|
||||
const pz = pool.reduce((a, p) => a + p.z, 0) / pool.length;
|
||||
|
||||
let fit = pool.length >= 3 ? fitPlane(pool) : null;
|
||||
// Ein Belag hat hoechstens rund 4 % Gefaelle. Bei fast kollinearen Koten
|
||||
// kippt die Ausgleichsebene weg; dann nur das Gefaelle begrenzen und die
|
||||
// Ebene am Schwerpunkt der Koten aufhaengen, nie am Polygonrand.
|
||||
if (fit) {
|
||||
const slope = Math.hypot(fit.a, fit.b);
|
||||
const k = slope > 0.04 ? 0.04 / slope : 1;
|
||||
const a = fit.a * k, b = fit.b * k;
|
||||
plane = { a, b, c: mean - (a * px + b * pz) };
|
||||
} else {
|
||||
plane = { a: 0, b: 0, c: mean };
|
||||
}
|
||||
}
|
||||
|
||||
s.plane = [round(plane.a, 5), round(plane.b, 5), round(plane.c, 4)];
|
||||
flatSurfaces.push({
|
||||
polygon: s.polygon, plane, type: s.type,
|
||||
min: Math.min(...ys) - 0.12, max: Math.max(...ys) + 0.12,
|
||||
min: Math.min(...ys) - 0.10, max: Math.max(...ys) + 0.10,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -399,7 +466,7 @@ const site = {
|
||||
lamps,
|
||||
walls,
|
||||
props: PROPS,
|
||||
stelen: STELEN,
|
||||
stelen,
|
||||
building: BUILDING,
|
||||
neighbourBuildings: NEIGHBOUR_BUILDINGS,
|
||||
startViews: START_VIEWS,
|
||||
@@ -417,7 +484,10 @@ for (const [t, a] of Object.entries(byType).sort((x, y) => y[1] - x[1])) {
|
||||
console.log(' ' + t.padEnd(16) + a.toFixed(1).padStart(7) + ' m2');
|
||||
}
|
||||
console.log(`Lose Travertinplatten 40x60: ${slabs.length}`);
|
||||
console.log(`Pflanzen ${plants.length}, Baeume ${trees.length}, Leuchten ${lamps.length}, Mauern ${walls.length}, Objekte ${PROPS.length}, Stelen ${STELEN.length}`);
|
||||
console.log(`Pflanzen ${plants.length}, Baeume ${trees.length}, Leuchten ${lamps.length}, Mauern ${walls.length}, Objekte ${PROPS.length}`);
|
||||
const bySide = {};
|
||||
for (const s of stelen) bySide[s.side] = (bySide[s.side] ?? 0) + 1;
|
||||
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}`);
|
||||
const hs = Array.from(smoothed);
|
||||
console.log(`Hoehen ${Math.min(...hs).toFixed(2)} .. ${Math.max(...hs).toFixed(2)} m (absolut ${(651.17 + Math.min(...hs)).toFixed(2)} .. ${(651.17 + Math.max(...hs)).toFixed(2)} m ue. M.)`);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Zieht die Referenzfotos aus dem Gartenplan-PDF (Mauerwerk, schwebende Stufen,
|
||||
* Travertin, Flammengrill, Eibenhecke). Sie sind die Vorlage fuer die
|
||||
* Materialisierung in der Szene.
|
||||
*
|
||||
* Aufruf: node tools/plan-bilder.mjs [Zielordner]
|
||||
*/
|
||||
import * as mupdf from 'mupdf'; import fs from 'fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PDF = path.join(__dirname, '..', 'Info Dateien', 'Gartenplan 1 Jasmin und Damian Kellenberger.pdf');
|
||||
const OUT = process.argv[2] ?? path.join(__dirname, 'out', 'planbilder');
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
const doc = mupdf.Document.openDocument(fs.readFileSync(PDF),'application/pdf');
|
||||
const page = doc.loadPage(0);
|
||||
let n=0;
|
||||
const noop=()=>{};
|
||||
const dev = new mupdf.Device({
|
||||
fillImage: (img, ctm, alpha) => {
|
||||
n++;
|
||||
const w = img.getWidth(), h = img.getHeight();
|
||||
// Platzierung auf dem Blatt aus der Matrix
|
||||
const x = ctm[4], y = ctm[5], sx = Math.hypot(ctm[0],ctm[1]), sy = Math.hypot(ctm[2],ctm[3]);
|
||||
console.log(`Bild ${n}: ${w}x${h}px auf Blatt bei (${x.toFixed(0)}, ${(y-sy).toFixed(0)}) Groesse ${sx.toFixed(0)}x${sy.toFixed(0)}pt`);
|
||||
try {
|
||||
const pix = img.toPixmap();
|
||||
fs.writeFileSync(path.join(OUT, `planbild-${n}.png`), pix.asPNG());
|
||||
} catch (e) { console.log(' (nicht extrahierbar)', e.message); }
|
||||
},
|
||||
fillPath: noop, strokePath: noop, fillShade: noop, fillImageMask: noop,
|
||||
clipPath: noop, clipStrokePath: noop, clipImageMask: noop, popClip: noop,
|
||||
fillText: noop, strokeText: noop, clipText: noop, clipStrokeText: noop, ignoreText: noop,
|
||||
beginMask: noop, endMask: noop, beginGroup: noop, endGroup: noop,
|
||||
beginTile: () => 0, endTile: noop, beginLayer: noop, endLayer: noop, close: noop,
|
||||
});
|
||||
page.run(dev, mupdf.Matrix.identity);
|
||||
dev.close();
|
||||
Reference in New Issue
Block a user