Files
Gartenvisualisierung/src/scene/walls.ts
T
admGitAICDSandClaude Opus 5 8979775895
Deploy App / deploy (push) Successful in 49s
Qualitaetssprung: ebene Belaege, echte Bauteile, Sichtschutz
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>
2026-09-09 07:47:47 +02:00

294 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Mauern als gebaute Elemente, nicht als hochgezogene Flaechen.
*
* Der Plan nennt vier verschiedene Konstruktionen, die auch verschieden
* aussehen muessen:
*
* - **Travertinmauer / Sitzmauer** – Schichtmauerwerk aus einzelnen Bloecken
* mit Lagerfugen und einer ueberstehenden Abdeckplatte.
* - **Winkelelemente aus Beton** – Fertigteile von rund 1 m Breite, einzeln
* versetzt, mit sichtbaren Stossfugen und leicht geneigter Ansichtsflaeche.
* - **Betonmauer Nachbar** – Ortbeton in Schaltafelfeldern mit Ankerloechern
* und abgefaster Krone.
* - **Lamellenwand** – senkrechte Holzlamellen im Rhythmus des Plans.
* - **Holzscheiterwand** – Stahlrahmen, gefuellt mit gestapelten Scheitern.
*/
import {
BufferGeometry, Group, Mesh, MeshStandardMaterial,
type MeshStandardMaterialParameters,
} from 'three';
import type { Vec2, Wall } from '../site';
import { boxGeometry, mergeGeometries, wallGeometry } from './geometry';
import * as tex from './textures';
function material(belag: tex.Belag, extra: MeshStandardMaterialParameters = {}) {
return new MeshStandardMaterial({
map: belag.map,
normalMap: belag.normalMap ?? null,
normalScale: belag.normalScale,
roughness: belag.roughness,
metalness: 0,
...extra,
});
}
function rng(seed: number) {
let a = seed >>> 0;
return () => {
a = (a + 0x9e3779b9) >>> 0;
let t = Math.imul(a ^ (a >>> 16), 0x21f0aaad);
t = Math.imul(t ^ (t >>> 15), 0x735a2d97);
return ((t ^ (t >>> 15)) >>> 0) / 4294967296;
};
}
/** Wertet ein Hoehenprofil (Stuetzelemente) an der Stelle t aus. */
function profileAt(profile: number[] | undefined, t: number, fallback: number): number {
if (!profile || !profile.length) return fallback;
const f = t * (profile.length - 1);
const i = Math.max(0, Math.min(profile.length - 2, Math.floor(f)));
const u = f - i;
return profile[i] * (1 - u) + profile[i + 1] * u;
}
interface Axis {
/** Gesamtlaenge des Polyzugs. */
length: number;
/** Punkt, Richtung und Normale bei Bogenlaenge s. */
at(s: number): { x: number; z: number; dx: number; dz: number; angle: number; t: number };
}
function axisOf(path: Vec2[]): Axis {
const segs = path.slice(1).map((p, i) => ({
a: path[i], b: p, len: Math.hypot(p[0] - path[i][0], p[1] - path[i][1]),
}));
const length = segs.reduce((s, g) => s + g.len, 0);
return {
length,
at(s: number) {
let want = Math.max(0, Math.min(length, s));
for (let i = 0; i < segs.length; i++) {
const g = segs[i];
if (want <= g.len || i === segs.length - 1) {
const u = g.len > 0 ? Math.min(1, want / g.len) : 0;
const dx = (g.b[0] - g.a[0]) / (g.len || 1);
const dz = (g.b[1] - g.a[1]) / (g.len || 1);
return {
x: g.a[0] + (g.b[0] - g.a[0]) * u,
z: g.a[1] + (g.b[1] - g.a[1]) * u,
dx, dz,
angle: (Math.atan2(dz, dx) * 180) / Math.PI,
t: length > 0 ? s / length : 0,
};
}
want -= g.len;
}
return { x: path[0][0], z: path[0][1], dx: 1, dz: 0, angle: 0, t: 0 };
},
};
}
/**
* Schichtmauerwerk: Lage fuer Lage aus einzelnen Bloecken, darueber eine
* ueberstehende Abdeckplatte. Fugen entstehen durch echte Zwischenraeume.
*/
function masonry(
axis: Axis, thickness: number,
top: (t: number) => number, base: (t: number) => number,
seed: number,
): { blocks: BufferGeometry; coping: BufferGeometry } {
const COURSE = 0.19; // Schichthoehe
const JOINT = 0.012; // Lagerfuge
const CAP = 0.06; // Abdeckplatte
const rnd = rng(seed);
const blocks: BufferGeometry[] = [];
const topMid = top(0.5);
const baseMid = base(0.5);
const wallTop = topMid - CAP;
const courses = Math.max(1, Math.round((wallTop - baseMid) / COURSE));
const courseH = (wallTop - baseMid) / courses;
for (let c = 0; c < courses; c++) {
const y0 = baseMid + c * courseH;
const y1 = y0 + courseH - JOINT;
// Jede zweite Lage beginnt mit einem halben Stein - so versetzen sich die
// Stossfugen, ohne dass Luecken entstehen.
let s = 0;
let first = true;
while (s < axis.length - 0.02) {
let len = 0.42 + rnd() * 0.40;
if (first && c % 2 === 1) len *= 0.55;
first = false;
len = Math.min(len, axis.length - s);
// Reststueck lieber an den letzten Stein anhaengen als einen Splitter setzen
if (axis.length - (s + len) < 0.18) len = axis.length - s;
const p = axis.at(s + len / 2);
const d = thickness + (rnd() - 0.5) * 0.016;
blocks.push(boxGeometry(p.x, p.z, len - JOINT, d, y0, y1, p.angle, 1.6));
s += len;
}
}
// Abdeckplatte, beidseits 2 cm ueberstehend
const coping: BufferGeometry[] = [];
const capSegs = Math.max(2, Math.round(axis.length / 0.9));
for (let i = 0; i < capSegs; i++) {
const s0 = (i / capSegs) * axis.length;
const len = axis.length / capSegs;
const p = axis.at(s0 + len / 2);
const y = top(p.t);
coping.push(boxGeometry(p.x, p.z, len - 0.006, thickness + 0.04, y - CAP, y, p.angle, 1.6));
}
return { blocks: mergeGeometries(blocks), coping: mergeGeometries(coping) };
}
/**
* Winkelelemente: Fertigteile von rund 1 m Breite. Sichtbar sind die
* Ansichtsflaeche mit den Stossfugen und die schmale Krone.
*/
function angleElements(
axis: Axis, thickness: number,
crest: (t: number) => number, foot: (t: number) => number,
): BufferGeometry {
const WIDTH = 1.0;
const JOINT = 0.015;
const parts: BufferGeometry[] = [];
const n = Math.max(1, Math.round(axis.length / WIDTH));
const w = axis.length / n;
for (let i = 0; i < n; i++) {
const p = axis.at(i * w + w / 2);
const yTop = crest(p.t);
const yBottom = foot(p.t);
// Stiel: oben duenn, unten dicker - wie ein echtes L-Element
const stem = 3;
for (let k = 0; k < stem; k++) {
const a = yBottom + ((yTop - yBottom) * k) / stem;
const b = yBottom + ((yTop - yBottom) * (k + 1)) / stem;
const th = thickness * (1.55 - 0.4 * (k / stem));
parts.push(boxGeometry(p.x, p.z, w - JOINT, th, a, b, p.angle, 2.0));
}
// Fussplatte, meist verdeckt, aber am Fuss sichtbar
parts.push(boxGeometry(
p.x - p.dz * thickness * 1.1, p.z + p.dx * thickness * 1.1,
w - JOINT, thickness * 2.4, yBottom, yBottom + 0.14, p.angle, 2.0,
));
}
return mergeGeometries(parts);
}
/** Ortbetonwand in Schaltafelfeldern mit abgefaster Krone. */
function concretePanels(
axis: Axis, thickness: number,
top: (t: number) => number, base: (t: number) => number,
): BufferGeometry {
const FIELD = 2.5;
const JOINT = 0.012;
const parts: BufferGeometry[] = [];
const n = Math.max(1, Math.round(axis.length / FIELD));
const w = axis.length / n;
for (let i = 0; i < n; i++) {
const p = axis.at(i * w + w / 2);
const yTop = top(p.t);
const yBase = base(p.t);
parts.push(boxGeometry(p.x, p.z, w - JOINT, thickness, yBase, yTop - 0.03, p.angle, 2.0));
// abgefaste Krone: schmaler Aufsatz
parts.push(boxGeometry(p.x, p.z, w - JOINT, thickness - 0.03, yTop - 0.03, yTop, p.angle, 2.0));
}
return mergeGeometries(parts);
}
export function buildWalls(walls: Wall[], height: (x: number, z: number) => number): Group {
const group = new Group();
group.name = 'Mauern und Wände';
const travertin = tex.travertinMauer();
const beton = tex.beton();
const holz = tex.fassadeHolz();
const scheiter = tex.holzscheiter();
const stahl = new MeshStandardMaterial({ color: 0x3f3a35, roughness: 0.5, metalness: 0.6 });
for (const w of walls) {
const axis = axisOf(w.path);
const top = (t: number) => {
if (w.crestProfile) return profileAt(w.crestProfile, t, 0);
if (w.top !== null) return w.top;
const p = axis.at(t * axis.length);
return height(p.x, p.z) + 0.4;
};
const base = (t: number) => {
if (w.footProfile) return profileAt(w.footProfile, t, -1);
if (w.base !== null) return w.base;
const p = axis.at(t * axis.length);
return height(p.x, p.z) - 0.5;
};
const node = new Group();
node.name = w.id;
node.userData.note = w.note;
if (w.type === 'travertin') {
const { blocks, coping } = masonry(axis, w.t, top, base, w.id.length * 977 + 13);
const body = new Mesh(blocks, material(travertin));
const cap = new Mesh(coping, material(travertin, { roughness: 0.6 }));
for (const m of [body, cap]) { m.castShadow = true; m.receiveShadow = true; node.add(m); }
} else if (w.retains) {
const m = new Mesh(angleElements(axis, w.t, top, base), material(beton));
m.castShadow = true; m.receiveShadow = true;
node.add(m);
} else if (w.type === 'beton') {
const m = new Mesh(concretePanels(axis, w.t, top, base), material(beton));
m.castShadow = true; m.receiveShadow = true;
node.add(m);
} else if (w.openSlats) {
// Lamellenwand: Rahmen aus Kanthoelzern, dazwischen senkrechte Bretter
const mat = material(holz, { roughness: 0.84 });
const parts: BufferGeometry[] = [];
const n = Math.max(2, Math.round(axis.length / 0.9));
for (let i = 0; i <= n; i++) {
const p = axis.at((i / n) * axis.length);
parts.push(boxGeometry(p.x, p.z, 0.10, w.t, base(p.t), top(p.t), p.angle, 1.2));
}
const slats = new Mesh(mergeGeometries(parts), mat);
slats.castShadow = true; slats.receiveShadow = true;
node.add(slats);
const band = new Mesh(
wallGeometry(w.path, 0.10, (t) => top(t), (t) => base(t), { segments: 8, tile: holz.tile }),
mat,
);
band.castShadow = true; band.receiveShadow = true;
node.add(band);
} else if (w.type === 'holzscheiter') {
// Stahlrahmen mit Scheiterfuellung
const y0 = base(0.5), y1 = top(0.5);
const fill = new Mesh(
boxGeometry(
(w.path[0][0] + w.path[1][0]) / 2, (w.path[0][1] + w.path[1][1]) / 2,
axis.length - 0.12, w.t - 0.08, y0 + 0.06, y1 - 0.06, axis.at(0).angle, scheiter.tile,
),
material(scheiter, { roughness: 0.92 }),
);
fill.castShadow = true; fill.receiveShadow = true;
node.add(fill);
const frameParts: BufferGeometry[] = [];
const cx = (w.path[0][0] + w.path[1][0]) / 2, cz = (w.path[0][1] + w.path[1][1]) / 2;
const ang = axis.at(0).angle;
frameParts.push(boxGeometry(cx, cz, axis.length, w.t, y0, y0 + 0.06, ang, 1));
frameParts.push(boxGeometry(cx, cz, axis.length, w.t, y1 - 0.06, y1, ang, 1));
for (const s of [-1, 1]) {
const p = axis.at(s < 0 ? 0.03 : axis.length - 0.03);
frameParts.push(boxGeometry(p.x, p.z, 0.06, w.t, y0, y1, ang, 1));
}
const frame = new Mesh(mergeGeometries(frameParts), stahl);
frame.castShadow = true;
node.add(frame);
}
group.add(node);
}
return group;
}