Memo vom 11.09.2026 umgesetzt
Deploy App / deploy (push) Successful in 28s

Haus
- Oeffnungen aus den Fassadenplaenen von EHLE abgegriffen. Zum Garten steht
  im Erdgeschoss eine einzige, mittige Fensterfront (x -8.82 bis -4.67),
  daneben das Lamellenband vor dem gedeckten Sitzplatz; im Obergeschoss zwei
  Fenster mit zwei schmalen Lamellen dazwischen.
- Der gedeckte Sitzplatz ist nur noch gegen Westen offen, so wie es die
  Suedwestfassade als "Aussensitzplatz" zeigt. Gegen den Garten steht dort
  eine geschlossene Wand - bisher schaute man durch die Fassade hinein.
- Hauszugang an der Nordostfassade statt an der Nordwestfassade: Tuerfluegel
  und feststehende Verglasung, zusammen 3.15 m, darueber das Vordach. Das
  Vordach kennt jetzt seine Fassadenseite.
- Die Fassadennamen im Datenmodell waren vertauscht; jetzt mit Tabelle im
  README festgehalten.

Garten
- Sichtschutz: die gruenen Punkte im Kiesbett sind Ziergraeser, keine
  Polster. Neue Halmgeometrie - schmales, sich verjuengendes Band, das nach
  oben ueberhaengt, rund 1.10 m. Die Bodenleuchten sind jetzt Strahler und
  leuchten Stelen und Graeser von unten an.
- Baenkli: die Fuesse standen quer zur Sitzplatte, weil der Versatz in
  Welt-Z statt entlang der gedrehten Achse gerechnet wurde.
- Obere Ebene bis an das Kraeuterbeet eben auf -0.02. Zwischen Grillfeld und
  Kraeuterbeet stieg der Rasen um gut 30 cm an.
- Unter dem Spielhaus steht der Betonklotz aus dem Plan (graues Rechteck
  1.01 x 2.70 m); die offene Plattform daneben bleibt auf Stuetzen.
- Aus dem Gartenhaus wird ein Gartenschrank: 1.80 x 0.50 m, 2.00 m hoch, mit
  dem Ruecken in der Ecke und den Doppeltueren zum Garten.
- Neben dem Gelaender der Osttreppe lag Rasen; dort liegt jetzt derselbe
  Belag wie auf dem Weg, bis an den Treppenkopf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 08:01:39 +02:00
co-authored by Claude Opus 5
parent fad23a34c3
commit fefa8e8c9a
9 changed files with 329 additions and 69 deletions
+25 -8
View File
@@ -118,7 +118,10 @@ export function buildBuilding(b: Building): Group {
const list: Rect[] = b.openings
.filter((o) => o.face === face)
.map((o) => ({ u0: Math.min(o.a, o.b), u1: Math.max(o.a, o.b), y0: o.sill, y1: o.head }));
if (face === 'south') list.push({ u0: b.loggia.x0, u1: b.loggia.x1, y0: b.floorLevel - 0.4, y1: b.loggia.ceiling });
// Der gedeckte Sitzplatz ist nur gegen Westen offen - so zeigt es die
// Suedwestfassade ("Aussensitzplatz"). Gegen den Garten steht dort eine
// geschlossene Wand mit dem Lamellenband davor; ohne sie schaut man von
// aussen durch die Fassade in den Sitzplatz.
if (face === 'west') list.push({ u0: b.loggia.z0, u1: b.loggia.z1, y0: b.floorLevel - 0.4, y1: b.loggia.ceiling });
return list;
};
@@ -159,6 +162,11 @@ export function buildBuilding(b: Building): Group {
holzMat,
);
loggia.add(back);
const front = new Mesh(
boxGeometry((lg.x0 + lg.x1) / 2, lg.z1 - 0.08, lg.x1 - lg.x0, 0.16, b.floorLevel - 0.4, lg.ceiling, 0, holz.tile),
holzMat,
);
loggia.add(front);
const ceil = new PlaneGeometry(lg.x1 - lg.x0, lg.z1 - lg.z0);
ceil.rotateX(Math.PI / 2);
const ceilMesh = new Mesh(ceil, new MeshStandardMaterial({ color: 0xd8d4cc, roughness: 0.85 }));
@@ -232,14 +240,23 @@ export function buildBuilding(b: Building): Group {
group.add(roof);
// --- Vordach ueber dem Eingang -----------------------------------------
// --- Vordach ueber dem Hauszugang --------------------------------------
// Es kragt senkrecht zu seiner Fassade aus; a und b laufen in der
// Fassadenrichtung (Site-X bei Nord/Sued, Site-Z bei Ost/West).
const cp = b.canopy;
const canopy = new Mesh(
boxGeometry((cp.a + cp.b) / 2, z0 - cp.depth / 2, cp.b - cp.a, cp.depth, cp.y, cp.y + cp.thickness, 0, 2),
new MeshStandardMaterial({ color: 0x5a5148, roughness: 0.6 }),
);
canopy.name = 'Vordach Eingang';
canopy.castShadow = true;
group.add(canopy);
const dachMat = new MeshStandardMaterial({ color: 0x5a5148, roughness: 0.6 });
const mitte = (cp.a + cp.b) / 2, laenge = Math.abs(cp.b - cp.a);
let dach: Mesh;
if (cp.face === 'east' || cp.face === 'west') {
const xf = cp.face === 'east' ? x1 + cp.depth / 2 : x0 - cp.depth / 2;
dach = new Mesh(boxGeometry(xf, mitte, cp.depth, laenge, cp.y, cp.y + cp.thickness, 0, 2), dachMat);
} else {
const zf = cp.face === 'south' ? z1 + cp.depth / 2 : z0 - cp.depth / 2;
dach = new Mesh(boxGeometry(mitte, zf, laenge, cp.depth, cp.y, cp.y + cp.thickness, 0, 2), dachMat);
}
dach.name = 'Vordach Hauszugang';
dach.castShadow = true;
group.add(dach);
return group;
}
+75 -22
View File
@@ -5,7 +5,7 @@
*/
import {
BoxGeometry, BufferGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial,
PointLight, type MeshStandardMaterialParameters,
PointLight, SpotLight, type MeshStandardMaterialParameters,
} from 'three';
import type { Lamp, Linear, Prop, Stele } from '../site';
import { boxGeometry, mergeGeometries } from './geometry';
@@ -80,35 +80,61 @@ function schwebendeStufe(p: Prop, y: number): Group {
// Freistehende Objekte
// ---------------------------------------------------------------------------
/**
* Gartenschrank: schmaler Schrank mit Doppeltuer und leicht geneigtem Deckel.
* Der Plan reserviert an dieser Stelle 1.8 x 1.8 m fuer ein Gartenhaus; gebaut
* wird ein Schrank von 1.80 x 0.50 m, rund 2.00 m hoch, mit dem Ruecken in der
* Ecke und den Tueren zum Garten.
*/
function gartenhaus(p: Prop, y: number): Group {
const g = new Group();
const holz = material(tex.fassadeHolz(), { roughness: 0.8 });
const dunkel = new MeshStandardMaterial({ color: 0x3b3934, roughness: 0.62 });
const metall = new MeshStandardMaterial({ color: 0x55565a, roughness: 0.45, metalness: 0.6 });
const a = (p.rot * Math.PI) / 180;
// u laeuft entlang p.w, v entlang p.d - so wie in boxGeometry.
const ux = Math.cos(a), uz = Math.sin(a);
const vx = -Math.sin(a), vz = Math.cos(a);
const body = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y, y + p.h, p.rot, 1.2), holz);
body.castShadow = true; body.receiveShadow = true;
g.add(body);
// Deckel mit leichtem Ueberstand
const roof = new Mesh(
boxGeometry(p.x, p.z, p.w + 0.16, p.d + 0.16, y + p.h, y + p.h + 0.09, p.rot, 2),
boxGeometry(p.x + ux * 0.03, p.z + uz * 0.03, p.w + 0.12, p.d + 0.14, y + p.h, y + p.h + 0.06, p.rot, 2),
material(tex.beton(), { color: 0x6a6a66, roughness: 0.9 }),
);
roof.castShadow = true;
g.add(roof);
// Tuer
const door = new Mesh(
new BoxGeometry(0.78, 1.95, 0.04),
new MeshStandardMaterial({ color: 0x3b3934, roughness: 0.6 }),
);
door.position.set(p.x, y + 0.98, p.z - p.d / 2 - 0.02);
g.add(door);
// Zwei Tuerfluegel auf der Laengsseite, leicht vor der Schrankfront
const front = p.w / 2 + 0.015;
const parts: BufferGeometry[] = [];
for (const s of [-1, 1]) {
const off = s * (p.d / 4);
parts.push(boxGeometry(
p.x - ux * front + vx * off, p.z - uz * front + vz * off,
0.03, p.d / 2 - 0.03, y + 0.06, y + p.h - 0.06, p.rot, 1.2,
));
}
const doors = new Mesh(mergeGeometries(parts), dunkel);
doors.castShadow = true;
g.add(doors);
// Zwei schlanke Griffe
const griffe: BufferGeometry[] = [];
for (const s of [-1, 1]) {
const off = s * 0.06;
griffe.push(boxGeometry(
p.x - ux * (front + 0.035) + vx * off, p.z - uz * (front + 0.035) + vz * off,
0.025, 0.025, y + p.h * 0.45, y + p.h * 0.62, p.rot, 1,
));
}
g.add(new Mesh(mergeGeometries(griffe), metall));
return g;
}
/** Spielhaus durch Zimmermann: Podest, Bretterwaende, Gelaender, Pultdach. */
/**
* Spielhaus des Zimmermanns: ein Stelzenhaus. Der Plan zeigt ein Podest von
* 1.81 x 2.70 m mit der Kote 1.47 - also rund 0.9 m ueber dem Gelaende - und
* darauf an der Ostseite eine Huette von 1.01 x 2.70 m. Der westliche Teil
* des Podests bleibt offene Plattform mit Bruestung und Leiter.
*/
function spielhaus(p: Prop, y: number): Group {
const g = new Group();
const holz = material(tex.fassadeHolz(), { color: 0xd8b98c, roughness: 0.85 });
@@ -122,12 +148,20 @@ function spielhaus(p: Prop, y: number): Group {
const plattW = p.w - hutW; // offene Plattform im Westen
const plattX = p.x - hw + plattW / 2;
// Unter der Huette steht ein Betonklotz - im Plan das graue Rechteck von
// 1.01 x 2.70 m unter dem Podest. Die offene Plattform daneben steht auf
// zwei schlanken Stuetzen.
const klotz = new Mesh(
boxGeometry(hutX, p.z, hutW, p.d, y - 0.45, deck - 0.06, p.rot, 2),
material(tex.beton(), { color: 0x93918c, roughness: 0.92 }),
);
klotz.name = 'Betonsockel Spielhaus';
klotz.castShadow = true; klotz.receiveShadow = true;
g.add(klotz);
const trag: BufferGeometry[] = [];
// Vier Stuetzen, etwas eingerueckt
for (const dx of [-hw + 0.14, hw - 0.14]) {
for (const dz of [-hd + 0.14, hd - 0.14]) {
trag.push(boxGeometry(p.x + dx, p.z + dz, 0.11, 0.11, y - 0.35, deck - 0.06, 0, 1));
}
for (const dz of [-hd + 0.14, hd - 0.14]) {
trag.push(boxGeometry(p.x - hw + 0.14, p.z + dz, 0.11, 0.11, y - 0.35, deck - 0.06, 0, 1));
}
// Podestboden
trag.push(boxGeometry(p.x, p.z, p.w, p.d, deck - 0.06, deck, p.rot, 1.2));
@@ -242,9 +276,14 @@ function bank(p: Prop, y: number): Group {
const seat = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + p.h - 0.09, y + p.h, p.rot, belag.tile), stein);
seat.castShadow = true; seat.receiveShadow = true;
g.add(seat);
// Die Fuesse sitzen unter den Enden der Sitzplatte. Der Versatz muss der
// gedrehten v-Achse folgen - sonst stehen sie quer daneben.
const a = (p.rot * Math.PI) / 180;
const vx = -Math.sin(a), vz = Math.cos(a);
for (const s of [-1, 1]) {
const off = s * (p.d / 2 - 0.22);
const m = new Mesh(
boxGeometry(p.x, p.z + (s * (p.d / 2 - 0.22)), p.w - 0.08, 0.14, y, y + p.h - 0.09, p.rot, belag.tile),
boxGeometry(p.x + vx * off, p.z + vz * off, p.w - 0.08, 0.14, y, y + p.h - 0.09, p.rot, belag.tile),
stein,
);
m.castShadow = true;
@@ -381,6 +420,7 @@ export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => numb
color: 0xffd9a0, emissive: 0xffc26e, emissiveIntensity: 0, roughness: 0.4,
});
const pointLights: PointLight[] = [];
const spots: SpotLight[] = [];
for (const l of lamps) {
const y = height(l.x, l.z);
@@ -405,6 +445,18 @@ export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => numb
pl.position.set(l.x, y + (l.kind === 'poller' ? 0.6 : 0.1), l.z);
pointLights.push(pl);
g.add(pl);
if (l.kind === 'boden') {
// Bodenleuchten sind Strahler: sie leuchten die Travertinstelen und die
// Ziergraeser darueber von unten an. Ohne Schatten, sonst wird die
// Abendszene mit 11 Strahlern zu teuer.
const sp = new SpotLight(0xffd6a2, 0, 5.5, 0.55, 0.55, 1.4);
sp.position.set(l.x, y + 0.04, l.z);
sp.target.position.set(l.x, y + 3.0, l.z);
g.add(sp);
g.add(sp.target);
spots.push(sp);
}
group.add(g);
}
@@ -413,6 +465,7 @@ export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => numb
setOn(on: boolean) {
glowMat.emissiveIntensity = on ? 1.6 : 0;
for (const pl of pointLights) pl.intensity = on ? 2.2 : 0;
for (const sp of spots) sp.intensity = on ? 5.0 : 0;
},
};
}
+47 -13
View File
@@ -6,7 +6,7 @@
* statt Blattgeometrie - das bleibt auch auf dem Handy fluessig.
*/
import {
BoxGeometry, BufferAttribute, BufferGeometry, Color, CylinderGeometry, DoubleSide,
BufferAttribute, BufferGeometry, Color, CylinderGeometry, DoubleSide,
Group, IcosahedronGeometry, InstancedMesh, Matrix4, Mesh, MeshStandardMaterial,
Object3D, Quaternion, Vector3,
} from 'three';
@@ -67,12 +67,43 @@ function blob(
return g;
}
/**
* Ein Grashalm: schmales Band, das sich nach oben verjuengt und ueberhaengt.
* Fusspunkt im Ursprung, Hoehe rund 1.10 m - Lage und Neigung kommen aus der
* Instanzmatrix, damit die Normalen nicht verzerrt werden.
*/
function halmGeometry(): BufferGeometry {
const SEG = 6, H = 1.10, W = 0.026;
const pos: number[] = [];
const uv: number[] = [];
const pt = (i: number) => {
const f = i / SEG;
const y = H * f;
// Ueberhang: der Halm biegt sich mit dem Quadrat der Hoehe nach vorn.
const x = 0.42 * H * f * f;
const w = W * (1 - f * 0.92);
return { x, y, w };
};
for (let i = 0; i < SEG; i++) {
const a = pt(i), b2 = pt(i + 1);
pos.push(a.x, a.y, -a.w, a.x, a.y, a.w, b2.x, b2.y, b2.w);
pos.push(a.x, a.y, -a.w, b2.x, b2.y, b2.w, b2.x, b2.y, -b2.w);
const v0 = i / SEG, v1 = (i + 1) / SEG;
uv.push(0, v0, 1, v0, 1, v1, 0, v0, 1, v1, 0, v1);
}
const g = new BufferGeometry();
g.setAttribute('position', new BufferAttribute(new Float32Array(pos), 3));
g.setAttribute('uv', new BufferAttribute(new Float32Array(uv), 2));
g.computeVertexNormals();
return g;
}
const LAUB = {
parrotia: 0x76944f,
apfel: 0x7e9c52,
eibe: 0x466b3a,
strauch: 0x6b8f45,
gras: 0xa3b45c,
gras: 0x9aad5b,
kraut: 0x8ba455,
liguster: 0x557a37,
};
@@ -437,24 +468,27 @@ export function buildVegetation(
}), 'Beerensträucher');
if (strauchMesh) group.add(strauchMesh);
// --- Graeser: Buendel schmaler, leicht geneigter Halme -------------------
// --- Ziergraeser --------------------------------------------------------
// Im Plan stehen zwischen den Travertinstelen kleine gruene Punkte. Das
// sind keine Polster, sondern Graeser, die an den Stelen hochwachsen und
// abends von den Bodenleuchten angestrahlt werden. Ein Halm ist ein
// schmales, sich verjuengendes Band, das nach oben ueberhaengt.
const graeser = plants.filter((p) => p.type === 'gras' && p.x !== undefined);
if (graeser.length) {
// Ein Halm als Einheitsgeometrie mit Fusspunkt im Ursprung; Neigung und
// Position kommen aus der Instanzmatrix, nicht aus einer Mesh-Rotation.
const blade = new BoxGeometry(0.022, 1, 0.022);
blade.translate(0, 0.5, 0);
const mesh = new InstancedMesh(blade, foliage(LAUB.gras, 0.98), graeser.length * 9);
const halm = halmGeometry();
const mesh = new InstancedMesh(halm, foliage(LAUB.gras, 0.96, false), graeser.length * 16);
(mesh.material as MeshStandardMaterial).side = DoubleSide;
const dummy = new Object3D();
let k = 0;
for (const p of graeser) {
const y = height(p.x!, p.z!);
for (let i = 0; i < 9; i++) {
for (let i = 0; i < 16; i++) {
const a = r() * Math.PI * 2;
const rad = (p.r ?? 0.12) * r() * 0.9;
const rad = (p.r ?? 0.12) * r() * 0.8;
dummy.position.set(p.x! + Math.cos(a) * rad, y, p.z! + Math.sin(a) * rad);
dummy.rotation.set((r() - 0.5) * 0.45, r() * Math.PI, (r() - 0.5) * 0.45);
dummy.scale.set(1, 0.42 + r() * 0.4, 1);
dummy.rotation.set(0, a + (r() - 0.5) * 1.2, 0);
const s = 0.85 + r() * 0.55;
dummy.scale.set(0.75 + r() * 0.5, s, 1);
dummy.updateMatrix();
mesh.setMatrixAt(k++, dummy.matrix);
}
@@ -462,7 +496,7 @@ export function buildVegetation(
mesh.count = k;
mesh.instanceMatrix.needsUpdate = true;
mesh.castShadow = true;
mesh.name = 'Gräser';
mesh.name = 'Ziergräser';
group.add(mesh);
}
+1 -1
View File
@@ -147,7 +147,7 @@ export interface Building {
parapet: number;
outline: Vec2[];
loggia: { x0: number; z0: number; x1: number; z1: number; ceiling: number };
canopy: { a: number; b: number; depth: number; y: number; thickness: number };
canopy: { face: 'north' | 'south' | 'east' | 'west'; a: number; b: number; depth: number; y: number; thickness: number };
openings: Opening[];
}