Planabgleich Etappe A bis C: Zugaenge, Rasenrandplatten, Torstufen
Deploy App / deploy (push) Successful in 28s
Deploy App / deploy (push) Successful in 28s
Systematischer Abgleich Plan gegen Szene, Bereich fuer Bereich. Neu gebaut - Lineare Bauteile (LINEAR): Treppe und Rampe am Ost- und Westzugang, Stahlrohrgelaender laengs des Gefaelles, Entwaesserungsrinnen mit buendigem Rost. Geometrie aus den Trittlinien des Plans abgegriffen (Ost: 8 Trittlinien z -4.18 bis -6.52, Koten -0.37/-1.57; West: Trittlinien alle 20 cm ab x -21.94, Koten -0.28/-1.08). - Rasenrandplatten: das durchgehende Band von 6 cm Breite entlang der Rasenkante wird als hochkant stehende Randplatte mit Sichtkante aufgebaut, nicht mehr als Belag (neuer Typ randplatte, 1.2 m2). - Zwei schwebende Travertinstufen am Gartentor, Koten -0.13 und -0.32. - Winkelelemente entlang der Suedgrenze, Tegula-Streifen des Nachbarn. Korrigiert - Holzscheiterwand: der Plan zeigt zwei Felder 1.50 x 0.46 m, nicht eines. Farbe #ffaa00 war faelschlich als Tegula klassiert. - Gartentor: Fluegel laeuft ueber z, nicht ueber x - das Tor stand um 90 Grad verdreht. Jetzt Stabgitter mit Pfosten und senkrechten Staeben statt geschlossenem schwarzem Blatt. - Winkelelemente Ost auf x 3.735 (Plan: Doppellinie 3.69/3.78), Fussweg auf x 2.86 bis 3.69 zwischen Verbundstein-Anpassung und Winkelelementen. - Rinne beim Gartentor auf die Schraffur des Plans (z 2.295, x -23.34 bis -21.74). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+9
-1
@@ -13,7 +13,7 @@ import './style.css';
|
||||
import { loadSite, makeHeightSampler, type SiteData } from './site';
|
||||
import { buildTerrain } from './scene/terrain';
|
||||
import { buildSurfaces } from './scene/surfaces';
|
||||
import { buildStelen, buildProps, buildLamps } from './scene/structures';
|
||||
import { buildStelen, buildProps, buildLamps, buildLinear } from './scene/structures';
|
||||
import { buildWalls } from './scene/walls';
|
||||
import { buildVegetation } from './scene/vegetation';
|
||||
import { buildBuilding, buildNeighbourBuildings } from './scene/building';
|
||||
@@ -59,6 +59,7 @@ async function main() {
|
||||
scene.add(buildWalls(site.walls, height));
|
||||
scene.add(buildStelen(site.stelen, height));
|
||||
scene.add(buildProps(site.props, height));
|
||||
scene.add(buildLinear(site.linear, height));
|
||||
|
||||
await progress(66, 'Haus und Nachbarschaft …');
|
||||
scene.add(buildBuilding(site.building));
|
||||
@@ -104,6 +105,13 @@ async function main() {
|
||||
obstacles.push(obstacleFromSegment(w.path[i - 1], w.path[i], w.t + 0.1));
|
||||
}
|
||||
}
|
||||
// Gelaender begrenzen den Rundgang, Treppen und Rinnen bleiben begehbar.
|
||||
for (const f of site.linear) {
|
||||
if (f.kind !== 'gelaender') continue;
|
||||
for (let i = 1; i < f.path.length; i++) {
|
||||
obstacles.push(obstacleFromSegment(f.path[i - 1], f.path[i], 0.14));
|
||||
}
|
||||
}
|
||||
for (const p of site.props) {
|
||||
if (p.type === 'schacht' || p.type === 'wasserhahn') continue;
|
||||
obstacles.push({ cx: p.x, cz: p.z, hw: p.w / 2, hd: p.d / 2, rot: (p.rot * Math.PI) / 180 });
|
||||
|
||||
+169
-11
@@ -7,7 +7,7 @@ import {
|
||||
BoxGeometry, BufferGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial,
|
||||
PointLight, type MeshStandardMaterialParameters,
|
||||
} from 'three';
|
||||
import type { Lamp, Prop, Stele } from '../site';
|
||||
import type { Lamp, Linear, Prop, Stele } from '../site';
|
||||
import { boxGeometry, mergeGeometries } from './geometry';
|
||||
import * as tex from './textures';
|
||||
|
||||
@@ -250,19 +250,40 @@ function schachtdeckel(p: Prop, y: number): Group {
|
||||
return g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gartentor: Stabgitterfluegel aus Stahl. Zwei Pfosten, umlaufender Rahmen aus
|
||||
* Flachstahl und senkrechte Staebe im Abstand von rund 11 cm - das Tor bleibt
|
||||
* durchsichtig, es ist kein geschlossenes Blatt.
|
||||
*/
|
||||
function gartentor(p: Prop, y: number): Group {
|
||||
const g = new Group();
|
||||
const mat = new MeshStandardMaterial({ color: 0x4a4a4c, roughness: 0.5, metalness: 0.4 });
|
||||
const frame = new Mesh(boxGeometry(p.x, p.z, p.w, 0.05, y, y + p.h, p.rot, 1), mat);
|
||||
g.add(frame);
|
||||
for (let i = 1; i < 6; i++) {
|
||||
const bar = new Mesh(
|
||||
boxGeometry(p.x - p.w / 2 + (i * p.w) / 6, p.z, 0.03, 0.03, y, y + p.h, p.rot, 1),
|
||||
mat,
|
||||
);
|
||||
g.add(bar);
|
||||
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));
|
||||
}
|
||||
g.children.forEach((c) => { (c as Mesh).castShadow = true; });
|
||||
// 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));
|
||||
}
|
||||
// Senkrechte Staebe
|
||||
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));
|
||||
}
|
||||
const m = new Mesh(mergeGeometries(parts), mat);
|
||||
m.castShadow = true;
|
||||
g.add(m);
|
||||
return g;
|
||||
}
|
||||
|
||||
@@ -344,3 +365,140 @@ export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => numb
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lineare Bauteile: Treppen mit Rampe, Gelaender, Entwaesserungsrinnen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Winkel fuer boxGeometry, so dass die lokale v-Achse (Tiefe d) entlang der
|
||||
* Richtung (dx, dz) liegt. Aus world = (cx + u*cos - v*sin, y, cz + u*sin + v*cos)
|
||||
* folgt v-Achse = (-sin, cos), also sin = -dx und cos = dz.
|
||||
*/
|
||||
function axisAngle(dx: number, dz: number): number {
|
||||
return (Math.atan2(-dx, dz) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blockstufentreppe. Der Pfad laeuft von oben nach unten; jede Stufe ist ein
|
||||
* Travertinblock, dessen Trittflaeche eine Steigung tiefer liegt als die
|
||||
* vorige. Die Bloecke reichen bis unter das untere Niveau, damit die Treppe
|
||||
* auch von der Seite geschlossen wirkt.
|
||||
*/
|
||||
function blockTreppe(f: Linear): Group {
|
||||
const g = new Group();
|
||||
const belag = tex.travertinSlab();
|
||||
const mat = material(belag, { roughness: 0.6 });
|
||||
const [[x0, z0], [x1, z1]] = [f.path[0], f.path[f.path.length - 1]];
|
||||
const dx = x1 - x0, dz = z1 - z0;
|
||||
const len = Math.hypot(dx, dz) || 1;
|
||||
const rot = axisAngle(dx / len, dz / len);
|
||||
const steps = f.steps ?? 4;
|
||||
const width = f.width ?? 1.2;
|
||||
const fromY = f.fromY ?? 0, toY = f.toY ?? -1;
|
||||
const riser = (fromY - toY) / steps;
|
||||
const tread = len / steps;
|
||||
const parts: BufferGeometry[] = [];
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const t = (i + 0.5) / steps;
|
||||
const top = fromY - (i + 1) * riser;
|
||||
parts.push(boxGeometry(
|
||||
x0 + dx * t, z0 + dz * t, width, tread + 0.01,
|
||||
toY - 0.45, top, rot, belag.tile,
|
||||
));
|
||||
}
|
||||
const m = new Mesh(mergeGeometries(parts), mat);
|
||||
m.castShadow = true;
|
||||
m.receiveShadow = true;
|
||||
g.add(m);
|
||||
return g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gelaender aus Stahlrohr: Pfosten alle rund 1.1 m, Handlauf und Knieholm
|
||||
* folgen dem Gefaelle des Laufs.
|
||||
*/
|
||||
function gelaender(f: Linear): Group {
|
||||
const g = new Group();
|
||||
const mat = new MeshStandardMaterial({ color: 0x4c4f52, roughness: 0.42, metalness: 0.65 });
|
||||
const [[x0, z0], [x1, z1]] = [f.path[0], f.path[f.path.length - 1]];
|
||||
const dx = x1 - x0, dz = z1 - z0;
|
||||
const len = Math.hypot(dx, dz) || 1;
|
||||
const rot = axisAngle(dx / len, dz / len);
|
||||
const fromY = f.fromY ?? 0, toY = f.toY ?? -1;
|
||||
const h = f.height ?? 0.95;
|
||||
const parts: BufferGeometry[] = [];
|
||||
const n = Math.max(2, Math.round(len / 1.1) + 1);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = i / (n - 1);
|
||||
const y = fromY + (toY - fromY) * t;
|
||||
parts.push(boxGeometry(x0 + dx * t, z0 + dz * t, 0.05, 0.05, y - 0.35, y + h, rot, 1));
|
||||
}
|
||||
// Holme: als flache Bloecke ueber die ganze Laenge, um die Neigung gelegt.
|
||||
for (const [level, thick] of [[h, 0.055], [h * 0.52, 0.04]] as const) {
|
||||
const yA = fromY + level, yB = toY + level;
|
||||
const rise = yA - yB;
|
||||
const holmLen = Math.hypot(len, rise);
|
||||
const geo = boxGeometry(0, 0, thick, holmLen, -thick / 2, thick / 2, 0, 1);
|
||||
geo.rotateX(Math.atan2(rise, len));
|
||||
geo.rotateY((rot * Math.PI) / 180);
|
||||
geo.translate((x0 + x1) / 2, (yA + yB) / 2, (z0 + z1) / 2);
|
||||
parts.push(geo);
|
||||
}
|
||||
const m = new Mesh(mergeGeometries(parts), mat);
|
||||
m.castShadow = true;
|
||||
g.add(m);
|
||||
return g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entwaesserungsrinne: schmaler Kanal im Belag mit buendigem Stahlrost.
|
||||
*/
|
||||
function rinne(f: Linear, height: (x: number, z: number) => number): Group {
|
||||
const g = new Group();
|
||||
const beton = new MeshStandardMaterial({ color: 0x8e8a84, roughness: 0.85 });
|
||||
const rost = new MeshStandardMaterial({ color: 0x6a6d70, roughness: 0.4, metalness: 0.7 });
|
||||
const w = f.width ?? 0.15;
|
||||
for (let i = 1; i < f.path.length; i++) {
|
||||
const [ax, az] = f.path[i - 1], [bx, bz] = f.path[i];
|
||||
const dx = bx - ax, dz = bz - az;
|
||||
const len = Math.hypot(dx, dz) || 1;
|
||||
const rot = axisAngle(dx / len, dz / len);
|
||||
const cx = (ax + bx) / 2, cz = (az + bz) / 2;
|
||||
const y = height(cx, cz);
|
||||
const kasten = new Mesh(boxGeometry(cx, cz, w + 0.09, len, y - 0.30, y - 0.015, rot, 1), beton);
|
||||
kasten.receiveShadow = true;
|
||||
g.add(kasten);
|
||||
// Rost in Segmenten von 50 cm, damit die Stossfugen sichtbar bleiben.
|
||||
const seg = Math.max(1, Math.round(len / 0.5));
|
||||
for (let s = 0; s < seg; s++) {
|
||||
const t = (s + 0.5) / seg;
|
||||
const r = new Mesh(
|
||||
boxGeometry(ax + dx * t, az + dz * t, w, len / seg - 0.012, y - 0.035, y + 0.004, rot, 1),
|
||||
rost,
|
||||
);
|
||||
r.receiveShadow = true;
|
||||
g.add(r);
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
/** Baut Treppen, Gelaender und Rinnen aus dem Plan. */
|
||||
export function buildLinear(features: Linear[], height: (x: number, z: number) => number): Group {
|
||||
const group = new Group();
|
||||
group.name = 'Treppen, Gelaender und Rinnen';
|
||||
for (const f of features) {
|
||||
let obj: Group;
|
||||
switch (f.kind) {
|
||||
case 'treppe': obj = blockTreppe(f); break;
|
||||
case 'gelaender': obj = gelaender(f); break;
|
||||
case 'rinne': obj = rinne(f, height); break;
|
||||
default: continue;
|
||||
}
|
||||
obj.name = f.id;
|
||||
obj.userData.note = f.note;
|
||||
group.add(obj);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ interface BelagDef {
|
||||
edging?: boolean;
|
||||
/** Material der Sichtkante, falls abweichend. */
|
||||
edge?: () => tex.Belag;
|
||||
/**
|
||||
* Randplatte: folgt dem Gelaende wie eine gewachsene Flaeche, steht aber
|
||||
* mit einer sichtbaren Kante darueber. Fuer die Rasenrandplatten.
|
||||
*/
|
||||
kerb?: boolean;
|
||||
}
|
||||
|
||||
const BELAG: Record<SurfaceType, BelagDef> = {
|
||||
@@ -45,6 +50,7 @@ const BELAG: Record<SurfaceType, BelagDef> = {
|
||||
rasen: { belag: tex.rasen, label: 'Rasen', lift: 0.012, built: false },
|
||||
schotterwiese: { belag: tex.schotterwiese, label: 'Schotterwiese', lift: 0.010, built: false },
|
||||
beet: { belag: tex.beet, label: 'Pflanzbeet', lift: 0.008, built: false, edging: true },
|
||||
randplatte: { belag: tex.travertinMauer, label: 'Rasenrandplatten', lift: 0.045, built: false, kerb: true },
|
||||
};
|
||||
|
||||
const STAHL = () => new MeshStandardMaterial({ color: 0x8a877f, roughness: 0.55, metalness: 0.62 });
|
||||
@@ -100,6 +106,14 @@ export function buildSurfaces(
|
||||
tops.push(surfaceGeometryMulti([s.polygon], height, {
|
||||
offset: def.lift, tile: belag.tile, maxEdge: 0.28,
|
||||
}));
|
||||
if (def.kerb) {
|
||||
sides.push(surfaceEdgeGeometry(
|
||||
s.polygon,
|
||||
(x, z) => height(x, z) + def.lift,
|
||||
(x, z) => height(x, z) - 0.22,
|
||||
belag.tile,
|
||||
));
|
||||
}
|
||||
if (def.edging) {
|
||||
edgingParts.push(edgingGeometry(s.polygon, (x, z) => height(x, z) + def.lift));
|
||||
}
|
||||
|
||||
+23
-1
@@ -11,7 +11,8 @@ export type Vec2 = [number, number];
|
||||
|
||||
export type SurfaceType =
|
||||
| 'travertin80' | 'dekorkies' | 'travertinstell' | 'keramik'
|
||||
| 'verbundstein' | 'tegula' | 'rasen' | 'schotterwiese' | 'beet' | 'spielsand';
|
||||
| 'verbundstein' | 'tegula' | 'rasen' | 'schotterwiese' | 'beet' | 'spielsand'
|
||||
| 'randplatte';
|
||||
|
||||
export interface Surface {
|
||||
type: SurfaceType;
|
||||
@@ -105,6 +106,26 @@ export interface Stele {
|
||||
side: 'sued' | 'nord' | 'west';
|
||||
}
|
||||
|
||||
/**
|
||||
* Lineares Bauteil entlang einer Achse: Treppe mit Rampe, Gelaender oder
|
||||
* Entwaesserungsrinne. Der Plan zeichnet diese nur als Linienbuendel.
|
||||
*/
|
||||
export interface Linear {
|
||||
id: string;
|
||||
kind: 'treppe' | 'gelaender' | 'rinne';
|
||||
note?: string;
|
||||
path: Vec2[];
|
||||
/** Breite quer zur Achse (Treppe, Rinne). */
|
||||
width?: number;
|
||||
/** Hoehe an Anfang und Ende der Achse. */
|
||||
fromY?: number;
|
||||
toY?: number;
|
||||
/** Anzahl Blockstufen. */
|
||||
steps?: number;
|
||||
/** Gelaenderhoehe ueber dem Lauf. */
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface Opening {
|
||||
face: 'north' | 'south' | 'east' | 'west';
|
||||
a: number;
|
||||
@@ -174,6 +195,7 @@ export interface SiteData {
|
||||
lamps: Lamp[];
|
||||
walls: Wall[];
|
||||
props: Prop[];
|
||||
linear: Linear[];
|
||||
stelen: Stele[];
|
||||
building: Building;
|
||||
neighbourBuildings: NeighbourBuilding[];
|
||||
|
||||
+7
-1
@@ -90,6 +90,7 @@ export function setupHud(opts: HudOptions): Hud {
|
||||
travertin80: 'Travertinplatten 80×80×3 cm',
|
||||
dekorkies: 'Dekorkies',
|
||||
travertinstell: 'Travertinstellplatten',
|
||||
randplatte: 'Rasenrandplatten',
|
||||
keramik: 'Keramikplatten 60×60×2 cm',
|
||||
verbundstein: 'Verbundsteine (Nachbar)',
|
||||
tegula: 'Tegula-Pflastersteine grau',
|
||||
@@ -140,7 +141,12 @@ export function setupHud(opts: HudOptions): Hud {
|
||||
<li>${site.walls.length} Mauern und Wände</li>
|
||||
<li>${site.lamps.length} Leuchten – sie schalten sich abends automatisch ein</li>
|
||||
<li>${site.slabs.length} lose Travertinplatten 40 × 60 × 3 cm im Dekorkies</li>
|
||||
<li>Gartenhaus 1,8 × 1,8 m, Spielhaus, Flammengrill mit Holzscheiterwand</li>
|
||||
<li>Rasenrandplatten als 6 cm schmale Stellplatten entlang der Rasenkante</li>
|
||||
<li>Gartenhaus 1,8 × 1,8 m, Spielhaus, Flammengrill mit zwei Holzscheiterwänden
|
||||
(je 150 × 200 cm)</li>
|
||||
<li>Zwei schwebende Travertinstufen am Gartentor, Stabgittertor 100 cm</li>
|
||||
<li>${site.linear.filter((l) => l.kind === 'treppe').length} Treppen mit Rampe
|
||||
(Ost- und Westzugang), Geländer und Entwässerungsrinnen</li>
|
||||
</ul>
|
||||
|
||||
<h3>Bedienung</h3>
|
||||
|
||||
Reference in New Issue
Block a user