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
+20 -6
View File
@@ -1,7 +1,7 @@
/**
* 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
* von Mauern, Gebaeuden und Objekten aufgehalten.
* Der Spieler folgt dem Gelaende, kann Stufen bis 45 cm ersteigen, mit der
* Leertaste springen und wird von Mauern, Gebaeuden und Objekten aufgehalten.
*/
import { Euler, PerspectiveCamera, Vector3 } from 'three';
@@ -28,6 +28,9 @@ export interface WalkerOptions {
const SPEED_WALK = 1.65;
const SPEED_RUN = 3.6;
/** Absprunggeschwindigkeit: reicht fuer rund 55 cm Sprunghoehe. */
const JUMP = 3.3;
const GRAVITY = 9.81;
export class Walker {
readonly camera: PerspectiveCamera;
@@ -155,13 +158,24 @@ export class Walker {
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
const ground = this.opts.height(this.pos.x, this.pos.z) + this.opts.eyeHeight;
if (this.pos.y > ground + 0.02) {
this.velocityY -= 9.81 * dt;
if (!this.grounded || this.pos.y > ground + 0.02) {
this.velocityY -= GRAVITY * dt;
this.pos.y += this.velocityY * dt;
if (this.pos.y <= ground) { this.pos.y = ground; this.velocityY = 0; this.grounded = true; }
else this.grounded = false;
if (this.velocityY <= 0 && this.pos.y <= ground) {
this.pos.y = ground;
this.velocityY = 0;
this.grounded = true;
} else {
this.grounded = false;
}
} else {
// sanftes Nachziehen beim Aufsteigen
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 { buildTerrain } from './scene/terrain';
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 { buildVegetation } from './scene/vegetation';
import { buildBuilding, buildNeighbourBuildings } from './scene/building';
@@ -80,6 +80,35 @@ async function main() {
});
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 --------------------------------------
const obstacles: Obstacle[] = [];
const b = site.building;
@@ -113,7 +142,8 @@ async function main() {
}
}
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 });
}
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 });
}
const walker = new Walker(camera, { height, obstacles, bounds: BOUNDS });
const walker = new Walker(camera, { height: walkHeight, obstacles, bounds: BOUNDS });
walker.attach(canvas);
const start = site.startViews[0];
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();
group.name = 'Objekte';
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;
switch (p.type) {
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
* 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 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]];
@@ -483,7 +483,11 @@ function gelaender(f: Linear): Group {
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));
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.
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;
}
/**
* 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. */
export function buildLinear(features: Linear[], height: (x: number, z: number) => number): Group {
const group = new Group();
@@ -543,7 +575,7 @@ export function buildLinear(features: Linear[], height: (x: number, z: number) =
let obj: Group;
switch (f.kind) {
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;
default: continue;
}
+2
View File
@@ -86,6 +86,8 @@ export interface Prop {
rot: number;
/** Absolute Oberkante, sofern das Bauteil nicht auf dem Gelaende aufsitzt. */
top?: number;
/** Absolute Unterkante, sofern das Bauteil nicht auf dem Gelaende steht. */
base?: number;
note?: string;
}
+4 -1
View File
@@ -151,7 +151,10 @@ export function setupHud(opts: HudOptions): Hud {
<h3>Bedienung</h3>
<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>Übersicht: ziehen zum Drehen, Mausrad zum Zoomen</li>
<li>Die Sonne steht für Abtwil SG astronomisch korrekt</li>