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