Qualitaetssprung: ebene Belaege, echte Bauteile, Sichtschutz
Deploy App / deploy (push) Successful in 49s

Nach der Plankontrolle ueberarbeitet:

- Belaege sind eben. Je Belag wird aus den Hoehenkoten eine Ebene bestimmt
  (Gefaelle <= 5 %, am Kotenschwerpunkt aufgehaengt, nur Koten derselben
  Gelaendezone). Das Raster wird darauf gezwungen und dort nicht geglaettet.
  Jeder Belag bekommt eine senkrechte Sichtkante zum Gelaende.
- Mauern sind Bauteile: Travertin-Schichtmauerwerk mit versetzten Stossfugen
  und Abdeckplatte, einzelne Winkelelemente mit Fussplatte, Ortbeton in
  Schaltafelfeldern.
- Sichtschutz: die Eibenreihen des Plans werden zu geschnittenen Hecken
  verkettet, ebenso die Ligusterhecke. Lamellenwand im Planrhythmus.
- Der Plan zeichnet die Travertinplatten 40x60 einzeln im Dekorkies - jetzt
  30 erhabene Platten an den Planpositionen statt geschlossenem Belag.
- Normalmaps aus dem Fugenbild fuer Platten, Pflaster, Holzschalung und Laub.
- Nachbarhaus Zollet richtig platziert (stand auf dem Nachbargarten) und
  wie Haus 9a ausgebildet.

Behobene Fehler: boxGeometry erzeugte Seiten und Deckel mit umgekehrtem
Wickelsinn - saemtliche Boxen waren innen nach aussen gekehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 07:47:47 +02:00
co-authored by Claude Opus 5
parent f2b7b1b236
commit 8979775895
24 changed files with 1452 additions and 319 deletions
+21 -20
View File
@@ -14,8 +14,8 @@
/** Farbe im Plan -> Belagsart. Reihenfolge egal, Zuordnung ueber Hex. */
export const SURFACE_BY_COLOR = {
'#ffffcc': 'travertin80', // Travertinplatten 80x80x3, Hauptsitzplatz West
'#fcffe7': 'travertin4060', // Travertinplatten 40x60x3 mit Dekorkies + Keramikterrasse Zollet
'#ffffcc': 'travertin80', // Travertinplatten: grosse Felder 80x80x3, kleine Rechtecke 40x60x3
'#fcffe7': 'dekorkies', // Dekorkies-Bett; noerdlich der Grenze die Keramikterrasse Zollet
'#f8ffcc': 'travertinstell',// Travertinstellplatten-Streifen
'#eee8cc': 'travertinmauer',// Mauerkronen, Sitzmauern, schwebende Stufen
'#e5ffe5': 'rasen',
@@ -115,8 +115,13 @@ export const WALLS = [
},
{
id: 'holzscheiterwand', type: 'holzscheiter',
note: 'Holzscheiterwand 150 x 200 cm hinter dem Flammengrill',
path: [[-5.75, 4.35], [-4.25, 4.35]], t: 0.45, top: 1.75, base: 0.26,
note: 'Holzscheiterwand 150 cm breit, 200 cm hoch, hinter dem Flammengrill',
path: [[-5.75, 4.35], [-4.25, 4.35]], t: 0.45, top: 2.26, base: 0.20,
},
{
id: 'stufe-gartentor', type: 'travertin',
note: 'Blockstufen beim Gartentor an der Suedwestecke',
path: [[-21.05, 2.15], [-21.05, 3.75]], t: 0.80, top: -0.13, base: -0.75,
},
{
id: 'lamellenwand', type: 'lamellen',
@@ -153,26 +158,19 @@ export const EXTRA_SURFACES = [
note: 'Gedeckter Sitzplatz 14.4 m2, Keramikplatten 60x60x2 cm',
polygon: [[-13.93, -3.06], [-8.73, -3.06], [-8.73, 0.86], [-13.93, 0.86]],
},
{
type: 'travertin80',
note: 'Schwebende Stufe Travertin, obere Tritthoehe -0.17',
polygon: [[-14.74, -2.69], [-13.93, -2.69], [-13.93, 0.56], [-14.74, 0.56]],
},
{
type: 'travertin80',
note: 'Schwebende Stufe Travertin, untere Tritthoehe -0.33',
polygon: [[-15.28, -3.08], [-14.48, -3.08], [-14.48, 0.12], [-15.28, 0.12]],
},
{
type: 'travertin80',
note: 'Schwebende Stufen beim Gartentor an der Suedwestecke',
polygon: [[-21.45, 2.15], [-20.65, 2.15], [-20.66, 3.75], [-21.45, 3.75]],
},
{
type: 'tegula',
note: 'Feuerwehrzufahrt und Zugangsweg, Tegula-Pflastersteine grau',
polygon: [[-25.60, -9.00], [-22.10, -9.00], [-22.10, 6.40], [-25.60, 6.40]],
},
{
// Die schwebenden Stufen im Plan: eine Zwischenebene zwischen dem
// gedeckten Sitzplatz (-0.02) und dem Hauptsitzplatz (rund -0.15).
// Die Hoehensprünge zeigen sich als Sichtkanten der Belaege.
type: 'travertin80',
note: 'Schwebende Stufen (Travertin) zwischen den beiden Sitzplaetzen',
polygon: [[-15.32, -3.12], [-13.87, -3.12], [-13.87, 0.60], [-15.32, 0.60]],
},
];
/**
@@ -237,8 +235,11 @@ export const BUILDING = {
*/
export const NEIGHBOUR_BUILDINGS = [
{
// Gleicher Baukoerper wie 9a, gegen Nordwesten versetzt und 1.70 m tiefer.
// Die Westkante endet bei x = -13.9, damit die Keramikterrasse des
// Nachbargartens (Plan: Schwerpunkt -17.1 / -8.6) frei bleibt.
id: 'haus-9', name: 'Haus 9 - Sharon und Alan Zollet',
outline: [[-15.60, -13.60], [-1.20, -13.60], [-1.20, -6.87], [-15.60, -6.87]],
outline: [[-13.90, -14.70], [0.20, -14.70], [0.20, -6.87], [-13.90, -6.87]],
floorLevel: -1.70, roofLevel: 4.45,
},
];
+93 -21
View File
@@ -87,12 +87,39 @@ function fitPlane(points) {
/** Bauteile, die als Mauer modelliert werden - nicht als begehbare Flaeche. */
const NOT_A_SURFACE = new Set(['travertinmauer', 'winkelelement', 'spielhaus', 'gartenhaus', 'roboter', 'ligusterhecke']);
const bboxOf = (pts) => {
let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity;
for (const [x, z] of pts) { x0 = Math.min(x0, x); x1 = Math.max(x1, x); z0 = Math.min(z0, z); z1 = Math.max(z1, z); }
return { x0, z0, x1, z1, w: x1 - x0, d: z1 - z0 };
};
/**
* Einzelne Travertinplatten 40x60x3 cm. Der Plan zeichnet sie als Reihe
* loser Platten im Dekorkies - kein geschlossener Belag. Sie werden als
* erhabene Platten modelliert, nicht als Flaeche.
*/
const slabs = [];
const surfaces = [];
for (const s of raw.shapes) {
if (s.op !== 'f' || s.circle) continue;
const type = SURFACE_BY_COLOR[s.hex];
let type = SURFACE_BY_COLOR[s.hex];
if (!type) continue;
if (s.area < 0.35) continue;
const b = bboxOf(s.pts);
// 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),
});
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 (s.area < 0.3) continue;
if (NOT_A_SURFACE.has(type)) continue;
const poly = polygonArea(s.pts) < 0 ? [...s.pts].reverse() : s.pts;
surfaces.push({ type, area: round(s.area, 2), polygon: poly.map((p) => [round(p[0]), round(p[1])]) });
@@ -162,26 +189,66 @@ const spots = [
// Die Kote 1.47 gehoert zum Spielhauspodest, nicht zum Gelaende.
const terrainSpots = spots.filter((s) => s.zone && s.y < 1.2);
/** Flaechen, die als Ebene ausgebildet sind - dort wird das Raster begradigt. */
const FLAT_TYPES = new Set(['travertin80', 'travertin4060', 'travertinstell', 'keramik', 'verbundstein', 'tegula', 'spielsand']);
/**
* Belaege sind gebaute Flaechen und muessen eben sein. Fuer jede solche Flaeche
* wird eine Ebene bestimmt - aus den Koten darin und knapp daneben, mit
* begrenztem Gefaelle. Die Ebene geht als `plane` mit ins Szenenmodell; das
* Gelaenderaster wird darauf gezwungen und in diesen Feldern nicht geglaettet.
*/
const FLAT_TYPES = new Set([
'travertin80', 'travertin4060', 'travertinstell', 'keramik',
'verbundstein', 'tegula', 'spielsand', 'dekorkies',
]);
/** Abstand eines Punktes zum Polygonrand (positiv aussen wie innen). */
function distToPolygon(x, z, poly) {
let best = Infinity;
for (let i = 0; i < poly.length; i++) {
const [ax, az] = poly[i], [bx, bz] = poly[(i + 1) % poly.length];
const dx = bx - ax, dz = bz - az;
const l2 = dx * dx + dz * dz;
const t = l2 > 0 ? Math.max(0, Math.min(1, ((x - ax) * dx + (z - az) * dz) / l2)) : 0;
best = Math.min(best, Math.hypot(x - (ax + dx * t), z - (az + dz * t)));
}
return best;
}
const flatSurfaces = [];
for (const s of surfaces) {
if (!FLAT_TYPES.has(s.type) || s.area < 1.0) continue;
if (!FLAT_TYPES.has(s.type) || s.area < 0.6) continue;
const inside = terrainSpots.filter((sp) => pointInPolygon(sp.x, sp.z, s.polygon));
if (!inside.length) continue;
let plane = inside.length >= 3 ? fitPlane(inside) : null;
// Fast kollineare Koten liefern wilde Ebenen. Nur uebernehmen, wenn das
// Gefaelle plausibel ist (< 10 %) und alle Koten getroffen werden.
// 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;
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;
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 maxDev = Math.max(...inside.map((p) => Math.abs(plane.a * p.x + plane.b * p.z + plane.c - p.y)));
if (slope > 0.10 || maxDev > 0.06) plane = null;
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) };
} else {
plane = { a: 0, b: 0, c: mean };
}
const ys = inside.map((p) => p.y);
const mean = ys.reduce((a, b) => a + b, 0) / ys.length;
s.plane = [round(plane.a, 5), round(plane.b, 5), round(plane.c, 4)];
flatSurfaces.push({
polygon: s.polygon, plane, mean, type: s.type,
min: Math.min(...ys) - 0.15, max: Math.max(...ys) + 0.15,
polygon: s.polygon, plane, type: s.type,
min: Math.min(...ys) - 0.12, max: Math.max(...ys) + 0.12,
});
}
@@ -206,6 +273,9 @@ function interpolate(x, z, zoneId) {
return num / den;
}
/** Merkt sich, welche Rasterfelder von einem Belag festgelegt sind. */
const locked = new Uint8Array(nx * nz);
for (let j = 0; j < nz; j++) {
const z = GRID.z0 + j * CELL;
for (let i = 0; i < nx; i++) {
@@ -225,8 +295,8 @@ for (let j = 0; j < nz; j++) {
y = interpolate(x, z, zone);
for (const f of flatSurfaces) {
if (!pointInPolygon(x, z, f.polygon)) continue;
y = f.plane ? f.plane.a * x + f.plane.b * z + f.plane.c : f.mean;
y = Math.max(f.min, Math.min(f.max, y));
y = Math.max(f.min, Math.min(f.max, f.plane.a * x + f.plane.b * z + f.plane.c));
locked[j * nx + i] = 1;
break;
}
}
@@ -234,16 +304,16 @@ for (let j = 0; j < nz; j++) {
}
}
// Zwei Glaettungsrunden, damit weder die Kanten der Ebenen schneiden noch
// einzelne Koten als Beulen stehenbleiben.
// Glaetten - aber nur die freien Felder. Die Belaege bleiben exakt eben.
let smoothed = Float32Array.from(height);
for (let pass = 0; pass < 2; pass++) {
for (let pass = 0; pass < 3; pass++) {
const src = smoothed;
const dst = Float32Array.from(src);
for (let j = 1; j < nz - 1; j++) {
for (let i = 1; i < nx - 1; i++) {
const k = j * nx + i;
dst[k] = (src[k] * 4 + src[k - 1] + src[k + 1] + src[k - nx] + src[k + nx]) / 8;
if (locked[k]) continue;
dst[k] = (src[k] * 3 + src[k - 1] + src[k + 1] + src[k - nx] + src[k + nx]) / 7;
}
}
smoothed = dst;
@@ -323,6 +393,7 @@ const site = {
spots: terrainSpots.map((s) => ({ x: round(s.x), z: round(s.z), y: s.y, zone: s.zone })),
},
surfaces,
slabs,
plants,
trees,
lamps,
@@ -345,6 +416,7 @@ console.log(`Belagsflaechen ${surfaces.length}:`);
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(`Hoehenraster ${nx} x ${nz} (${CELL} m), Koten ${terrainSpots.length}`);
const hs = Array.from(smoothed);