Sites are data: extract backyard_01.json; world.js builds from it
SPRINT10 gate 1a, carried since Sprint 8. Everything that was a module constant
— bed rect, house line, tree and post placements, shed, gnome, sun angle, fence
sides — is now data/sites/backyard_01.json, and world.js is a builder.
The extraction is byte-identical, and that's checkable rather than claimable:
E's branch-anchor tripwires pin five positions to 6 dp, a.test's quad-band
asserts pin the whole rigging geometry (>=3 quads in 18-45 m2, full coverage
still >45 m2), D's ladder field and B's balance lines all pass unchanged.
287/0/0.
It's byte-identical by construction, not by luck: the site-derived values keep
the builder's original local names, so ~500 lines of geometry that was already
correct is untouched and only its SOURCE changed.
Two things earned their place:
Anchors carry D's `work: "cloth" | "bracket"` — the MECHANISM, not the type. A
post is tensioned from a cleat at its base and a tree strop is thrown, but a
bracket bolted up a bare wall is the one you cannot fake. Keyed on mechanism
because `type` is a closed enum ('house'|'tree'|'post') that site_02's carport
doesn't fit, and needsLadder keyed on type FAILS OPEN there — the ladder
silently vanishes and you re-rig a 2.6 m bracket standing on the grass.
p4's swept position table moved INTO the JSON's `_why`, beside the coordinate it
justifies. That table is the only thing stopping someone nudging the post toward
the bed and collapsing DESIGN.md's central tension; it belongs with the number,
not in a file the number left.
validateSite() fails loud: sites are content, and a missing bed silently becomes
NaN coverage while a missing anchor list silently becomes an unriggable site.
createWorld now requires a site and says so.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
63beec87a7
commit
b98dcad13a
86
web/world/data/sites/backyard_01.json
Normal file
86
web/world/data/sites/backyard_01.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"id": "backyard_01",
|
||||
"name": "The Backyard",
|
||||
"blurb": "Your own place. You know where everything is, which is the only advantage you get.",
|
||||
|
||||
"yard": { "width": 30, "depth": 20 },
|
||||
|
||||
"sun": { "elevationDeg": 55, "azimuthDeg": -125 },
|
||||
|
||||
"gardenBed": { "x": 1, "z": 2, "w": 6, "d": 4 },
|
||||
|
||||
"house": {
|
||||
"model": "house_yardside_v1",
|
||||
"x": 0, "z": -10.5,
|
||||
"anchors": [
|
||||
{ "id": "h1", "node": "fascia_anchor_01", "type": "house", "work": "bracket" },
|
||||
{ "id": "h2", "node": "fascia_anchor_02", "type": "house", "work": "bracket" },
|
||||
{ "id": "h3", "node": "fascia_anchor_03", "type": "house", "work": "bracket" }
|
||||
]
|
||||
},
|
||||
|
||||
"trees": [
|
||||
{
|
||||
"id": "t1", "model": "tree_gum_01_v1",
|
||||
"x": -9, "z": 2, "phase": 0.7, "trunkH": 4.2, "anchorY": 3.4,
|
||||
"anchors": [
|
||||
{ "id": "t1", "node": "branch_anchor_01", "type": "tree", "work": "cloth" },
|
||||
{ "id": "t1b", "node": "branch_anchor_02", "type": "tree", "work": "cloth" },
|
||||
{ "id": "t1c", "node": "branch_anchor_03", "type": "tree", "work": "cloth" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "t2", "model": "tree_gum_02_v1",
|
||||
"x": 8, "z": -2, "phase": 2.9, "trunkH": 3.8, "anchorY": 3.1,
|
||||
"anchors": [
|
||||
{ "id": "t2", "node": "branch_anchor_01", "type": "tree", "work": "cloth" },
|
||||
{ "id": "t2b", "node": "branch_anchor_02", "type": "tree", "work": "cloth" }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"posts": [
|
||||
{ "id": "p1", "x": -4.5, "z": 5.5, "h": 4.0, "type": "post", "work": "cloth" },
|
||||
{ "id": "p2", "x": 4.0, "z": 6.0, "h": 4.0, "type": "post", "work": "cloth" },
|
||||
{ "id": "p3", "x": 0, "z": 7.0, "h": 4.0, "type": "post", "work": "cloth" },
|
||||
{
|
||||
"id": "p4", "x": -3.2, "z": -1.2, "h": 4.0, "type": "post", "work": "cloth",
|
||||
"_why": [
|
||||
"DO NOT move this post toward the bed. Its position is swept, not chosen, and it is the",
|
||||
"single number holding DESIGN.md's central tension open.",
|
||||
"",
|
||||
"Before p4 the wild night had NO winnable line, for a geometric reason: every anchor near",
|
||||
"the bed (p1/p2/p3) is SOUTH of it and nothing stands north short of the house 10 m away,",
|
||||
"so covering rigs had to span the yard and died while rigs small enough to survive sat",
|
||||
"beside the bed rather than over it. p4 supplies the missing north-west corner.",
|
||||
"",
|
||||
"Swept against the smallest quad covering 90% of the bed:",
|
||||
" (-2.2, -1.2) -> 44.4 m2 DO NOT: full coverage lands INSIDE the survivable band,",
|
||||
" so covering the bed stops costing risk and the rigging",
|
||||
" puzzle loses its wrong answers.",
|
||||
" (-3.2, -1.2) -> 48.6 m2 <- here (with the 8 deg rake the top anchor lands at",
|
||||
" -3.72,-1.40 and full coverage costs 51.6 m2)",
|
||||
" (-3.2, -2.0) -> 51.7 m2",
|
||||
" (-4.2, -3.0) -> 60.0 m2",
|
||||
" beyond z=-4 -> 63.5 m2, i.e. too far out to matter at all",
|
||||
"",
|
||||
"Full coverage costs 63.5 m2 without it and 51.6 with: a real new option, still bigger",
|
||||
"than the 23-38 m2 rigs that survive unaided. a.test.js asserts the >45 m2 floor, so",
|
||||
"moving this inward goes red — that assert is the tension's only guard."
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"shed": { "model": "shed_01_v1", "x": 11.8, "z": 6.2, "rotYDeg": -90 },
|
||||
"shedTable": { "model": "shed_table_v1", "x": 9, "z": 6, "rotYDeg": -90, "pickupNode": "pickup_anchor" },
|
||||
|
||||
"gnome": { "model": "garden_gnome_01_v1", "x": 4.3, "z": 4.4, "rotYDeg": -126, "collateralValue": 25 },
|
||||
|
||||
"fence": {
|
||||
"sides": ["south", "west", "east"]
|
||||
},
|
||||
|
||||
"wind": {
|
||||
"venturi": []
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
|
||||
import * as THREE from '../vendor/three.module.js';
|
||||
import { FIXED_DT, PHASES, STORM_LEN, HARDWARE, SPARE_COST, Emitter } from './contracts.js';
|
||||
import { createWorld } from './world.js';
|
||||
import { createWorld, loadSite } from './world.js';
|
||||
import { createCameraRig } from './camera.js';
|
||||
import { loadStorm, createWind } from './weather.js';
|
||||
import { SailRig, createSailView } from './sail.js';
|
||||
@ -376,7 +376,13 @@ export async function boot(opts = {}) {
|
||||
const wind = createWindRouter(Object.values(winds));
|
||||
|
||||
// --- world & camera -----------------------------------------------------
|
||||
const world = createWorld(scene, { wind });
|
||||
// SPRINT10: the yard is data. `site` decides which one, and the week hands it
|
||||
// over per night — night three is the corner block.
|
||||
const site = await loadSite(opts.site ?? week.site);
|
||||
const world = createWorld(scene, { wind, site });
|
||||
// Lane C: a venturi is a shelter's opposite — a gap SPEEDS wind along its axis.
|
||||
// Absent/empty is a perfect no-op, which is what keeps backyard_01 identical.
|
||||
wind.setVenturi?.(site.wind?.venturi ?? []);
|
||||
// Lane E's GLBs land over the graybox. Awaited here because it resolves
|
||||
// world.shedTable onto E's baked pickup_anchor, and wireYardActions (below,
|
||||
// inside rigSail) reads that position when it registers the spare pickup.
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import * as THREE from '../../vendor/three.module.js';
|
||||
import { FIXED_DT, STORM_LEN, YARD, checkContract, createStubWind } from '../contracts.js';
|
||||
import { createWorld, heightAt } from '../world.js';
|
||||
import { createWorld, heightAt, loadSite, validateSite } from '../world.js';
|
||||
import { createCameraRig } from '../camera.js';
|
||||
import { createGame, createWindRouter, verdictFor } from '../main.js';
|
||||
import { orderRing } from '../sail.js';
|
||||
@ -16,7 +16,10 @@ import { assert, assertEq, assertLess, fixedLoop } from '../testkit.js';
|
||||
/** @param {import('../testkit.js').Suite} t */
|
||||
export default async function run(t) {
|
||||
const scene = new THREE.Scene();
|
||||
const world = createWorld(scene, { wind: createStubWind({ calm: true }) });
|
||||
// SPRINT10: the yard is data now. Loading the REAL site rather than a fixture
|
||||
// is the point — these asserts are the proof that the extraction moved nothing.
|
||||
const site = await loadSite('backyard_01');
|
||||
const world = createWorld(scene, { wind: createStubWind({ calm: true }), site });
|
||||
|
||||
// Dress the yard before asserting anything about it: anchors are only FINAL
|
||||
// after dress(), which moves them onto the positions Lane E baked and adds the
|
||||
|
||||
@ -64,7 +64,12 @@ async function buildYard() {
|
||||
speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
|
||||
gustTelegraph: () => null, setSheltersFromTrees() {}, eventsBetween: () => [],
|
||||
};
|
||||
const world = createWorld(scene, { wind: calm });
|
||||
// SPRINT10 (Lane A, in Lane B's file — minimal, shout if you'd rather it read
|
||||
// differently): the yard is data now, so createWorld needs the site. Loading
|
||||
// the real backyard_01 keeps this suite's "read the yard, never copy it" rule
|
||||
// intact — it's the same yard, from its source instead of from code.
|
||||
const { loadSite } = await import('../world.js');
|
||||
const world = createWorld(scene, { wind: calm, site: await loadSite('backyard_01') });
|
||||
if (world.dress) { try { await world.dress(); } catch { /* graybox anchors are enough */ } }
|
||||
const anchors = world.anchors.map((a) => {
|
||||
const pos = { x: a.pos.x, y: a.pos.y, z: a.pos.z };
|
||||
|
||||
@ -12,7 +12,10 @@
|
||||
*/
|
||||
|
||||
import * as THREE from '../vendor/three.module.js';
|
||||
import { YARD, createStubWind } from './contracts.js';
|
||||
import { createStubWind } from './contracts.js';
|
||||
|
||||
/** Degrees → radians. Site JSON is authored in degrees; nobody writes π/2 by hand. */
|
||||
const rad = (deg) => ((deg ?? 0) * Math.PI) / 180;
|
||||
|
||||
/**
|
||||
* Ground height in meters at a world XZ. Pure and cheap — this is called by the
|
||||
@ -33,32 +36,87 @@ export function heightAt(x, z) {
|
||||
);
|
||||
}
|
||||
|
||||
const GARDEN_BED = { x: 1, z: 2, w: 6, d: 4 };
|
||||
const SITE_DIR = new URL('../data/sites', import.meta.url).href;
|
||||
|
||||
// Shed on the east side, table out in front of it. Lane D tested reachability
|
||||
// against (9, 6), so the table stays there and the shed tucks in behind.
|
||||
const SHED = { x: 11.8, z: 6.2, rotY: -Math.PI / 2 };
|
||||
const SHED_TABLE = { x: 9, z: 6, rotY: -Math.PI / 2 };
|
||||
/**
|
||||
* Load a site. SPRINT10 gate 1 — the yard stopped being code today.
|
||||
*
|
||||
* Everything that used to be a constant in this file (bed rect, house line,
|
||||
* tree and post placements, shed, gnome, sun angle) is now `data/sites/*.json`,
|
||||
* and world.js is a builder. The proof of a faithful extraction is that nothing
|
||||
* moved: Lane E's anchor tripwires pin five branch positions to 6 dp and
|
||||
* a.test's quad-band asserts pin the whole rigging geometry, so a byte-identical
|
||||
* backyard_01 is checkable rather than claimable.
|
||||
*
|
||||
* Async and separate from createWorld() for the same reason dress() is: the
|
||||
* selftest builds yards with no server, and a fetch in the constructor is either
|
||||
* a break or a flake. Callers load the site, then build.
|
||||
*
|
||||
* @param {string} name e.g. 'backyard_01'
|
||||
*/
|
||||
export async function loadSite(name, dir = SITE_DIR) {
|
||||
const url = `${dir}/${name}.json`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`world: cannot load site ${url} (${res.status})`);
|
||||
return validateSite(await res.json(), name);
|
||||
}
|
||||
|
||||
// Lane E's house_yardside GLB is a 9.2 x 2.9 x 1.05 m façade whose fascia
|
||||
// anchors sit at local z = +0.55, so placing it here lands them on z = -9.95 —
|
||||
// the same line the graybox taught everyone to expect.
|
||||
const HOUSE = { x: 0, z: -10.5 };
|
||||
/**
|
||||
* Sites are content, so they fail loud rather than half-build. Every field this
|
||||
* checks is one that would otherwise produce a yard that looks fine and is
|
||||
* subtly wrong — an absent bed silently becomes NaN coverage, a missing anchor
|
||||
* list silently becomes an unriggable site.
|
||||
*/
|
||||
export function validateSite(site, name = site?.id ?? '?') {
|
||||
const bad = [];
|
||||
if (!site || typeof site !== 'object') bad.push('not an object');
|
||||
else {
|
||||
if (!site.yard?.width || !site.yard?.depth) bad.push('yard.width/depth');
|
||||
if (!site.gardenBed) bad.push('gardenBed');
|
||||
if (!Number.isFinite(site.sun?.elevationDeg)) bad.push('sun.elevationDeg');
|
||||
if (!Number.isFinite(site.sun?.azimuthDeg)) bad.push('sun.azimuthDeg');
|
||||
const ids = [
|
||||
...(site.house?.anchors ?? []).map((a) => a.id),
|
||||
...(site.trees ?? []).flatMap((t) => (t.anchors ?? []).map((a) => a.id)),
|
||||
...(site.posts ?? []).map((p) => p.id),
|
||||
...(site.structures ?? []).flatMap((s) => (s.anchors ?? []).map((a) => a.id)),
|
||||
];
|
||||
if (!ids.length) bad.push('no anchors at all — nothing to rig to');
|
||||
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
|
||||
if (dupes.length) bad.push(`duplicate anchor ids: ${[...new Set(dupes)].join(', ')}`);
|
||||
// Lane D's field. `work` is the MECHANISM, not the anchor's type: a post is
|
||||
// tensioned from a cleat at its base and a tree strop is thrown, but a
|
||||
// bracket bolted up a bare wall is the one you cannot fake — so that, and
|
||||
// only that, wants the ladder. Keyed on mechanism because `type` is a closed
|
||||
// enum ('house'|'tree'|'post') that site_02's carport doesn't fit, and
|
||||
// needsLadder keyed on type FAILS OPEN there: the ladder mechanic silently
|
||||
// vanishes and you re-rig a 2.6 m bracket standing on the grass (D's audit).
|
||||
const works = [
|
||||
...(site.house?.anchors ?? []),
|
||||
...(site.trees ?? []).flatMap((t) => t.anchors ?? []),
|
||||
...(site.posts ?? []),
|
||||
...(site.structures ?? []).flatMap((s) => s.anchors ?? []),
|
||||
];
|
||||
for (const a of works) {
|
||||
if (a.work !== 'cloth' && a.work !== 'bracket') {
|
||||
bad.push(`anchor ${a.id}: work must be "cloth" or "bracket", got ${JSON.stringify(a.work)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bad.length) throw new Error(`world: site '${name}' is invalid:\n ${bad.join('\n ')}`);
|
||||
return site;
|
||||
}
|
||||
|
||||
// The gnome stands just off the bed's south-east corner — inside the footprint
|
||||
// of most rigs, so a sail that lets go has something of the client's to land on.
|
||||
// DESIGN.md: your own failure is the worst debris of all.
|
||||
const GNOME = { x: 4.3, z: 4.4, rotY: -2.2, collateralValue: 25 };
|
||||
|
||||
// Sun: mid-afternoon, high and off the north-west shoulder. Elevation 55°.
|
||||
// Stored as the direction from the GROUND toward the SUN (see contracts.js).
|
||||
const SUN_ELEV = (55 * Math.PI) / 180;
|
||||
const SUN_AZIM = (-125 * Math.PI) / 180;
|
||||
const SUN_DIR = new THREE.Vector3(
|
||||
Math.cos(SUN_ELEV) * Math.sin(SUN_AZIM),
|
||||
Math.sin(SUN_ELEV),
|
||||
Math.cos(SUN_ELEV) * Math.cos(SUN_AZIM),
|
||||
).normalize();
|
||||
/** Direction from the GROUND toward the SUN (see contracts.js), off site data. */
|
||||
function sunDirOf(site) {
|
||||
const el = (site.sun.elevationDeg * Math.PI) / 180;
|
||||
const az = (site.sun.azimuthDeg * Math.PI) / 180;
|
||||
return new THREE.Vector3(
|
||||
Math.cos(el) * Math.sin(az),
|
||||
Math.sin(el),
|
||||
Math.cos(el) * Math.cos(az),
|
||||
).normalize();
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
sky: 0x9fc4dd,
|
||||
@ -89,6 +147,26 @@ const COLORS = {
|
||||
export function createWorld(scene, opts = {}) {
|
||||
const wind = opts.wind ?? createStubWind({ calm: true });
|
||||
|
||||
// SPRINT10 gate 1. Every one of these was a module constant in this file an
|
||||
// hour ago; they're site data now. They keep their old local names on purpose
|
||||
// — the builder below is ~500 lines of geometry that was already correct, and
|
||||
// rebinding the names is a change of SOURCE, not of behaviour. That's what
|
||||
// makes "backyard_01 is byte-identical" a structural claim rather than a hope.
|
||||
const site = validateSite(
|
||||
opts.site ?? (() => { throw new Error(
|
||||
'world: createWorld now needs a site — `createWorld(scene, { wind, site: await loadSite("backyard_01") })`. '
|
||||
+ 'The yard stopped being code in SPRINT10; data/sites/*.json is the source of truth.'); })(),
|
||||
);
|
||||
const YARD = site.yard;
|
||||
const GARDEN_BED = site.gardenBed;
|
||||
const SUN_DIR = sunDirOf(site);
|
||||
const HOUSE = site.house;
|
||||
const SHED = site.shed;
|
||||
const SHED_TABLE = site.shedTable ? { ...site.shedTable, rotY: rad(site.shedTable.rotYDeg) } : null;
|
||||
const GNOME = site.gnome ? { ...site.gnome, rotY: rad(site.gnome.rotYDeg) } : null;
|
||||
const treeSpecs = site.trees ?? [];
|
||||
const postSpecs = site.posts ?? [];
|
||||
|
||||
const root = new THREE.Group();
|
||||
root.name = 'yard';
|
||||
scene.add(root);
|
||||
@ -194,18 +272,22 @@ export function createWorld(scene, opts = {}) {
|
||||
solids.push(wall, roof);
|
||||
graybox.house = house;
|
||||
|
||||
for (const [i, x] of [-5, 0, 5].entries()) {
|
||||
anchors.push(makeStaticAnchor(`h${i + 1}`, 'house', new THREE.Vector3(x, 2.6, -9.9)));
|
||||
// Graybox stand-ins at the graybox's own spacing. dress() moves every one of
|
||||
// these onto the position Lane E baked, which is why the numbers here don't
|
||||
// matter and the IDS do — the site names them, and everything downstream
|
||||
// (Lane B's quads, D's prompts, the balance suite) refers to them by name.
|
||||
for (const [i, a] of (HOUSE?.anchors ?? []).entries()) {
|
||||
anchors.push(Object.assign(
|
||||
makeStaticAnchor(a.id, a.type ?? 'house', new THREE.Vector3(-5 + i * 5, 2.6, -9.9)),
|
||||
{ work: a.work },
|
||||
));
|
||||
}
|
||||
|
||||
// --- trees -------------------------------------------------------------
|
||||
// Two, on opposite shoulders, mirroring the prototype's tree placement.
|
||||
// Canopies are separate named nodes so they can be swayed independently —
|
||||
// Lane E's tree_gum_01.glb must keep `trunk` / `canopy_*` / `branch_anchor_*`.
|
||||
const treeSpecs = [
|
||||
{ id: 't1', x: -9, z: 2, phase: 0.7, trunkH: 4.2, anchorY: 3.4 },
|
||||
{ id: 't2', x: 8, z: -2, phase: 2.9, trunkH: 3.8, anchorY: 3.1 },
|
||||
];
|
||||
// (treeSpecs is site.trees — bound at the top of createWorld.)
|
||||
for (const spec of treeSpecs) {
|
||||
const y0 = heightAt(spec.x, spec.z);
|
||||
const tree = new THREE.Group();
|
||||
@ -243,13 +325,14 @@ export function createWorld(scene, opts = {}) {
|
||||
root.add(tree);
|
||||
graybox.trees.set(spec.id, tree);
|
||||
|
||||
// The anchor is at a branch fork, not the canopy centre.
|
||||
anchors.push(makeSwayAnchor(
|
||||
spec.id,
|
||||
new THREE.Vector3(spec.x, y0 + spec.anchorY, spec.z),
|
||||
spec.phase,
|
||||
wind,
|
||||
));
|
||||
// The anchor is at a branch fork, not the canopy centre. One per entry in
|
||||
// the site's list; dress() moves each onto its baked branch_anchor_* node.
|
||||
for (const a of spec.anchors ?? []) {
|
||||
anchors.push(Object.assign(
|
||||
makeSwayAnchor(a.id, new THREE.Vector3(spec.x, y0 + spec.anchorY, spec.z), spec.phase, wind),
|
||||
{ work: a.work },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// --- posts -------------------------------------------------------------
|
||||
@ -271,32 +354,10 @@ export function createWorld(scene, opts = {}) {
|
||||
// be the expensive answer; the small quads buy survival and pay in patchy
|
||||
// shade. That IS the design (DESIGN.md, "big flat low vs small twisted steep").
|
||||
// SPRINT6 gate 1: p4 is the ONE close anchor the balance pass allows, and its
|
||||
// position is measured rather than guessed.
|
||||
//
|
||||
// The wild night had no winnable line because every anchor near the bed —
|
||||
// p1/p2/p3 — is SOUTH of it, and nothing stands north short of the house 10 m
|
||||
// away. So covering rigs had to span the yard and died, while rigs small
|
||||
// enough to survive sat beside the bed instead of over it.
|
||||
//
|
||||
// p4 supplies the missing north-west corner. Swept its position against the
|
||||
// smallest quad that covers 90% of the bed:
|
||||
// (-2.2, -1.2) → 44.4 m² ← breaks the tradeoff (see below)
|
||||
// (-3.2, -1.2) → 48.6 m² ← here
|
||||
// (-3.2, -2.0) → 51.7 m²
|
||||
// (-4.2, -3.0) → 60.0 m²
|
||||
// beyond ~z=-4 → 63.5 m², i.e. too far away to matter at all
|
||||
// Full coverage costs 63.5 m² without it. At (-3.2,-1.2) that drops to 48.6 —
|
||||
// a real new option, 23% cheaper — while still costing you a bigger sail than
|
||||
// the 23-38 m² rigs that survive on their own. Pulling it in to (-2.2,-1.2)
|
||||
// would put full coverage at 44.4 m², INSIDE the survivable band, which
|
||||
// collapses DESIGN.md's central tension: covering the bed has to cost risk.
|
||||
// a.test.js asserts that >45 m² floor, so moving this post inward goes red.
|
||||
const postSpecs = [
|
||||
{ id: 'p1', x: -4.5, z: 5.5, h: 4.0 },
|
||||
{ id: 'p2', x: 4.0, z: 6.0, h: 4.0 },
|
||||
{ id: 'p3', x: 0, z: 7.0, h: 4.0 },
|
||||
{ id: 'p4', x: -3.2, z: -1.2, h: 4.0 },
|
||||
];
|
||||
// position is measured rather than guessed — and the measurement moved with
|
||||
// the data. p4's swept position table now lives in backyard_01.json's `_why`,
|
||||
// beside the coordinate it justifies, because that is the number someone will
|
||||
// be tempted to nudge. (postSpecs is site.posts, bound at the top.)
|
||||
const RAKE = (8 * Math.PI) / 180;
|
||||
for (const spec of postSpecs) {
|
||||
const y0 = heightAt(spec.x, spec.z);
|
||||
@ -330,7 +391,10 @@ export function createWorld(scene, opts = {}) {
|
||||
footing.receiveShadow = true;
|
||||
root.add(footing);
|
||||
|
||||
anchors.push(makeStaticAnchor(spec.id, 'post', top));
|
||||
anchors.push(Object.assign(
|
||||
makeStaticAnchor(spec.id, spec.type ?? 'post', top),
|
||||
{ work: spec.work },
|
||||
));
|
||||
}
|
||||
|
||||
// --- garden bed (the thing you are protecting) -------------------------
|
||||
@ -370,11 +434,16 @@ export function createWorld(scene, opts = {}) {
|
||||
fence.name = 'fence';
|
||||
const railMat = new THREE.MeshStandardMaterial({ color: 0x7a6a4f, roughness: 1 });
|
||||
const hx = YARD.width / 2, hz = YARD.depth / 2;
|
||||
const runs = [
|
||||
{ from: [-hx, hz], to: [hx, hz] }, // south
|
||||
{ from: [-hx, -hz], to: [-hx, hz] }, // west
|
||||
{ from: [hx, -hz], to: [hx, hz] }, // east
|
||||
];
|
||||
const SIDES = {
|
||||
south: { from: [-hx, hz], to: [hx, hz] },
|
||||
west: { from: [-hx, -hz], to: [-hx, hz] },
|
||||
east: { from: [hx, -hz], to: [hx, hz] },
|
||||
north: { from: [-hx, -hz], to: [hx, -hz] },
|
||||
};
|
||||
// backyard_01 fences south/west/east — the house IS its north boundary. The
|
||||
// corner block has a street on two sides and fences the other two, which is
|
||||
// the whole reason this is a site field rather than three hardcoded runs.
|
||||
const runs = (site.fence?.sides ?? ['south', 'west', 'east']).map((s) => SIDES[s]).filter(Boolean);
|
||||
for (const run of runs) {
|
||||
const [x0, z0] = run.from, [x1, z1] = run.to;
|
||||
const len = Math.hypot(x1 - x0, z1 - z0);
|
||||
@ -466,6 +535,7 @@ export function createWorld(scene, opts = {}) {
|
||||
};
|
||||
|
||||
const load = async (name) => {
|
||||
if (!name) return null; // the site simply doesn't have one
|
||||
try {
|
||||
const gltf = await loader.loadAsync(new URL(`../models/${name}.glb`, import.meta.url).href);
|
||||
gltf.scene.traverse((o) => {
|
||||
@ -482,11 +552,13 @@ export function createWorld(scene, opts = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const [shed, table, houseGlb, tree1, tree2, bedGlb, gnome] = await Promise.all([
|
||||
load('shed_01_v1'), load('shed_table_v1'), load('house_yardside_v1'),
|
||||
load('tree_gum_01_v1'), load('tree_gum_02_v1'), load('garden_bed_v1'),
|
||||
load('garden_gnome_01_v1'),
|
||||
// Model names come off the site now. `load(null)` resolves null, so a site
|
||||
// that simply has no shed (site_02 has a carport) skips it without a branch.
|
||||
const [shed, table, houseGlb, bedGlb, gnome] = await Promise.all([
|
||||
load(SHED?.model), load(SHED_TABLE?.model), load(HOUSE?.model),
|
||||
load(site.gardenBedModel ?? 'garden_bed_v1'), load(GNOME?.model),
|
||||
]);
|
||||
const treeGlbs = await Promise.all(treeSpecs.map((s) => load(s.model)));
|
||||
|
||||
// --- garden bed ------------------------------------------------------
|
||||
if (bedGlb) {
|
||||
@ -524,9 +596,7 @@ export function createWorld(scene, opts = {}) {
|
||||
root.add(houseGlb);
|
||||
solids.push(houseGlb);
|
||||
houseGlb.updateWorldMatrix(true, true);
|
||||
for (const [i, id] of ['h1', 'h2', 'h3'].entries()) {
|
||||
adoptAnchor(houseGlb, `fascia_anchor_0${i + 1}`, id);
|
||||
}
|
||||
for (const a of HOUSE.anchors ?? []) adoptAnchor(houseGlb, a.node, a.id);
|
||||
}
|
||||
|
||||
// --- trees -----------------------------------------------------------
|
||||
@ -534,7 +604,8 @@ export function createWorld(scene, opts = {}) {
|
||||
// the fork, 0.76 out where the limb is thin) — the intel DESIGN.md wants
|
||||
// inspection to buy. branch_anchor_01 keeps the original t1/t2 id so
|
||||
// nothing that already references them breaks; the rest are added.
|
||||
for (const [glb, spec] of [[tree1, treeSpecs[0]], [tree2, treeSpecs[1]]]) {
|
||||
for (const [i, spec] of treeSpecs.entries()) {
|
||||
const glb = treeGlbs[i];
|
||||
if (!glb) continue;
|
||||
const old = graybox.trees.get(spec.id);
|
||||
retire(old);
|
||||
@ -552,20 +623,13 @@ export function createWorld(scene, opts = {}) {
|
||||
canopies.push({ group: canopy.parent, phase: spec.phase, base: canopy.parent.rotation.clone() });
|
||||
}
|
||||
|
||||
// Every branch anchor the site names now EXISTS before dress() runs (they
|
||||
// were created off spec.anchors above), so this only has to move them onto
|
||||
// the nodes Lane E baked. That deleted the old suffix-guessing loop, which
|
||||
// built t1/t1b/t1c out of an array index and could only ever describe the
|
||||
// two trees it was written for.
|
||||
glb.updateWorldMatrix(true, true);
|
||||
const suffix = ['', 'b', 'c'];
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const node = glb.getObjectByName(`branch_anchor_0${i}`);
|
||||
if (!node) continue;
|
||||
const id = spec.id + suffix[i - 1];
|
||||
if (i === 1) adoptAnchor(glb, `branch_anchor_01`, id);
|
||||
else {
|
||||
const p = new THREE.Vector3().setFromMatrixPosition(node.matrixWorld);
|
||||
const a = makeSwayAnchor(id, p, spec.phase, wind);
|
||||
a.ratingHint = node.userData?.rating_hint ?? 1;
|
||||
anchors.push(a);
|
||||
}
|
||||
}
|
||||
for (const a of spec.anchors ?? []) adoptAnchor(glb, a.node, a.id);
|
||||
}
|
||||
|
||||
if (shed) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user