Qualitaetssprung: ebene Belaege, echte Bauteile, Sichtschutz
Deploy App / deploy (push) Successful in 49s
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:
+21
-20
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user