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 |
|
| Feuerwehrzufahrt, Nordwestecke | −2.08 | 648.09 m |
|
||||||
| Nachbargarten Zollet (Haus 9, OK FB EG) | −1.70 | 649.47 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 |
|
| Fixpunkt Hauseingang | 0.00 | 651.17 m |
|
||||||
| Ostseite beim Apfelbaum | +0.63 | 651.80 m |
|
| Ostseite beim Apfelbaum | +0.63 | 651.80 m |
|
||||||
| OK Fertigboden Obergeschoss | +2.90 | 654.07 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
|
SVG-Draufsicht, `node tools/inspect-colors.mjs` listet die Planfarben mit den
|
||||||
nächstgelegenen Beschriftungen.
|
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
|
### Ebene Beläge statt gewelltem Gelände
|
||||||
|
|
||||||
Gebaute Flächen sind eben. Für jeden Belag wird aus den Höhenkoten darin und
|
Gebaute Flächen sind eben. Wo kein festes Niveau vorgegeben ist, wird aus den
|
||||||
knapp daneben eine Ebene bestimmt – Gefälle auf 5 % begrenzt, am Schwerpunkt der
|
Höhenkoten eine Ausgleichsebene bestimmt – Gefälle auf 4 % begrenzt, am
|
||||||
Koten aufgehängt, und nur Koten derselben Geländezone. Das Geländeraster wird in
|
Schwerpunkt der Koten aufgehängt. Jede Kote gehört dabei zu genau **einer**
|
||||||
diesen Feldern auf die Ebene gezwungen und dort **nicht** geglättet. Im Rendering
|
Fläche, nämlich der kleinsten, die sie enthält; sonst zieht etwa die Kote einer
|
||||||
bekommt jeder Belag eine senkrechte Sichtkante zum angrenzenden Gelände – erst
|
Stufe die darunterliegende Terrasse mit hoch und aus zwei sauberen Ebenen wird
|
||||||
dadurch wirkt eine Terrasse gebaut und nicht aufgemalt. Wachsende Flächen
|
eine schiefe.
|
||||||
(Rasen, Schotterwiese, Beet) folgen weiterhin dem Gelände.
|
|
||||||
|
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
|
### Bauteile statt hochgezogener Flächen
|
||||||
|
|
||||||
@@ -95,15 +131,20 @@ aus (`src/scene/walls.ts`):
|
|||||||
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.
|
||||||
|
|
||||||
Die **Eibenhecke** entsteht nicht aus Einzelkugeln: die Pflanzstandorte des Plans
|
Die **Eibengruppen** entstehen nicht aus Einzelkugeln: die Pflanzstandorte des
|
||||||
werden zu Reihen verkettet, begradigt, an scharfen Knicken getrennt und als
|
Plans werden gruppenweise verkettet, begradigt, an scharfen Knicken getrennt und
|
||||||
geschnittener Heckenkörper mit gerundetem Querschnitt aufgezogen. Ebenso die
|
als geschnittener Heckenkörper mit gerundetem Querschnitt aufgezogen – siehe
|
||||||
Ligusterhecke (170 cm hoch, 75 cm breit). Das ist der eigentliche Sichtschutz
|
Abschnitt „Der Sichtschutz". Ebenso die Ligusterhecke (170 cm, 75 cm breit).
|
||||||
im Garten.
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
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
|
### 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
|
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) {
|
for (const t of site.trees) {
|
||||||
obstacles.push({ cx: t.x, cz: t.z, hw: 0.22, hd: 0.22, rot: 0 });
|
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 });
|
const walker = new Walker(camera, { height, obstacles, bounds: BOUNDS });
|
||||||
walker.attach(canvas);
|
walker.attach(canvas);
|
||||||
|
|||||||
+84
-37
@@ -4,11 +4,11 @@
|
|||||||
* Die Mauern stehen in walls.ts.
|
* Die Mauern stehen in walls.ts.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
BoxGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial, PointLight,
|
BoxGeometry, BufferGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial,
|
||||||
type MeshStandardMaterialParameters,
|
PointLight, type MeshStandardMaterialParameters,
|
||||||
} from 'three';
|
} from 'three';
|
||||||
import type { Lamp, Prop, Stele } from '../site';
|
import type { Lamp, Prop, Stele } from '../site';
|
||||||
import { boxGeometry } from './geometry';
|
import { boxGeometry, mergeGeometries } from './geometry';
|
||||||
import * as tex from './textures';
|
import * as tex from './textures';
|
||||||
|
|
||||||
function material(belag: tex.Belag, extra: MeshStandardMaterialParameters = {}) {
|
function material(belag: tex.Belag, extra: MeshStandardMaterialParameters = {}) {
|
||||||
@@ -21,15 +21,28 @@ function material(belag: tex.Belag, extra: MeshStandardMaterialParameters = {})
|
|||||||
...extra,
|
...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 {
|
export function buildStelen(stelen: Stele[], height: (x: number, z: number) => number): Group {
|
||||||
const group = new Group();
|
const group = new Group();
|
||||||
group.name = 'Travertinstelen';
|
group.name = 'Travertinstelen';
|
||||||
const stein = tex.travertinMauer();
|
const stein = tex.travertinStele();
|
||||||
const mat = material(stein, { roughness: 0.66 });
|
const mat = material(stein, { roughness: 0.62 });
|
||||||
|
const parts: BufferGeometry[] = [];
|
||||||
for (const s of stelen) {
|
for (const s of stelen) {
|
||||||
const y0 = height(s.x, s.z) - 0.3;
|
const y0 = height(s.x, s.z) - 0.25;
|
||||||
const geo = boxGeometry(s.x, s.z, 0.22, 0.22, y0, y0 + 0.3 + s.h, 0, stein.tile);
|
// 2.5 cm Stossfuge: die Dreiergruppen des Plans sollen als einzelne
|
||||||
const m = new Mesh(geo, mat);
|
// 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.castShadow = true;
|
||||||
m.receiveShadow = true;
|
m.receiveShadow = true;
|
||||||
group.add(m);
|
group.add(m);
|
||||||
@@ -37,6 +50,32 @@ export function buildStelen(stelen: Stele[], height: (x: number, z: number) => n
|
|||||||
return group;
|
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
|
// Freistehende Objekte
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -96,45 +135,52 @@ function spielhaus(p: Prop, y: number): Group {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flammengrill: Cortenstahl-Kubus mit umlaufender Grillplatte und offener
|
* Flammengrill nach dem Referenzbild des Planers: anthrazitfarbener Stahlkubus,
|
||||||
* Feuerkammer, wie auf dem Referenzfoto des Plans.
|
* unten ein offenes Holzfach, oben ein perforiertes Band, in der Deckplatte die
|
||||||
|
* runde Feueroeffnung.
|
||||||
*/
|
*/
|
||||||
function flammengrill(p: Prop, y: number): Group {
|
function flammengrill(p: Prop, y: number): Group {
|
||||||
const g = new Group();
|
const g = new Group();
|
||||||
const corten = new MeshStandardMaterial({ color: 0x5c3a26, roughness: 0.62, metalness: 0.45 });
|
const stahl = new MeshStandardMaterial({ color: 0x4a4c50, roughness: 0.55, metalness: 0.45 });
|
||||||
const dark = new MeshStandardMaterial({ color: 0x241d18, roughness: 0.75, metalness: 0.3 });
|
const dunkel = new MeshStandardMaterial({ color: 0x25272a, roughness: 0.85, metalness: 0.2 });
|
||||||
const plateMat = new MeshStandardMaterial({ color: 0x3b3a37, roughness: 0.34, metalness: 0.72 });
|
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 hw = p.w / 2, hd = p.d / 2;
|
||||||
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);
|
const holzTop = y + p.h * 0.42; // Oberkante Holzfach
|
||||||
g.add(base);
|
const bandTop = y + p.h - 0.04; // Unterkante Deckplatte
|
||||||
|
const bandBot = bandTop - 0.16; // perforiertes Kopfband
|
||||||
|
|
||||||
const bodyTop = y + p.h - 0.05;
|
// Holzfach: sichtbare Scheiter zwischen zwei Stahlwangen
|
||||||
const body = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + 0.08, bodyTop, p.rot, 1), corten);
|
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;
|
body.castShadow = true; body.receiveShadow = true;
|
||||||
g.add(body);
|
g.add(body);
|
||||||
|
|
||||||
// Feuerkammer: dunkle Nische in der Vorderseite
|
// Perforiertes Band: einzelne Loecher waeren zu teuer, deshalb ein dunkleres,
|
||||||
const a = (p.rot * Math.PI) / 180;
|
// leicht zurueckversetztes Band mit rauer Oberflaeche
|
||||||
const fx = p.x - Math.sin(a) * (p.d / 2 - 0.02);
|
const band = new Mesh(boxGeometry(p.x, p.z, p.w - 0.01, p.d - 0.01, bandBot, bandTop, p.rot, 1), dunkel);
|
||||||
const fz = p.z + Math.cos(a) * (p.d / 2 - 0.02);
|
band.castShadow = true;
|
||||||
const fire = new Mesh(
|
g.add(band);
|
||||||
boxGeometry(fx, fz, p.w * 0.62, 0.06, y + 0.24, y + 0.52, p.rot, 1), dark,
|
|
||||||
);
|
|
||||||
g.add(fire);
|
|
||||||
|
|
||||||
// Grillplatte mit Ausschnitt fuer das Feuer
|
// Deckplatte mit runder Feueroeffnung
|
||||||
const plate = new Mesh(
|
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);
|
||||||
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;
|
||||||
);
|
|
||||||
plate.castShadow = true;
|
|
||||||
plate.receiveShadow = true;
|
|
||||||
g.add(plate);
|
g.add(plate);
|
||||||
const ring = new Mesh(
|
const loch = new Mesh(new CylinderGeometry(p.w * 0.19, p.w * 0.19, 0.06, 24), dunkel);
|
||||||
new CylinderGeometry(p.w * 0.24, p.w * 0.24, 0.05, 20), dark,
|
loch.position.set(p.x, bandTop + 0.035, p.z);
|
||||||
);
|
g.add(loch);
|
||||||
ring.position.set(p.x, bodyTop + 0.04, p.z);
|
|
||||||
g.add(ring);
|
|
||||||
return g;
|
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 'flammengrill': obj = flammengrill(p, y); break;
|
||||||
case 'bank': obj = bank(p, y); break;
|
case 'bank': obj = bank(p, y); break;
|
||||||
case 'tor': obj = gartentor(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 'roboter': obj = roboterGarage(p, y); break;
|
||||||
case 'wasserhahn': obj = wasserhahn(p, y); break;
|
case 'wasserhahn': obj = wasserhahn(p, y); break;
|
||||||
case 'schacht': obj = schachtdeckel(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);
|
const rnd = mulberry32(seed + 7);
|
||||||
// Poren und Adern, typisch fuer Travertin
|
// Poren und Adern, typisch fuer Travertin
|
||||||
for (let i = 0; i < 900; i++) {
|
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.beginPath();
|
||||||
ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 1 + rnd() * 5, 0.6 + rnd() * 1.6, rnd() * 0.6, 0, Math.PI * 2);
|
ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 1 + rnd() * 5, 0.6 + rnd() * 1.6, rnd() * 0.6, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
@@ -242,7 +242,7 @@ function travertinFinish(ctx: CanvasRenderingContext2D, seed: number) {
|
|||||||
/** Travertinplatten 80 x 80 x 3 cm, Hauptsitzplatz. */
|
/** Travertinplatten 80 x 80 x 3 cm, Hauptsitzplatz. */
|
||||||
export const travertin80 = () => makeRelief('travertin80', 3.2, 0.70, 3.2,
|
export const travertin80 = () => makeRelief('travertin80', 3.2, 0.70, 3.2,
|
||||||
(ctx) => {
|
(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);
|
travertinFinish(ctx, 21);
|
||||||
},
|
},
|
||||||
(ctx) => slabHeight(ctx, 3.2, 0.8, 0.8, 0.010, 0.015));
|
(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. */
|
/** Einzelne Travertinplatte 40 x 60 x 3 cm - Textur fuer die losen Platten. */
|
||||||
export const travertinSlab = () => makeRelief('travertinSlab', 0.8, 0.70, 2.2,
|
export const travertinSlab = () => makeRelief('travertinSlab', 0.8, 0.70, 2.2,
|
||||||
(ctx) => {
|
(ctx) => {
|
||||||
ctx.fillStyle = 'rgb(213,199,171)';
|
ctx.fillStyle = 'rgb(228,218,196)';
|
||||||
ctx.fillRect(0, 0, SIZE, SIZE);
|
ctx.fillRect(0, 0, SIZE, SIZE);
|
||||||
travertinFinish(ctx, 26);
|
travertinFinish(ctx, 26);
|
||||||
},
|
},
|
||||||
@@ -259,7 +259,7 @@ export const travertinSlab = () => makeRelief('travertinSlab', 0.8, 0.70, 2.2,
|
|||||||
/** Travertinstellplatten als schmaler Randstreifen. */
|
/** Travertinstellplatten als schmaler Randstreifen. */
|
||||||
export const travertinstell = () => makeRelief('travertinstell', 2.4, 0.73, 3.0,
|
export const travertinstell = () => makeRelief('travertinstell', 2.4, 0.73, 3.0,
|
||||||
(ctx) => {
|
(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);
|
travertinFinish(ctx, 23);
|
||||||
},
|
},
|
||||||
(ctx) => slabHeight(ctx, 2.4, 0.4, 0.4, 0.012, 0.012));
|
(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,
|
export const travertinMauer = () => makeRelief('travertinMauer', 0.7, 0.68, 1.9,
|
||||||
(ctx) => {
|
(ctx) => {
|
||||||
ctx.fillStyle = 'rgb(203,183,148)';
|
ctx.fillStyle = 'rgb(221,208,182)';
|
||||||
ctx.fillRect(0, 0, SIZE, SIZE);
|
ctx.fillRect(0, 0, SIZE, SIZE);
|
||||||
const rnd = mulberry32(61);
|
const rnd = mulberry32(61);
|
||||||
// liegende Sedimentbaender, bewusst zurueckhaltend - sonst wirkt der Stein
|
// 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++) {
|
for (let i = 0; i < 34; i++) {
|
||||||
const y = rnd() * SIZE;
|
const y = rnd() * SIZE;
|
||||||
const hgt = 3 + rnd() * 12;
|
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.beginPath();
|
||||||
ctx.moveTo(0, y);
|
ctx.moveTo(0, y);
|
||||||
for (let x = 0; x <= SIZE; x += 24) ctx.lineTo(x, y + Math.sin(x / 55 + i) * 4);
|
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);
|
export const laubEibe = () => laub('laubEibe', 'rgb(64,92,52)', 201, 3.4);
|
||||||
/** Liguster: mittelgruen, groesseres Blatt. */
|
/** Liguster: mittelgruen, groesseres Blatt. */
|
||||||
export const laubLiguster = () => laub('laubLiguster', 'rgb(85,118,55)', 211, 5.2);
|
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. */
|
/** Trennt einen Polyzug an Knicken ueber `maxTurnDeg` in eigene Abschnitte. */
|
||||||
function splitAtCorners(line: Vec2[], maxTurnDeg: number): Vec2[][] {
|
function splitAtCorners(line: Vec2[], maxTurnDeg: number): Vec2[][] {
|
||||||
if (line.length <= 2) return [line];
|
if (line.length <= 2) return [line];
|
||||||
@@ -273,16 +286,26 @@ function hedgeMesh(
|
|||||||
pos.push(...A, ...D, ...C, ...A, ...C, ...B);
|
pos.push(...A, ...D, ...C, ...A, ...C, ...B);
|
||||||
uv.push(U0, v0, U0, v1, U1, v1, U0, v0, U1, v1, U1, v0);
|
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) {
|
if (i === 0 || i === stations.length - 2) {
|
||||||
const s = i === 0 ? s0 : s1;
|
const s = i === 0 ? s0 : s1;
|
||||||
const y = i === 0 ? y0 : y1;
|
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++) {
|
for (let k = 0; k < profile.length - 1; k++) {
|
||||||
const A = at(s, y, profile[k]);
|
const A = at(s, y, profile[k]);
|
||||||
const B = at(s, y, profile[k + 1]);
|
const B = at(s, y, profile[k + 1]);
|
||||||
if (i === 0) pos.push(...c, ...A, ...B); else pos.push(...c, ...B, ...A);
|
const ua = capUv(profile[k]), ub = capUv(profile[k + 1]);
|
||||||
uv.push(0.5, 0.5, 0, 0, 1, 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;
|
run += seg;
|
||||||
@@ -339,29 +362,32 @@ export function buildVegetation(
|
|||||||
|
|
||||||
const r = rng(4242);
|
const r = rng(4242);
|
||||||
|
|
||||||
// --- Eibenhecke `Hilli` 80/100 cm ---------------------------------------
|
// --- Eibenhecke `Hilli` als Sichtschutz ----------------------------------
|
||||||
// Der Plan setzt die Eiben in dichten Reihen: das ist eine geschnittene
|
// Der Sichtschutz ist bewusst **nicht** durchgehend: im Plan wechseln sich
|
||||||
// Hecke und der eigentliche Sichtschutz im Garten, keine Einzelkugeln.
|
// 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)
|
const eiben = plants.filter((p) => p.type === 'eibe' && p.x !== undefined)
|
||||||
.map((p) => [p.x!, p.z!] as Vec2);
|
.map((p) => [p.x!, p.z!] as Vec2);
|
||||||
// Reihen begradigen und an scharfen Knicken trennen: eine gemiterte Hecke
|
const rows = chainRows(eiben, 0.95).flatMap((row) => splitAtCorners(simplifyLine(row, 0.22), 32));
|
||||||
// um eine 90-Grad-Ecke herum verdreht sich sonst.
|
|
||||||
const rows = chainRows(eiben, 2.2).flatMap((row) => splitAtCorners(simplifyLine(row, 0.22), 32));
|
|
||||||
const eibeLaub = tex.laubEibe();
|
const eibeLaub = tex.laubEibe();
|
||||||
const eibeMat = new MeshStandardMaterial({
|
const eibeMat = new MeshStandardMaterial({
|
||||||
map: eibeLaub.map, normalMap: eibeLaub.normalMap ?? null,
|
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,
|
side: DoubleSide,
|
||||||
});
|
});
|
||||||
for (const row of rows) {
|
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) {
|
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 {
|
} else {
|
||||||
// Einzelstand: als Solitaerpolster
|
// Einzelstand: als schmales, aufrechtes Polster
|
||||||
const [x, z] = row[0];
|
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);
|
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.castShadow = true;
|
||||||
m.receiveShadow = true;
|
m.receiveShadow = true;
|
||||||
group.add(m);
|
group.add(m);
|
||||||
|
|||||||
+21
-2
@@ -19,6 +19,8 @@ export interface Surface {
|
|||||||
polygon: Vec2[];
|
polygon: Vec2[];
|
||||||
/** Ebene a*x + b*z + c, sofern es sich um einen gebauten Belag handelt. */
|
/** Ebene a*x + b*z + c, sofern es sich um einen gebauten Belag handelt. */
|
||||||
plane?: [number, number, number];
|
plane?: [number, number, number];
|
||||||
|
/** Zugehoerige Hauptebene, sofern fest vorgegeben. */
|
||||||
|
level?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,17 +76,34 @@ export interface Wall {
|
|||||||
|
|
||||||
export interface Prop {
|
export interface Prop {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'gartenhaus' | 'spielhaus' | 'flammengrill' | 'bank' | 'roboter' | 'wasserhahn' | 'schacht' | 'tor';
|
type: 'gartenhaus' | 'spielhaus' | 'flammengrill' | 'bank' | 'roboter' | 'wasserhahn' | 'schacht' | 'tor' | 'stufe';
|
||||||
x: number;
|
x: number;
|
||||||
z: number;
|
z: number;
|
||||||
w: number;
|
w: number;
|
||||||
d: number;
|
d: number;
|
||||||
h: number;
|
h: number;
|
||||||
rot: number;
|
rot: number;
|
||||||
|
/** Absolute Oberkante, sofern das Bauteil nicht auf dem Gelaende aufsitzt. */
|
||||||
|
top?: number;
|
||||||
note?: string;
|
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 {
|
export interface Opening {
|
||||||
face: 'north' | 'south' | 'east' | 'west';
|
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);
|
for (const s of site.surfaces) totals.set(s.type, (totals.get(s.type) ?? 0) + s.area);
|
||||||
const NAMES: Record<string, string> = {
|
const NAMES: Record<string, string> = {
|
||||||
travertin80: 'Travertinplatten 80×80×3 cm',
|
travertin80: 'Travertinplatten 80×80×3 cm',
|
||||||
travertin4060: 'Travertinplatten 40×60×3 cm',
|
dekorkies: 'Dekorkies',
|
||||||
travertinstell: 'Travertinstellplatten',
|
travertinstell: 'Travertinstellplatten',
|
||||||
keramik: 'Keramikplatten 60×60×2 cm',
|
keramik: 'Keramikplatten 60×60×2 cm',
|
||||||
verbundstein: 'Verbundsteine (Nachbar)',
|
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 heights = site.terrain.spots.filter((s) => s.zone === 'garten').map((s) => s.y);
|
||||||
const eibenCount = site.plants.filter((p) => p.type === 'eibe').length;
|
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 = `
|
$<HTMLElement>('info-body').innerHTML = `
|
||||||
<p>Alle Flächen, Mauern und Pflanzstandorte sind massstäblich aus dem Gartenplan
|
<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>
|
<dt>Höhen im Garten</dt><dd>${Math.min(...heights).toFixed(2)} bis ${Math.max(...heights).toFixed(2)} m</dd>
|
||||||
</dl>
|
</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>
|
<h3>Beläge</h3>
|
||||||
<dl>${[...totals.entries()]
|
<dl>${[...totals.entries()]
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
@@ -124,10 +133,13 @@ export function setupHud(opts: HudOptions): Hud {
|
|||||||
<h3>Bepflanzung und Ausstattung</h3>
|
<h3>Bepflanzung und Ausstattung</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>${site.trees.length} Solitärgehölze und Bäume, inkl. Apfelbaum</li>
|
<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>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.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>
|
<li>Gartenhaus 1,8 × 1,8 m, Spielhaus, Flammengrill mit Holzscheiterwand</li>
|
||||||
</ul>
|
</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 },
|
{ 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.
|
* Mauern, Stuetzelemente und Waende.
|
||||||
* `path` = Mittellinie, `t` = Dicke, `top`/`base` = Hoehe der Krone bzw. des Fusses
|
* `path` = Mittellinie, `t` = Dicke, `top`/`base` = Hoehe der Krone bzw. des Fusses
|
||||||
@@ -79,13 +110,13 @@ export const TREES = [
|
|||||||
export const WALLS = [
|
export const WALLS = [
|
||||||
{
|
{
|
||||||
id: 'travertinmauer-nord', type: 'travertin',
|
id: 'travertinmauer-nord', type: 'travertin',
|
||||||
note: 'Travertinmauer an der Grenze zum Nachbargrundstueck Zollet, stuetzt rund 1.5 m ab',
|
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.30, base: -1.90,
|
path: [[-18.17, -6.895], [-13.82, -6.895]], t: 0.55, top: -0.33, base: -1.95,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sitzmauer-nord', type: 'travertin',
|
id: 'sitzmauer-nord', type: 'travertin',
|
||||||
note: 'Sitzmauer 25 cm ueber der Travertinmauer (gelbe Kote "25" im Plan)',
|
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.03, base: -0.45,
|
path: [[-20.08, -6.47], [-13.82, -6.47]], t: 0.30, top: -0.08, base: -0.55,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'winkelelemente-west', type: 'beton',
|
id: 'winkelelemente-west', type: 'beton',
|
||||||
@@ -94,14 +125,17 @@ export const WALLS = [
|
|||||||
t: 0.22, top: null, base: null, retains: 'west',
|
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',
|
id: 'travertinsitzmauer-west', type: 'travertin',
|
||||||
note: 'Travertinsitzmauer als Abschluss des Hauptsitzplatzes gegen den Rasen',
|
note: 'Travertinsitzmauer: 31 cm Hoehensprung vom Hauptsitzplatz zum Rasen',
|
||||||
path: [[-18.64, 0.755], [-14.73, 0.755]], t: 0.41, top: 0.40, base: -0.45, seating: true,
|
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',
|
id: 'travertinsitzmauer-sued', type: 'travertin',
|
||||||
note: 'Schenkel der Travertinsitzmauer laengs des Rasens',
|
note: 'Schenkel der Travertinsitzmauer laengs des Rasens, gleicher Hoehensprung',
|
||||||
path: [[-18.44, 0.55], [-18.44, 3.94]], t: 0.40, top: 0.40, base: -0.40, seating: true,
|
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',
|
id: 'betonmauer-nachbar', type: 'beton',
|
||||||
@@ -121,7 +155,7 @@ export const WALLS = [
|
|||||||
{
|
{
|
||||||
id: 'stufe-gartentor', type: 'travertin',
|
id: 'stufe-gartentor', type: 'travertin',
|
||||||
note: 'Blockstufen beim Gartentor an der Suedwestecke',
|
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',
|
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,
|
{ 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 },
|
{ 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.
|
* Hoehe des Sichtschutzes. Er besteht aus aufrechten Travertinstelen
|
||||||
* Positionen aus den Planbeschriftungen "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 = [
|
export const STELE_HEIGHTS = [
|
||||||
{ 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 },
|
{ id: 'sued', height: 1.80, test: (x, z) => z > 3.0 },
|
||||||
{ 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 },
|
{ id: 'nord', height: 1.50, test: (x, z) => z < -5.0 },
|
||||||
{ 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 },
|
{ id: 'west', height: 1.50, test: (x) => x < -19.4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+94
-24
@@ -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, STELEN,
|
SURFACE_BY_COLOR, PLANT_BY_COLOR, TREES, WALLS, PROPS, LEVELS, STELE_HEIGHTS,
|
||||||
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';
|
||||||
@@ -100,24 +100,45 @@ const bboxOf = (pts) => {
|
|||||||
*/
|
*/
|
||||||
const slabs = [];
|
const slabs = [];
|
||||||
const surfaces = [];
|
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) {
|
for (const s of raw.shapes) {
|
||||||
if (s.op !== 'f' || s.circle) continue;
|
if (s.op !== 'f' || s.circle) continue;
|
||||||
let type = SURFACE_BY_COLOR[s.hex];
|
let type = SURFACE_BY_COLOR[s.hex];
|
||||||
if (!type) continue;
|
if (!type) continue;
|
||||||
const b = bboxOf(s.pts);
|
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)
|
// 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) {
|
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({
|
slabs.push({ x: round(cx), z: round(cz), w: round(b.w, 2), d: round(b.d, 2) });
|
||||||
x: round((b.x0 + b.x1) / 2), z: round((b.z0 + b.z1) / 2),
|
continue;
|
||||||
w: round(b.w, 2), d: round(b.d, 2),
|
}
|
||||||
|
|
||||||
|
// 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;
|
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' && (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 (s.area < 0.3) continue;
|
||||||
if (NOT_A_SURFACE.has(type)) 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);
|
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
|
// 2. Hecken und Pflanzen aus den Plankreisen
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -212,43 +242,80 @@ function distToPolygon(x, z, poly) {
|
|||||||
return best;
|
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 = [];
|
const flatSurfaces = [];
|
||||||
for (const s of surfaces) {
|
for (const s of surfaces) {
|
||||||
if (!FLAT_TYPES.has(s.type) || s.area < 0.6) continue;
|
if (!FLAT_TYPES.has(s.type) || s.area < 0.4) continue;
|
||||||
const inside = terrainSpots.filter((sp) => pointInPolygon(sp.x, sp.z, s.polygon));
|
|
||||||
// Nur Koten derselben Gelaendezone heranziehen - ueber eine Stuetzmauer
|
const fixed = levelFor(s);
|
||||||
// hinweg gehoert die Nachbarkote nicht zu dieser Flaeche.
|
let plane, ys;
|
||||||
|
|
||||||
|
if (fixed) {
|
||||||
|
plane = { a: 0, b: 0, c: fixed.level };
|
||||||
|
ys = [fixed.level];
|
||||||
|
s.level = fixed.id;
|
||||||
|
} else {
|
||||||
|
const inside = terrainSpots.filter((sp) => owner.get(sp) === s);
|
||||||
const [pcx, pcz] = centroid(s.polygon);
|
const [pcx, pcz] = centroid(s.polygon);
|
||||||
const zone = zoneOf(pcx, pcz);
|
const zone = zoneOf(pcx, pcz);
|
||||||
const near = terrainSpots.filter(
|
const near = inside.length
|
||||||
(sp) => !inside.includes(sp) && sp.zone === zone && distToPolygon(sp.x, sp.z, s.polygon) < 1.1,
|
? []
|
||||||
);
|
: terrainSpots.filter((sp) => !owner.has(sp) && sp.zone === zone && distToPolygon(sp.x, sp.z, s.polygon) < 1.2);
|
||||||
const pool = [...inside, ...near];
|
const pool = [...inside, ...near];
|
||||||
if (!pool.length) continue;
|
if (!pool.length) continue;
|
||||||
|
|
||||||
const ys = pool.map((p) => p.y);
|
ys = pool.map((p) => p.y);
|
||||||
const mean = ys.reduce((a, b) => a + b, 0) / ys.length;
|
const mean = ys.reduce((a, b) => a + b, 0) / ys.length;
|
||||||
const px = pool.reduce((a, p) => a + p.x, 0) / pool.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 pz = pool.reduce((a, p) => a + p.z, 0) / pool.length;
|
||||||
|
|
||||||
let plane = pool.length >= 3 ? fitPlane(pool) : null;
|
let fit = pool.length >= 3 ? fitPlane(pool) : null;
|
||||||
// Ein Belag hat hoechstens rund 5 % Gefaelle - genug fuer die Entwaesserung. Bei fast kollinearen Koten
|
// Ein Belag hat hoechstens rund 4 % Gefaelle. Bei fast kollinearen Koten
|
||||||
// kippt die Ausgleichsebene weg; dann nur das Gefaelle begrenzen und die
|
// kippt die Ausgleichsebene weg; dann nur das Gefaelle begrenzen und die
|
||||||
// Ebene wieder am Schwerpunkt der Koten aufhaengen - nie am Polygonrand,
|
// Ebene am Schwerpunkt der Koten aufhaengen, nie am Polygonrand.
|
||||||
// sonst wandert sie mit dem gekappten Gefaelle davon.
|
if (fit) {
|
||||||
if (plane) {
|
const slope = Math.hypot(fit.a, fit.b);
|
||||||
const slope = Math.hypot(plane.a, plane.b);
|
const k = slope > 0.04 ? 0.04 / slope : 1;
|
||||||
const k = slope > 0.05 ? 0.05 / slope : 1;
|
const a = fit.a * k, b = fit.b * k;
|
||||||
const a = plane.a * k, b = plane.b * k;
|
|
||||||
plane = { a, b, c: mean - (a * px + b * pz) };
|
plane = { a, b, c: mean - (a * px + b * pz) };
|
||||||
} else {
|
} else {
|
||||||
plane = { a: 0, b: 0, c: mean };
|
plane = { a: 0, b: 0, c: mean };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.plane = [round(plane.a, 5), round(plane.b, 5), round(plane.c, 4)];
|
s.plane = [round(plane.a, 5), round(plane.b, 5), round(plane.c, 4)];
|
||||||
flatSurfaces.push({
|
flatSurfaces.push({
|
||||||
polygon: s.polygon, plane, type: s.type,
|
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,
|
lamps,
|
||||||
walls,
|
walls,
|
||||||
props: PROPS,
|
props: PROPS,
|
||||||
stelen: STELEN,
|
stelen,
|
||||||
building: BUILDING,
|
building: BUILDING,
|
||||||
neighbourBuildings: NEIGHBOUR_BUILDINGS,
|
neighbourBuildings: NEIGHBOUR_BUILDINGS,
|
||||||
startViews: START_VIEWS,
|
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(' ' + t.padEnd(16) + a.toFixed(1).padStart(7) + ' m2');
|
||||||
}
|
}
|
||||||
console.log(`Lose Travertinplatten 40x60: ${slabs.length}`);
|
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}`);
|
console.log(`Hoehenraster ${nx} x ${nz} (${CELL} m), Koten ${terrainSpots.length}`);
|
||||||
const hs = Array.from(smoothed);
|
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.)`);
|
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