Wege, Ebenen, Stufen und ein Sprung
Deploy App / deploy (push) Successful in 28s

Rueckmeldung Punkt fuer Punkt:

Weg rund ums Haus
- Der Ostweg ist jetzt durchgehend gepflastert von der Verbundstein-
  Anpassung (x 2.86) bis zur Grenze (x 4.45) - vorher 0.83 m breit mit
  Rasenstreifen daneben. Der Weg an der Suedgrenze reicht von der Grenze
  bis an die Nachbarmauer (z 4.49 bis 5.97).
- Winkelelemente Ost auf die Grenzlinie x 4.45; die Linie bei z 4.96 im
  Sueden war keine eigene Stuetzwand, das ist die Nachbarmauer selbst.

Hoehen und Gefaelle
- LEVELS koennen jetzt auch eine geneigte Ebene vorgeben. Die Wege sind
  Rampen: Ostweg rund 12%, Suedweg rund 3%, Feuerwehrzufahrt 13.6% aus
  den Plankoten -2.08 und -1.08. Der Architekturplan nennt an denselben
  Stellen "Steigung 9.9%" und "Steigung 13.0%".
- Ausserhalb der Zonen wird die Interpolation der naechsten Zone
  fortgesetzt statt ihres konstanten Ersatzwerts - die Feuerwehrzufahrt
  stand vorher als schwebende Platte ueber einer Gelaendekante.
- Gelaende oestlich der Grenze korrigiert: dort lag ein Huegel ueber dem
  Weg, obwohl der Plan -0.72 und -1.57 angibt.

Obere Ebene
- Der Rasen ist eben auf -0.02. Die Koten 0.09 bis 0.26 liegen schon im
  angehobenen Kiesbett des Sichtschutzes, nicht mehr im Rasen; ohne feste
  Vorgabe zog die Interpolation den Rasen huegelig hoch.

Stufen
- Beim gedeckten Sitzplatz stimmen jetzt beide schwebenden Platten: der
  Plan zeichnet zwei versetzte Rechtecke mit den Koten -0.02 und -0.17,
  bisher lagen sie auf -0.17 und -0.31 - die untere verschwand im Belag.
- Das Gartentor steht auf dem oberen Niveau (-0.13), von dort zwei Stufen
  hinunter auf den Hauptsitzplatz.
- Westtreppe ueber den ganzen Treppenblock (5 Stufen statt 4), die
  Gelaenderpfosten reichen bis ins Gelaende statt in der Luft zu enden.

Rundgang
- Treppen und schwebende Stufen sind begehbar: ihre Trittflaechen gehen in
  die Hoehenabfrage ein, die Stufen sind kein Hindernis mehr.
- Springen mit der Leertaste, rund 55 cm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 19:49:46 +02:00
co-authored by Claude Opus 5
parent ce40d6275b
commit fad23a34c3
10 changed files with 215 additions and 51 deletions
+27
View File
@@ -168,6 +168,33 @@ als auskragende Platten mit Schattenfuge, der Flammengrill als anthrazitfarbener
Stahlkubus mit Holzfach und runder Feueröffnung. Die Bilder lassen sich mit Stahlkubus mit Holzfach und runder Feueröffnung. Die Bilder lassen sich mit
`node tools/plan-bilder.mjs` aus dem PDF ziehen. `node tools/plan-bilder.mjs` aus dem PDF ziehen.
### Ebene Ebenen, geneigte Wege
`LEVELS` in `tools/authoring.mjs` gibt einzelnen Bereichen ihre Höhe fest vor,
statt sie aus streuenden Koten zu schätzen. Zwei Formen:
- **`level` + `flatten`** – waagrecht und zwingend. Die obere Ebene mit Rasen
und Kiesfeld liegt eben auf −0.02. Ohne diese Vorgabe zieht die Interpolation
den Rasen hügelig hoch, weil die Koten 0.09 bis 0.26 schon im angehobenen
Kiesbett des Sichtschutzes liegen und nicht mehr im Rasen.
- **`plane`** – eine geneigte Ebene, für die Wege. Sie sind Rampen, keine
waagrechten Felder: Ostweg rund 12 %, Weg an der Südgrenze rund 3 %,
Feuerwehrzufahrt rund 13.6 % (Plankoten −2.08 und −1.08). Der
Architekturplan (`Erdgeschoss mit Umgebung 1:100`) beschriftet dieselben
Gefälle mit „Steigung 9.9 %“ und „Steigung 13.0 %“.
Ausserhalb aller Zonen wird die Interpolation der nächstgelegenen Zone
fortgesetzt, nicht ihr konstanter Ersatzwert – sonst steht der Belag als
schwebende Platte über einer Geländekante.
### Treppen begehen und springen
Stufen und Treppen stecken nicht im Höhenraster, sondern sind eigene Körper.
Damit man sie begehen kann, rechnet `main.ts` ihre Trittflächen in die
Höhenabfrage des Rundgangs ein (`treppeTritte` in `src/scene/structures.ts`);
die schwebenden Stufen sind entsprechend **kein** Hindernis mehr. Dazu kommt
der Sprung mit der Leertaste – rund 55 cm, genug für jede Mauerkrone.
### Warum Zonen statt eines einzigen Geländes ### Warum Zonen statt eines einzigen Geländes
Das Grundstück fällt über rund 3.5 m ab, die Sprünge liegen aber nicht im Das Grundstück fällt über rund 3.5 m ab, die Sprünge liegen aber nicht im
+1 -1
View File
@@ -43,7 +43,7 @@
<div id="crosshair"></div> <div id="crosshair"></div>
<div id="hint"> <div id="hint">
<span class="desktop">Klicken zum Umsehen &middot; <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> gehen &middot; <kbd>Shift</kbd> schneller &middot; <kbd>Esc</kbd> Maus freigeben</span> <span class="desktop">Klicken zum Umsehen &middot; <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> gehen &middot; <kbd>Shift</kbd> schneller &middot; <kbd>Leertaste</kbd> springen &middot; <kbd>Esc</kbd> Maus freigeben</span>
<span class="touch">Links ziehen zum Gehen &middot; rechts wischen zum Umsehen</span> <span class="touch">Links ziehen zum Gehen &middot; rechts wischen zum Umsehen</span>
</div> </div>
File diff suppressed because one or more lines are too long
+20 -6
View File
@@ -1,7 +1,7 @@
/** /**
* Begehen des Gartens: Maus/Tastatur am Desktop, Joystick und Wischen am Handy. * Begehen des Gartens: Maus/Tastatur am Desktop, Joystick und Wischen am Handy.
* Der Spieler folgt dem Gelaende, kann Stufen bis 45 cm ersteigen und wird * Der Spieler folgt dem Gelaende, kann Stufen bis 45 cm ersteigen, mit der
* von Mauern, Gebaeuden und Objekten aufgehalten. * Leertaste springen und wird von Mauern, Gebaeuden und Objekten aufgehalten.
*/ */
import { Euler, PerspectiveCamera, Vector3 } from 'three'; import { Euler, PerspectiveCamera, Vector3 } from 'three';
@@ -28,6 +28,9 @@ export interface WalkerOptions {
const SPEED_WALK = 1.65; const SPEED_WALK = 1.65;
const SPEED_RUN = 3.6; const SPEED_RUN = 3.6;
/** Absprunggeschwindigkeit: reicht fuer rund 55 cm Sprunghoehe. */
const JUMP = 3.3;
const GRAVITY = 9.81;
export class Walker { export class Walker {
readonly camera: PerspectiveCamera; readonly camera: PerspectiveCamera;
@@ -155,13 +158,24 @@ export class Walker {
this.move(dx, dz); this.move(dx, dz);
// Springen: nur vom Boden weg, danach uebernimmt die Schwerkraft.
if (this.keys.has('Space') && this.grounded) {
this.velocityY = JUMP;
this.grounded = false;
}
// Vertikal: dem Gelaende folgen, Absaetze weich abfangen // Vertikal: dem Gelaende folgen, Absaetze weich abfangen
const ground = this.opts.height(this.pos.x, this.pos.z) + this.opts.eyeHeight; const ground = this.opts.height(this.pos.x, this.pos.z) + this.opts.eyeHeight;
if (this.pos.y > ground + 0.02) { if (!this.grounded || this.pos.y > ground + 0.02) {
this.velocityY -= 9.81 * dt; this.velocityY -= GRAVITY * dt;
this.pos.y += this.velocityY * dt; this.pos.y += this.velocityY * dt;
if (this.pos.y <= ground) { this.pos.y = ground; this.velocityY = 0; this.grounded = true; } if (this.velocityY <= 0 && this.pos.y <= ground) {
else this.grounded = false; this.pos.y = ground;
this.velocityY = 0;
this.grounded = true;
} else {
this.grounded = false;
}
} else { } else {
// sanftes Nachziehen beim Aufsteigen // sanftes Nachziehen beim Aufsteigen
this.pos.y += (ground - this.pos.y) * Math.min(1, dt * 12); this.pos.y += (ground - this.pos.y) * Math.min(1, dt * 12);
+33 -3
View File
@@ -13,7 +13,7 @@ import './style.css';
import { loadSite, makeHeightSampler, type SiteData } from './site'; import { loadSite, makeHeightSampler, type SiteData } from './site';
import { buildTerrain } from './scene/terrain'; import { buildTerrain } from './scene/terrain';
import { buildSurfaces } from './scene/surfaces'; import { buildSurfaces } from './scene/surfaces';
import { buildStelen, buildProps, buildLamps, buildLinear } from './scene/structures'; import { buildStelen, buildProps, buildLamps, buildLinear, treppeTritte } from './scene/structures';
import { buildWalls } from './scene/walls'; import { buildWalls } from './scene/walls';
import { buildVegetation } from './scene/vegetation'; import { buildVegetation } from './scene/vegetation';
import { buildBuilding, buildNeighbourBuildings } from './scene/building'; import { buildBuilding, buildNeighbourBuildings } from './scene/building';
@@ -80,6 +80,35 @@ async function main() {
}); });
scene.add(env.group); scene.add(env.group);
// -- Begehbare Bauteile --------------------------------------------------
// Stufen und Treppen stecken nicht im Hoehenraster, sondern sind eigene
// Koerper. Damit man sie wirklich begehen kann, wird ihre Trittflaeche in
// die Hoehenabfrage des Rundgangs eingerechnet - sonst blieben sie blosse
// Hindernisse, um die man herumlaufen muss.
interface Tritt { cx: number; cz: number; hw: number; hd: number; rot: number; top: number }
const tritte: Tritt[] = [];
for (const p of site.props) {
if (p.type !== 'stufe' || p.top === undefined) continue;
tritte.push({
cx: p.x, cz: p.z, hw: p.w / 2, hd: p.d / 2,
rot: (p.rot * Math.PI) / 180, top: p.top,
});
}
for (const f of site.linear) {
if (f.kind === 'treppe') tritte.push(...treppeTritte(f));
}
const onTritt = (t: Tritt, x: number, z: number) => {
const dx = x - t.cx, dz = z - t.cz;
const c = Math.cos(-t.rot), s = Math.sin(-t.rot);
return Math.abs(dx * c - dz * s) <= t.hw && Math.abs(dx * s + dz * c) <= t.hd;
};
/** Hoehe unter den Fuessen: Gelaende oder die hoechste Trittflaeche darauf. */
const walkHeight = (x: number, z: number) => {
let y = height(x, z);
for (const t of tritte) if (onTritt(t, x, z) && t.top > y) y = t.top;
return y;
};
// -- Hindernisse fuer den Rundgang -------------------------------------- // -- Hindernisse fuer den Rundgang --------------------------------------
const obstacles: Obstacle[] = []; const obstacles: Obstacle[] = [];
const b = site.building; const b = site.building;
@@ -113,7 +142,8 @@ async function main() {
} }
} }
for (const p of site.props) { for (const p of site.props) {
if (p.type === 'schacht' || p.type === 'wasserhahn') continue; // Stufen sind begehbar, kein Hindernis - sie stehen in `tritte`.
if (p.type === 'schacht' || p.type === 'wasserhahn' || p.type === 'stufe') continue;
obstacles.push({ cx: p.x, cz: p.z, hw: p.w / 2, hd: p.d / 2, rot: (p.rot * Math.PI) / 180 }); obstacles.push({ cx: p.x, cz: p.z, hw: p.w / 2, hd: p.d / 2, rot: (p.rot * Math.PI) / 180 });
} }
for (const pl of site.plants) { for (const pl of site.plants) {
@@ -137,7 +167,7 @@ async function main() {
obstacles.push({ cx: p.x, cz: p.z, hw: 0.26, hd: 0.26, rot: 0 }); obstacles.push({ cx: p.x, cz: p.z, hw: 0.26, hd: 0.26, rot: 0 });
} }
const walker = new Walker(camera, { height, obstacles, bounds: BOUNDS }); const walker = new Walker(camera, { height: walkHeight, obstacles, bounds: BOUNDS });
walker.attach(canvas); walker.attach(canvas);
const start = site.startViews[0]; const start = site.startViews[0];
walker.teleport(start.x, start.z, start.yaw); walker.teleport(start.x, start.z, start.yaw);
+36 -4
View File
@@ -342,7 +342,7 @@ export function buildProps(props: Prop[], height: (x: number, z: number) => numb
const group = new Group(); const group = new Group();
group.name = 'Objekte'; group.name = 'Objekte';
for (const p of props) { for (const p of props) {
const y = height(p.x, p.z); const y = p.base ?? height(p.x, p.z);
let obj: Group | Mesh; let obj: Group | Mesh;
switch (p.type) { switch (p.type) {
case 'gartenhaus': obj = gartenhaus(p, y); break; case 'gartenhaus': obj = gartenhaus(p, y); break;
@@ -469,7 +469,7 @@ function blockTreppe(f: Linear): Group {
* Gelaender aus Stahlrohr: Pfosten alle rund 1.1 m, Handlauf und Knieholm * Gelaender aus Stahlrohr: Pfosten alle rund 1.1 m, Handlauf und Knieholm
* folgen dem Gefaelle des Laufs. * folgen dem Gefaelle des Laufs.
*/ */
function gelaender(f: Linear): Group { function gelaender(f: Linear, height: (x: number, z: number) => number): Group {
const g = new Group(); const g = new Group();
const mat = new MeshStandardMaterial({ color: 0x4c4f52, roughness: 0.42, metalness: 0.65 }); 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 [[x0, z0], [x1, z1]] = [f.path[0], f.path[f.path.length - 1]];
@@ -483,7 +483,11 @@ function gelaender(f: Linear): Group {
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const t = i / (n - 1); const t = i / (n - 1);
const y = fromY + (toY - fromY) * t; 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)); const px = x0 + dx * t, pz = z0 + dz * t;
// Der Pfosten muss bis ins Gelaende reichen, sonst schwebt er neben der
// Treppe in der Luft.
const fuss = Math.min(y, height(px, pz)) - 0.3;
parts.push(boxGeometry(px, pz, 0.06, 0.06, fuss, y + h, rot, 1));
} }
// Holme: als flache Bloecke ueber die ganze Laenge, um die Neigung gelegt. // 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) { for (const [level, thick] of [[h, 0.055], [h * 0.52, 0.04]] as const) {
@@ -535,6 +539,34 @@ function rinne(f: Linear, height: (x: number, z: number) => number): Group {
return g; return g;
} }
/**
* Trittflaechen einer Blockstufentreppe. Der Rundgang braucht sie, damit man
* die Treppen wirklich hinauf- und hinuntergehen kann - sonst waere die Treppe
* fuer den Spieler nur ein Hindernis im Gelaende.
*/
export function treppeTritte(f: Linear): { cx: number; cz: number; hw: number; hd: number; rot: number; top: number }[] {
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 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 out = [];
for (let i = 0; i < steps; i++) {
const s = (i + 0.5) / steps;
out.push({
cx: x0 + dx * s, cz: z0 + dz * s,
// Wie in obstacleFromSegment: hw laeuft laengs der Achse, hd quer dazu.
hw: tread / 2, hd: width / 2,
rot: Math.atan2(dz, dx),
top: fromY - (i + 1) * riser,
});
}
return out;
}
/** Baut Treppen, Gelaender und Rinnen aus dem Plan. */ /** Baut Treppen, Gelaender und Rinnen aus dem Plan. */
export function buildLinear(features: Linear[], height: (x: number, z: number) => number): Group { export function buildLinear(features: Linear[], height: (x: number, z: number) => number): Group {
const group = new Group(); const group = new Group();
@@ -543,7 +575,7 @@ export function buildLinear(features: Linear[], height: (x: number, z: number) =
let obj: Group; let obj: Group;
switch (f.kind) { switch (f.kind) {
case 'treppe': obj = blockTreppe(f); break; case 'treppe': obj = blockTreppe(f); break;
case 'gelaender': obj = gelaender(f); break; case 'gelaender': obj = gelaender(f, height); break;
case 'rinne': obj = rinne(f, height); break; case 'rinne': obj = rinne(f, height); break;
default: continue; default: continue;
} }
+2
View File
@@ -86,6 +86,8 @@ export interface Prop {
rot: number; rot: number;
/** Absolute Oberkante, sofern das Bauteil nicht auf dem Gelaende aufsitzt. */ /** Absolute Oberkante, sofern das Bauteil nicht auf dem Gelaende aufsitzt. */
top?: number; top?: number;
/** Absolute Unterkante, sofern das Bauteil nicht auf dem Gelaende steht. */
base?: number;
note?: string; note?: string;
} }
+4 -1
View File
@@ -151,7 +151,10 @@ export function setupHud(opts: HudOptions): Hud {
<h3>Bedienung</h3> <h3>Bedienung</h3>
<ul> <ul>
<li>Rundgang: Klicken, dann <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd>, <kbd>Shift</kbd> zum Rennen</li> <li>Rundgang: Klicken, dann <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd>, <kbd>Shift</kbd> zum Rennen,
<kbd>Leertaste</kbd> zum Springen</li>
<li>Treppen und die schwebenden Stufen sind begehbar &ndash; hinauf geht es zu Fuss
oder mit einem Sprung</li>
<li>Am Handy: links der Joystick, rechts wischen zum Umsehen</li> <li>Am Handy: links der Joystick, rechts wischen zum Umsehen</li>
<li>Übersicht: ziehen zum Drehen, Mausrad zum Zoomen</li> <li>Übersicht: ziehen zum Drehen, Mausrad zum Zoomen</li>
<li>Die Sonne steht für Abtwil SG astronomisch korrekt</li> <li>Die Sonne steht für Abtwil SG astronomisch korrekt</li>
+72 -29
View File
@@ -84,6 +84,42 @@ export const TREES = [
* bleibt die Ausgleichsebene aus den Koten stehen. * bleibt die Ausgleichsebene aus den Koten stehen.
*/ */
export const LEVELS = [ export const LEVELS = [
{
// Die Feuerwehrzufahrt ist eine Rampe: der Plan gibt -2.08 bei z -7.21
// und -1.08 bei z 0.12 an, also rund 13.6 % Steigung. Ohne feste Vorgabe
// legt sich der Belag als schwebende Platte ueber das Gelaende.
id: 'feuerwehrzufahrt', plane: [0, 0.1364, -1.096], types: ['tegula'],
note: 'Feuerwehrzufahrt als Rampe',
polygon: [[-25.90, -13.80], [-21.90, -13.80], [-21.90, 6.60], [-25.90, 6.60]],
},
{
// Der Ostweg steigt von der Treppenkrone (-0.37 bei z -4.18) bis zur
// Suedostecke (0.63 bei z 4.31) gleichmaessig an - im Architekturplan
// steht dort "Steigung 9.9%".
id: 'weg-ost', plane: [0, 0.1178, 0.1224], types: ['tegula'],
note: 'Ostweg als Rampe',
polygon: [[2.80, -4.30], [4.55, -4.30], [4.55, 5.05], [2.80, 5.05]],
},
{
id: 'weg-ost-unten', level: -1.57, types: ['tegula'],
note: 'Podest am Fuss der Osttreppe',
polygon: [[2.80, -7.70], [4.55, -7.70], [4.55, -6.45], [2.80, -6.45]],
},
{
// Der Weg an der Suedgrenze folgt dem Anstieg der Grenzkoten von -0.04
// im Westen auf 0.63 im Osten.
id: 'weg-sued', plane: [0.0305, 0, 0.5634], types: ['tegula'],
note: 'Weg an der Suedgrenze als Rampe',
polygon: [[-21.90, 4.45], [4.55, 4.45], [4.55, 6.05], [-21.90, 6.05]],
},
{
// Der Rasen der oberen Ebene ist eben. Die Koten 0.09 bis 0.26 liegen
// schon im angehobenen Kiesbett des Sichtschutzes, nicht mehr im Rasen -
// ohne feste Vorgabe zieht die Interpolation den Rasen huegelig hoch.
id: 'oben', level: -0.02, flatten: true,
note: 'Obere Ebene: Rasen und Kiesfeld, eben auf -0.02',
polygon: [[-18.30, 0.90], [-8.60, 0.90], [-8.60, 3.90], [-18.30, 3.90]],
},
{ {
id: 'unten', id: 'unten',
level: -0.33, level: -0.33,
@@ -142,16 +178,8 @@ export const WALLS = [
// sie stuetzen den Garten gegen den tiefer liegenden Fussweg ab. // sie stuetzen den Garten gegen den tiefer liegenden Fussweg ab.
id: 'winkelelemente-ost', type: 'beton', id: 'winkelelemente-ost', type: 'beton',
note: 'Winkelelemente aus Beton an der Ostgrenze, gegen den Fussweg', note: 'Winkelelemente aus Beton an der Ostgrenze, gegen den Fussweg',
path: [[3.735, -6.90], [3.735, 4.90]], path: [[4.45, -6.90], [4.45, 4.90]],
t: 0.14, top: null, base: null, retains: 'ost', t: 0.16, top: null, base: null, retains: 'ost',
},
{
// Der Plan beschriftet die Linie bei z 4.96 mit "Winkelelemente": sie
// fasst das hoeher liegende Nachbargrundstueck hinter dem Sichtschutz.
id: 'winkelelemente-sued', type: 'beton',
note: 'Winkelelemente aus Beton entlang der Suedgrenze',
path: [[-21.20, 4.96], [1.99, 4.96]],
t: 0.14, top: null, base: null, retains: 'sued',
}, },
{ {
id: 'betonmauer-nachbar', type: 'beton', id: 'betonmauer-nachbar', type: 'beton',
@@ -209,14 +237,14 @@ export const PROPS = [
note: 'Schlaufschacht D = 30 cm' }, note: 'Schlaufschacht D = 30 cm' },
// Der Torfluegel liegt im Plan bei x -21.53 und laeuft ueber z 2.56 bis 3.56. // Der Torfluegel liegt im Plan bei x -21.53 und laeuft ueber z 2.56 bis 3.56.
{ id: 'gartentor', type: 'tor', x: -21.53, z: 3.06, w: 1.00, d: 0.06, h: 1.20, rot: 90, { id: 'gartentor', type: 'tor', x: -21.53, z: 3.06, w: 1.00, d: 0.06, h: 1.20, rot: 90,
note: 'Gartentor 100 cm, Stabgitter aus Stahl' }, base: -0.13, note: 'Gartentor 100 cm, Stabgitter aus Stahl, auf dem oberen Niveau' },
// Zwei schwebende Travertinplatten fuehren vom gedeckten Sitzplatz (-0.02) // Zwei schwebende Travertinplatten fuehren vom gedeckten Sitzplatz (-0.02)
// auf den Hauptsitzplatz (-0.33). Lage und Format stehen im Plan, die // auf den Hauptsitzplatz (-0.33). Lage und Format stehen im Plan, die
// Ausbildung als auskragende Platte zeigt das Referenzbild des Planers. // Ausbildung als auskragende Platte zeigt das Referenzbild des Planers.
{ id: 'stufe-oben', type: 'stufe', x: -14.335, z: -1.065, w: 0.81, d: 3.25, h: 0.08, rot: 0, { id: 'stufe-oben', type: 'stufe', x: -14.335, z: -1.065, w: 0.81, d: 3.25, h: 0.08, rot: 0,
top: -0.17, note: 'Schwebende Travertinstufe, Tritthoehe -0.17' }, top: -0.02, note: 'Schwebende Travertinstufe, Tritthoehe -0.02 (Kote im Plan)' },
{ id: 'stufe-unten', type: 'stufe', x: -14.880, z: -1.480, w: 0.80, d: 3.20, h: 0.08, rot: 0, { id: 'stufe-unten', type: 'stufe', x: -14.880, z: -1.480, w: 0.80, d: 3.20, h: 0.08, rot: 0,
top: -0.31, note: 'Schwebende Travertinstufe, Tritthoehe -0.31' }, top: -0.17, note: 'Schwebende Travertinstufe, Tritthoehe -0.17 (Kote im Plan)' },
// Beim Gartentor liegen ebenfalls zwei schwebende Travertinstufen: der Plan // Beim Gartentor liegen ebenfalls zwei schwebende Travertinstufen: der Plan
// zeichnet ein Feld 0.80 x 1.60 m mit den Koten -0.13 (Podest am Tor) und // zeichnet ein Feld 0.80 x 1.60 m mit den Koten -0.13 (Podest am Tor) und
// -0.32 (Anschluss an den Hauptsitzplatz). // -0.32 (Anschluss an den Hauptsitzplatz).
@@ -245,15 +273,20 @@ export const EXTRA_SURFACES = [
// Der Plan zeigt oestlich der Grenze einen gepflasterten Weg, erst dahinter // Der Plan zeigt oestlich der Grenze einen gepflasterten Weg, erst dahinter
// beginnt die Wiese. Zwei Leuchten stehen daran. // beginnt die Wiese. Zwei Leuchten stehen daran.
type: 'tegula', type: 'tegula',
note: 'Fussweg an der Ostseite zwischen Verbundstein-Anpassung und Winkelelementen', note: 'Weg an der Ostseite, Rampe oberhalb der Treppe (Steigung rund 12%)',
polygon: [[2.86, -7.00], [3.69, -7.00], [3.69, 4.90], [2.86, 4.90]], polygon: [[2.86, -4.18], [4.45, -4.18], [4.45, 4.96], [2.86, 4.96]],
},
{
type: 'tegula',
note: 'Weg an der Ostseite, Podest unterhalb der Treppe auf -1.57',
polygon: [[2.86, -7.60], [4.45, -7.60], [4.45, -6.52], [2.86, -6.52]],
}, },
{ {
// Schmaler gepflasterter Streifen jenseits der Suedgrenze, zwischen // Schmaler gepflasterter Streifen jenseits der Suedgrenze, zwischen
// Grenze (z 4.49) und den Winkelelementen des Nachbarn (z 4.96). // Grenze (z 4.49) und den Winkelelementen des Nachbarn (z 4.96).
type: 'tegula', type: 'tegula',
note: 'Tegula-Streifen des Suednachbarn entlang der Grenze', note: 'Weg entlang der Suedgrenze: von der Grenze bis an die Nachbarmauer gepflastert',
polygon: [[-21.74, 4.49], [1.99, 4.49], [1.99, 4.96], [-21.74, 4.96]], polygon: [[-21.74, 4.49], [4.45, 4.49], [4.45, 5.97], [-21.74, 5.97]],
}, },
{ {
type: 'tegula', type: 'tegula',
@@ -295,12 +328,12 @@ export const LINEAR = [
// x -23.34; Koten -0.28 oben und -1.08 auf der Feuerwehrzufahrt. // x -23.34; Koten -0.28 oben und -1.08 auf der Feuerwehrzufahrt.
id: 'treppe-west', kind: 'treppe', id: 'treppe-west', kind: 'treppe',
note: 'Treppe und Rampe von der Feuerwehrzufahrt zum Gartentor (-0.28 auf -1.08)', note: 'Treppe und Rampe von der Feuerwehrzufahrt zum Gartentor (-0.28 auf -1.08)',
path: [[-21.84, 1.37], [-22.64, 1.37]], width: 1.70, fromY: -0.28, toY: -1.08, steps: 4, path: [[-21.80, 1.37], [-23.34, 1.37]], width: 1.70, fromY: -0.28, toY: -1.08, steps: 5,
}, },
{ {
id: 'gelaender-west', kind: 'gelaender', id: 'gelaender-west', kind: 'gelaender',
note: 'Gelaender an der Treppe zur Feuerwehrzufahrt', note: 'Gelaender an der Treppe zur Feuerwehrzufahrt',
path: [[-21.84, 0.52], [-22.64, 0.52]], fromY: -0.28, toY: -1.08, height: 0.95, path: [[-21.80, 0.52], [-23.34, 0.52]], fromY: -0.28, toY: -1.08, height: 0.95,
}, },
{ {
id: 'rinne-gartentor', kind: 'rinne', id: 'rinne-gartentor', kind: 'rinne',
@@ -397,7 +430,7 @@ export const NEIGHBOUR_BUILDINGS = [
export const TERRAIN_ZONES = [ export const TERRAIN_ZONES = [
{ {
id: 'garten', label: 'Garten Kellenberger', id: 'garten', label: 'Garten Kellenberger',
polygon: [[-20.90, -6.60], [3.74, -6.60], [3.74, 4.96], [-20.90, 4.96]], polygon: [[-20.90, -6.60], [4.55, -6.60], [4.55, 5.97], [-20.90, 5.97]],
fallback: -0.05, fallback: -0.05,
}, },
{ {
@@ -415,12 +448,12 @@ export const TERRAIN_ZONES = [
// faengt der Nachbar sein hoeher liegendes Terrain, dahinter steht die // faengt der Nachbar sein hoeher liegendes Terrain, dahinter steht die
// bestehende Betonmauer. // bestehende Betonmauer.
id: 'sued', label: 'Nachbargrundstück Süd', id: 'sued', label: 'Nachbargrundstück Süd',
polygon: [[-22.00, 4.96], [6.00, 4.96], [6.00, 12.00], [-22.00, 12.00]], polygon: [[-22.00, 5.97], [6.00, 5.97], [6.00, 12.00], [-22.00, 12.00]],
fallback: 0.45, fallback: 0.45,
}, },
{ {
id: 'ost', label: 'Wiese Ost', id: 'ost', label: 'Wiese Ost',
polygon: [[3.74, -6.60], [9.00, -6.60], [9.00, 4.96], [3.74, 4.96]], polygon: [[4.55, -6.60], [9.00, -6.60], [9.00, 5.97], [4.55, 5.97]],
fallback: 0.55, fallback: 0.55,
}, },
]; ];
@@ -430,10 +463,14 @@ export const TERRAIN_ZONES = [
* plausibles Gelaende aber noetig sind (Auslauf gegen die Grundstuecksraender). * plausibles Gelaende aber noetig sind (Auslauf gegen die Grundstuecksraender).
*/ */
export const EXTRA_SPOTS = [ export const EXTRA_SPOTS = [
{ x: -24.5, z: -8.0, y: -2.60, zone: 'zufahrt' }, // Auf der Zufahrt selbst gelten die Plankoten -2.08 (z -7.21) und -1.08
{ x: -24.5, z: 0.0, y: -1.60, zone: 'zufahrt' }, // (z 0.12); quer dazu ist sie eben, sonst entsteht eine Gelaendekante.
{ x: -24.5, z: 5.0, y: -0.60, zone: 'zufahrt' }, { x: -25.0, z: -12.0, y: -2.75, zone: 'zufahrt' },
{ x: -22.6, z: 5.5, y: -0.30, zone: 'zufahrt' }, { x: -25.0, z: -7.2, y: -2.08, zone: 'zufahrt' },
{ x: -25.0, z: 0.1, y: -1.08, zone: 'zufahrt' },
{ x: -25.0, z: 6.0, y: -0.28, zone: 'zufahrt' },
{ x: -22.4, z: -7.2, y: -2.08, zone: 'zufahrt' },
{ x: -22.4, z: 6.0, y: -0.28, zone: 'zufahrt' },
{ x: -20.0, z: -12.5, y: -1.75, zone: 'zollet' }, { x: -20.0, z: -12.5, y: -1.75, zone: 'zollet' },
{ x: -8.0, z: -12.5, y: -1.70, zone: 'zollet' }, { x: -8.0, z: -12.5, y: -1.70, zone: 'zollet' },
{ x: 2.0, z: -11.5, y: -1.65, zone: 'zollet' }, { x: 2.0, z: -11.5, y: -1.65, zone: 'zollet' },
@@ -446,9 +483,15 @@ export const EXTRA_SPOTS = [
{ x: -18.0, z: 9.0, y: 0.25, zone: 'sued' }, { x: -18.0, z: 9.0, y: 0.25, zone: 'sued' },
{ x: -6.0, z: 9.0, y: 0.60, zone: 'sued' }, { x: -6.0, z: 9.0, y: 0.60, zone: 'sued' },
{ x: 4.0, z: 9.0, y: 1.00, zone: 'sued' }, { x: 4.0, z: 9.0, y: 1.00, zone: 'sued' },
{ x: 7.5, z: -5.0, y: 0.10, zone: 'ost' }, // Oestlich der Grenze faellt das Gelaende nach Norden zum Stellplatz des
{ x: 7.5, z: 0.0, y: 0.45, zone: 'ost' }, // Nachbarn ab - die Plankoten dort lauten -0.72 und -1.57. Ohne diese
{ x: 7.5, z: 5.0, y: 0.80, zone: 'ost' }, // Werte legt sich ein Huegel neben den Weg und deckt ihn zu.
{ x: 7.5, z: -6.4, y: -1.25, zone: 'ost' },
{ x: 7.5, z: -3.0, y: -0.35, zone: 'ost' },
{ x: 7.5, z: 0.0, y: 0.20, zone: 'ost' },
{ x: 7.5, z: 5.0, y: 0.75, zone: 'ost' },
{ x: 5.0, z: -6.4, y: -1.35, zone: 'ost' },
{ x: 3.0, z: -8.5, y: -1.55, zone: 'zollet' },
]; ];
/** Startposition und Blickrichtung beim Betreten der Szene. yaw = Grad ab Nord. */ /** Startposition und Blickrichtung beim Betreten der Szene. yaw = Grad ab Nord. */
+19 -6
View File
@@ -293,8 +293,12 @@ for (const s of surfaces) {
let plane, ys; let plane, ys;
if (fixed) { if (fixed) {
plane = { a: 0, b: 0, c: fixed.level }; // Eine Ebene darf auch geneigt vorgegeben sein - Wege sind Rampen.
ys = [fixed.level]; plane = fixed.plane
? { a: fixed.plane[0], b: fixed.plane[1], c: fixed.plane[2] }
: { a: 0, b: 0, c: fixed.level };
const corners = s.polygon.map(([x, z]) => plane.a * x + plane.b * z + plane.c);
ys = [Math.min(...corners), Math.max(...corners)];
s.level = fixed.id; s.level = fixed.id;
} else { } else {
const inside = terrainSpots.filter((sp) => owner.get(sp) === s); const inside = terrainSpots.filter((sp) => owner.get(sp) === s);
@@ -363,16 +367,25 @@ for (let j = 0; j < nz; j++) {
const zone = zoneOf(x, z); const zone = zoneOf(x, z);
let y; let y;
if (!zone) { if (!zone) {
// Ausserhalb aller Zonen: naechstgelegene Zone weiterziehen // Ausserhalb aller Zonen: naechstgelegene Zone weiterziehen. Dabei wird
// deren Interpolation fortgesetzt, nicht ihr konstanter Ersatzwert -
// sonst entsteht am Zonenrand eine Gelaendekante.
let best = null, bd = Infinity; let best = null, bd = Infinity;
for (const t of TERRAIN_ZONES) { for (const t of TERRAIN_ZONES) {
const [cx, cz] = centroid(t.polygon); const d = distToPolygon(x, z, t.polygon);
const d = Math.hypot(cx - x, cz - z);
if (d < bd) { bd = d; best = t; } if (d < bd) { bd = d; best = t; }
} }
y = best.fallback; y = interpolate(x, z, best.id);
} else { } else {
y = interpolate(x, z, zone); y = interpolate(x, z, zone);
// Ausdruecklich flach vorgegebene Ebenen: der Rasen der oberen Ebene
// ist eben, ohne Vorgabe zieht ihn die Interpolation huegelig hoch.
for (const lv of LEVELS) {
if (!lv.flatten || !pointInPolygon(x, z, lv.polygon)) continue;
y = lv.level;
locked[j * nx + i] = 1;
break;
}
for (const f of flatSurfaces) { for (const f of flatSurfaces) {
if (!pointInPolygon(x, z, f.polygon)) continue; if (!pointInPolygon(x, z, f.polygon)) continue;
y = Math.max(f.min, Math.min(f.max, f.plane.a * x + f.plane.b * z + f.plane.c)); y = Math.max(f.min, Math.min(f.max, f.plane.a * x + f.plane.b * z + f.plane.c));