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:
+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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user