Nordgrenze zum Nachbarn, Torbogen, einfacher Rundgang
Deploy App / deploy (push) Successful in 27s

Nordgrenze
- Sitzmauer und Travertinmauer waren vertauscht. Der Plan zeichnet aussen,
  auf der Terrasse der Nachbarn, die "Sitzmauer" (0.55 tief, rund 40 cm
  hoch: Terrasse -1.77, Krone -1.35) und direkt dahinter die
  "Travertinmauer", die jetzt bis auf das Niveau der Nachbarn hinunter-
  reicht - sie sehen Naturstein statt Beton.
- Terrasse der Nachbarn fest auf -1.77.
- Schotterwiese (Flaechen 4 und 5) eben: quer kein Hang mehr, laengs folgt
  sie der Feuerwehrzufahrt. Unter der Ligusterhecke fassen Winkelelemente
  den Garten der Nachbarn, so wie der Plan sie dort beschriftet. Wiesen mit
  fester Ebene werden jetzt plan gerendert statt dem Raster zu folgen.
- Eingang fuer die Nachbarn: Travertinstufe in der Luecke zwischen Hecke
  und Mauer ("Stufe" im Plan), darueber ein Torbogen aus Cortenstahl mit
  Kletterrose. Kein Tor.

Rundgang
- Klick auf den Boden laeuft dorthin, Ziehen dreht den Blick. Kein
  Mausfang mehr.
- Weiches Anfahren und Bremsen, gedaempftes Nachfuehren bei Stufen statt
  Fall-und-Aufprall, Rutschen an Hindernissen.
- Travertinmauern mit fester Krone sind begehbar; man steigt ueber die
  Sitzmauer vom Sitzplatz auf den Rasen, statt springen zu muessen.
- Gartentor oeffnet sich beim Naehertreten und schliesst hinter einem, E
  uebersteuert. Schwelle unter dem Tor schliesst die Luecke zum Podest.
- Blickrichtung blieb beim Wechsel Uebersicht/Rundgang nicht erhalten
  (Vorzeichenfehler in yawDegrees).
- mergeGeometries loest indizierte Geometrien vorher auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 09:50:44 +02:00
co-authored by Claude Opus 5
parent 866bedf64d
commit 1ac99b1660
11 changed files with 469 additions and 112 deletions
+125 -19
View File
@@ -1,11 +1,12 @@
/**
* Freistehende Bauteile: Travertinstelen, Gartenhaus, Spielhaus,
* Flammengrill, Baenkli, Maehroboter-Garage, Gartentor und die Leuchten.
* Flammengrill, Baenkli, Maehroboter-Garage, Gartentor, Torbogen und die Leuchten.
* Die Mauern stehen in walls.ts.
*/
import {
BoxGeometry, BufferGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial,
PointLight, SpotLight, type MeshStandardMaterialParameters,
BoxGeometry, BufferGeometry, CatmullRomCurve3, CylinderGeometry, Group, IcosahedronGeometry,
Mesh, MeshStandardMaterial, PointLight, SpotLight, TubeGeometry, Vector3,
type MeshStandardMaterialParameters,
} from 'three';
import type { Lamp, Linear, Prop, Stele } from '../site';
import { boxGeometry, mergeGeometries } from './geometry';
@@ -346,35 +347,139 @@ function schachtdeckel(p: Prop, y: number): Group {
* Flachstahl und senkrechte Staebe im Abstand von rund 11 cm - das Tor bleibt
* durchsichtig, es ist kein geschlossenes Blatt.
*/
/**
* Gartentor mit drehbarem Fluegel. Die Pfosten stehen fest, der Fluegel haengt
* in einer eigenen Gruppe am Scharnier - so kann der Rundgang ihn oeffnen und
* schliessen. Er schlaegt nach aussen auf das Podest auf, weil innen gleich
* die beiden Stufen liegen.
*/
function gartentor(p: Prop, y: number): Group {
const g = new Group();
const mat = new MeshStandardMaterial({ color: 0x43464a, roughness: 0.45, metalness: 0.55 });
const a = (p.rot * Math.PI) / 180;
// Laengsrichtung des Fluegels in der Ebene, quer dazu die Blattdicke.
const ux = Math.cos(a), uz = Math.sin(a);
const at = (t: number): [number, number] => [p.x + ux * t, p.z + uz * t];
const parts: BufferGeometry[] = [];
const half = p.w / 2;
const top = y + p.h;
// Pfosten links und rechts, etwas hoeher als der Fluegel
for (const t of [-half - 0.05, half + 0.05]) {
const [x, z] = at(t);
parts.push(boxGeometry(x, z, 0.08, 0.08, y - 0.35, top + 0.06, p.rot, 1));
const pfosten: BufferGeometry[] = [];
for (const s of [-half - 0.05, half + 0.05]) {
pfosten.push(boxGeometry(p.x + ux * s, p.z + uz * s, 0.08, 0.08, y - 0.35, top + 0.06, p.rot, 1));
}
// Rahmen: unterer und oberer Riegel
for (const [y0, y1] of [[y + 0.08, y + 0.14], [top - 0.08, top]] as const) {
parts.push(boxGeometry(p.x, p.z, p.w, 0.035, y0, y1, p.rot, 1));
const posts = new Mesh(mergeGeometries(pfosten), mat);
posts.castShadow = true;
g.add(posts);
// Fluegel in Scharnier-Koordinaten: er laeuft vom Scharnier aus in lokal -X.
const teile: BufferGeometry[] = [];
for (const [y0, y1] of [[0.08, 0.14], [p.h - 0.08, p.h]] as const) {
teile.push(boxGeometry(-half, 0, p.w - 0.02, 0.035, y0, y1, 0, 1));
}
// Senkrechte Staebe
for (const x of [-0.03, -p.w + 0.03]) teile.push(boxGeometry(x, 0, 0.04, 0.04, 0.08, p.h, 0, 1));
const n = Math.max(4, Math.round(p.w / 0.11));
for (let i = 0; i <= n; i++) {
const [x, z] = at(-half + (i * p.w) / n);
parts.push(boxGeometry(x, z, 0.016, 0.016, y + 0.08, top, p.rot, 1));
for (let i = 1; i < n; i++) {
teile.push(boxGeometry(-(i * p.w) / n, 0, 0.016, 0.016, 0.08, p.h, 0, 1));
}
const m = new Mesh(mergeGeometries(parts), mat);
m.castShadow = true;
g.add(m);
const leaf = new Mesh(mergeGeometries(teile), mat);
leaf.castShadow = true;
// Scharnier am +u-Ende. Lokal -X zeigt im geschlossenen Zustand nach -u:
// (-cos th, sin th) = (-ux, -uz) => th = atan2(-uz, ux).
const pivot = new Group();
pivot.name = 'Torfluegel';
pivot.position.set(p.x + ux * half, y, p.z + uz * half);
const zu = Math.atan2(-uz, ux);
pivot.rotation.y = zu;
pivot.add(leaf);
g.add(pivot);
g.userData.fluegel = pivot;
g.userData.zu = zu;
g.userData.offen = zu + 1.65;
return g;
}
/**
* Torbogen fuer den Eingang der Nachbarn: zwei Baender aus Cortenstahl, die
* als Pfosten aufsteigen und oben einen Halbkreis schlagen, dazwischen
* Querstaebe. An einer Seite rankt eine Kletterrose hoch. Kein Tor - der
* Durchgang bleibt offen.
*/
function torbogen(p: Prop, y: number): Group {
const g = new Group();
const corten = new MeshStandardMaterial({ color: 0x8b4a27, roughness: 0.78, metalness: 0.25 });
const a = (p.rot * Math.PI) / 180;
// u: quer zum Durchgang (Spannweite), v: laengs des Durchgangs (Tiefe)
const ux = Math.cos(a), uz = Math.sin(a);
const vx = -Math.sin(a), vz = Math.cos(a);
const r = p.w / 2;
const P = (u: number, yy: number, v: number) =>
new Vector3(p.x + ux * u + vx * v, yy, p.z + uz * u + vz * v);
const baender: BufferGeometry[] = [];
for (const v of [-p.d / 2, p.d / 2]) {
const pts: Vector3[] = [P(-r, y - 0.2, v), P(-r, y + p.h * 0.5, v), P(-r, y + p.h, v)];
for (let i = 1; i < 16; i++) {
const w = Math.PI - (i / 16) * Math.PI;
pts.push(P(Math.cos(w) * r, y + p.h + Math.sin(w) * r, v));
}
pts.push(P(r, y + p.h, v), P(r, y + p.h * 0.5, v), P(r, y - 0.2, v));
const kurve = new CatmullRomCurve3(pts, false, 'centripetal');
baender.push(new TubeGeometry(kurve, 96, 0.026, 6, false));
}
// Querstaebe laengs des Bogens
for (let i = 0; i <= 8; i++) {
const w = Math.PI - (i / 8) * Math.PI;
const u = Math.cos(w) * r, yy = y + p.h + Math.sin(w) * r;
const kurve = new CatmullRomCurve3([P(u, yy, -p.d / 2), P(u, yy, p.d / 2)]);
baender.push(new TubeGeometry(kurve, 2, 0.012, 5, false));
}
for (const u of [-r, r]) {
for (const yy of [y + 0.45, y + 0.95, y + 1.45]) {
const kurve = new CatmullRomCurve3([P(u, yy, -p.d / 2), P(u, yy, p.d / 2)]);
baender.push(new TubeGeometry(kurve, 2, 0.012, 5, false));
}
}
const bogen = new Mesh(mergeGeometries(baender), corten);
bogen.castShadow = true;
g.add(bogen);
// Kletterrose an einer Seite: viele kleine, unregelmaessige Laubbueschel den
// Pfosten hinauf und ein Stueck ueber den Bogen, dazwischen einzelne Blueten.
const laub = new MeshStandardMaterial({ color: 0x35592a, roughness: 0.92, flatShading: true });
const bluete = new MeshStandardMaterial({ color: 0xd47a8e, roughness: 0.7 });
const laubGeo = new IcosahedronGeometry(1, 0);
let seed = 7;
const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
const ranke: [number, number][] = [];
for (let i = 0; i < 14; i++) ranke.push([-r, y + 0.05 + (i / 13) * p.h]);
for (let i = 1; i <= 7; i++) {
const w = Math.PI - (i / 16) * Math.PI;
ranke.push([Math.cos(w) * r, y + p.h + Math.sin(w) * r]);
}
const blaetter: BufferGeometry[] = [];
const blueten: BufferGeometry[] = [];
for (const [u, yy] of ranke) {
const n = 3 + Math.floor(rnd() * 3);
for (let k = 0; k < n; k++) {
const geo = laubGeo.clone();
geo.scale(0.05 + rnd() * 0.05, 0.04 + rnd() * 0.04, 0.05 + rnd() * 0.05);
geo.rotateY(rnd() * 6.28);
geo.rotateX((rnd() - 0.5) * 0.8);
const q = P(u + (rnd() - 0.5) * 0.16, yy + (rnd() - 0.5) * 0.14, (rnd() - 0.5) * p.d * 1.1);
geo.translate(q.x, q.y, q.z);
blaetter.push(geo);
if (rnd() < 0.22) {
const bg = laubGeo.clone();
bg.scale(0.028, 0.024, 0.028);
bg.translate(q.x + (rnd() - 0.5) * 0.06, q.y + 0.04, q.z + (rnd() - 0.5) * 0.06);
blueten.push(bg);
}
}
}
const rose = new Mesh(mergeGeometries(blaetter), laub);
rose.castShadow = true;
g.add(rose);
if (blueten.length) g.add(new Mesh(mergeGeometries(blueten), bluete));
return g;
}
@@ -390,6 +495,7 @@ export function buildProps(props: Prop[], height: (x: number, z: number) => numb
case 'flammengrill': obj = flammengrill(p, y); break;
case 'bank': obj = bank(p, y); break;
case 'tor': obj = gartentor(p, y); break;
case 'torbogen': obj = torbogen(p, y); break;
case 'stufe': obj = schwebendeStufe(p, y); break;
case 'roboter': obj = roboterGarage(p, y); break;
case 'wasserhahn': obj = wasserhahn(p, y); break;