web/world/editor.html + js/editor.js. Loads any existing site or an empty template and renders the REAL dressed world through the same loadSite/createWorld/dress path the game boots -- not a diagram, and no second renderer to keep in sync. Place/drag/delete on the ground plane by raycast, writing the SAME named-key site-JSON shapes world.js already reads (the gnome pattern; there is deliberately no generic props[] and the editor does not invent one). Live validateSite into a panel, canonical stable-key-order export with a loud _INVALID key, visible _design/_why authoring fields, and an export panel that says export != shipped. Published the DOM seam contract in editor.html's header so B's SCORE IT and C's wind authoring can start against it immediately -- mountPanel + a class kit, the way E's .letterhead kit worked in Sprint 12. Two rendering bugs in world.js that the editor surfaced in its first ten minutes, both verified by looking at the yard rather than by reasoning: - the graybox house was built unconditionally and dress() only retires it when a house GLB loads, so site_02_corner_block -- which declares no house -- has been shipping a 16x3x6 m grey slab across night 3's whole north horizon, in solids, on the public URL. - SHED alone was bound raw while SHED_TABLE/GNOME/BIKE all get their degrees converted, so SHED.rotY was undefined, the shed's quaternion was all NaN, and three.js silently declines to draw a NaN world matrix. The Hendersons' shed has never rendered; ladder.js has been leaning the ladder on thin air. Also: createWorld threw on a site with no shedTable (unreachable for both shipped sites, reached by the editor template on its first frame). Now null, which every consumer already guards and which CONTRACT.world permits. Selftest 362/0/0. Asserts for the two fixes land with the round-trip test in the next commit -- pushing now because B and C are blocked on the seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
217 lines
8.9 KiB
HTML
217 lines
8.9 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>HARD YARDS — yard editor</title>
|
|
<!--
|
|
The yard editor. Lane A owns this file (SPRINT14 gate 1).
|
|
|
|
It lives in web/ and therefore DEPLOYS with the game, and that is intended:
|
|
site JSON carries no secrets, and a public "build a yard" toy is a feature.
|
|
Nothing here is loaded by index.html — the game never imports editor.js.
|
|
|
|
── THE DOM SEAM CONTRACT (Lane B's SCORE IT, Lane C's wind authoring) ────────
|
|
B and C build INTO this page. Everything they need is below and in editor.js;
|
|
the same way E's invoice kit got .letterhead/.brief/.jobsheet in SPRINT12,
|
|
this page publishes classes and mount points so three lanes can edit one UI
|
|
without editing one file.
|
|
|
|
MOUNT: never append to #ed-side by hand. Call
|
|
|
|
const { root, body } = EDITOR.mountPanel({ id: 'score', title: 'SCORE IT', order: 40 });
|
|
|
|
and fill `body`. `order` fixes the rail's vertical order across lanes so two
|
|
lanes landing in the same sprint can't fight over it. Reserved orders:
|
|
10 site 20 palette 30 inspector 40 SCORE IT (B) 50 wind (C)
|
|
80 validation 90 export
|
|
Calling mountPanel twice with one id returns the SAME panel (idempotent), so a
|
|
re-render is a re-fill, not a duplicate.
|
|
|
|
CLASSES (the contract — style with these, don't invent):
|
|
.ed-panel .ed-panel-title .ed-panel-body panel chrome (mountPanel makes these)
|
|
.ed-row one label+control line
|
|
.ed-label the label half of a row
|
|
.ed-btn .ed-btn.primary .ed-btn.danger buttons
|
|
.ed-num .ed-sel .ed-text number / select / text+textarea
|
|
.ed-card .ed-card-head .ed-card-row .ed-kv a RESULT card (B's score goes here)
|
|
.ed-ok .ed-warn .ed-err verdict colouring, on any element
|
|
.ed-note muted small print
|
|
.ed-tag inline pill (a flag, a count)
|
|
Anything you need that isn't here: add it, and say so in THREADS — a class
|
|
that only one lane knows about is the thing this contract exists to prevent.
|
|
|
|
The scene seams (three.js side) are on the EDITOR object — see editor.js's
|
|
header for the full API. The short version C needs:
|
|
EDITOR.overlay a THREE.Group, cleared on every rebuild — gizmos live here
|
|
EDITOR.raycastGround(ev) → {x,y,z}|null
|
|
EDITOR.registerTool({...}) / EDITOR.setTool(id)
|
|
EDITOR.site / EDITOR.markDirty() read+mutate the live site, then say so
|
|
EDITOR.on('change'|'rebuild'|'select', fn)
|
|
-->
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
--bg: #10161b;
|
|
--panel: #172129;
|
|
--panel2: #1d2831;
|
|
--line: #2b3a45;
|
|
--ink: #dde5ea;
|
|
--dim: #8598a5;
|
|
--accent: #7ee0ff;
|
|
--ok: #7fce6a;
|
|
--warn: #ffc46b;
|
|
--err: #ff6b6b;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body {
|
|
margin: 0; height: 100%; overflow: hidden;
|
|
background: var(--bg); color: var(--ink);
|
|
font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
}
|
|
#ed-root { display: flex; height: 100%; }
|
|
|
|
/* --- viewport ---------------------------------------------------------- */
|
|
#ed-viewport { position: relative; flex: 1 1 auto; min-width: 0; }
|
|
#ed-canvas { display: block; width: 100%; height: 100%; }
|
|
#ed-hint {
|
|
position: absolute; left: 10px; bottom: 10px; margin: 0;
|
|
color: #fff; text-shadow: 0 1px 3px #000; white-space: pre; pointer-events: none;
|
|
opacity: .85;
|
|
}
|
|
#ed-readout {
|
|
position: absolute; right: 10px; top: 10px; margin: 0; text-align: right;
|
|
color: #fff; text-shadow: 0 1px 3px #000; white-space: pre; pointer-events: none;
|
|
}
|
|
#ed-toolbar {
|
|
position: absolute; left: 10px; top: 10px; display: flex; gap: 6px; flex-wrap: wrap;
|
|
max-width: calc(100% - 220px);
|
|
}
|
|
|
|
/* --- side rail --------------------------------------------------------- */
|
|
#ed-side {
|
|
flex: 0 0 340px; height: 100%; overflow-y: auto; overflow-x: hidden;
|
|
background: var(--panel); border-left: 1px solid var(--line);
|
|
}
|
|
#ed-side::-webkit-scrollbar { width: 9px; }
|
|
#ed-side::-webkit-scrollbar-thumb { background: #2f3f4b; border-radius: 5px; }
|
|
|
|
/* --- the class contract ------------------------------------------------ */
|
|
.ed-panel { border-bottom: 1px solid var(--line); }
|
|
.ed-panel-title {
|
|
margin: 0; padding: 9px 12px; font-size: 11px; font-weight: 700;
|
|
letter-spacing: .14em; color: var(--accent); background: var(--panel2);
|
|
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
cursor: pointer; user-select: none;
|
|
}
|
|
.ed-panel-title::after { content: '▾'; color: var(--dim); font-size: 10px; }
|
|
.ed-panel.collapsed .ed-panel-title::after { content: '▸'; }
|
|
.ed-panel.collapsed .ed-panel-body { display: none; }
|
|
.ed-panel-body { padding: 10px 12px 12px; }
|
|
|
|
.ed-row { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; }
|
|
.ed-row:last-child { margin-bottom: 0; }
|
|
.ed-label { flex: 0 0 84px; color: var(--dim); }
|
|
.ed-row > .ed-num, .ed-row > .ed-sel, .ed-row > .ed-text { flex: 1 1 auto; min-width: 0; }
|
|
|
|
.ed-btn {
|
|
font: inherit; padding: 4px 9px; border-radius: 4px; cursor: pointer;
|
|
background: #24313b; color: var(--ink); border: 1px solid var(--line);
|
|
}
|
|
.ed-btn:hover { background: #2d3d49; }
|
|
.ed-btn:active { transform: translateY(1px); }
|
|
.ed-btn.on { background: #1d4a5c; border-color: #3d7f96; color: #cdf3ff; }
|
|
.ed-btn.primary { background: #1f5a34; border-color: #2f7d49; color: #d6ffdf; }
|
|
.ed-btn.primary:hover { background: #266c3e; }
|
|
.ed-btn.danger { background: #58211f; border-color: #7d2b2b; color: #ffd9d6; }
|
|
.ed-btn[disabled] { opacity: .45; cursor: not-allowed; }
|
|
|
|
.ed-num, .ed-sel, .ed-text {
|
|
font: inherit; padding: 3px 6px; border-radius: 4px;
|
|
background: #0e1418; color: var(--ink); border: 1px solid var(--line);
|
|
}
|
|
.ed-num { width: 74px; }
|
|
textarea.ed-text { width: 100%; resize: vertical; min-height: 62px; line-height: 1.5; }
|
|
|
|
.ed-card {
|
|
border: 1px solid var(--line); border-radius: 5px; background: var(--panel2);
|
|
padding: 8px 10px; margin: 0 0 8px;
|
|
}
|
|
.ed-card-head {
|
|
font-weight: 700; letter-spacing: .06em; margin: 0 0 6px;
|
|
display: flex; justify-content: space-between; gap: 8px;
|
|
}
|
|
.ed-card-row { display: flex; justify-content: space-between; gap: 10px; padding: 1px 0; }
|
|
.ed-kv { color: var(--dim); }
|
|
|
|
.ed-ok { color: var(--ok); }
|
|
.ed-warn { color: var(--warn); }
|
|
.ed-err { color: var(--err); }
|
|
.ed-note { color: var(--dim); font-size: 11px; line-height: 1.5; }
|
|
.ed-tag {
|
|
display: inline-block; padding: 0 5px; border-radius: 3px; font-size: 10px;
|
|
background: #24313b; border: 1px solid var(--line); color: var(--dim);
|
|
}
|
|
.ed-tag.ed-err { background: #3a1618; border-color: #7d2b2b; }
|
|
.ed-tag.ed-ok { background: #12321c; border-color: #2c6b3c; }
|
|
|
|
ul.ed-list { margin: 0; padding: 0; list-style: none; }
|
|
ul.ed-list li {
|
|
padding: 3px 6px; border-radius: 3px; cursor: pointer;
|
|
display: flex; justify-content: space-between; gap: 8px;
|
|
}
|
|
ul.ed-list li:hover { background: #22303a; }
|
|
ul.ed-list li.sel { background: #1d4a5c; color: #cdf3ff; }
|
|
|
|
#ed-boot-error {
|
|
position: absolute; inset: 0; display: none; padding: 28px;
|
|
background: #10161b; color: var(--err); white-space: pre-wrap; overflow: auto;
|
|
}
|
|
</style>
|
|
<!--
|
|
Required. world.js's dress() pulls GLTFLoader, which imports the BARE
|
|
specifier 'three'. Without this map the throw is swallowed by dress()'s
|
|
per-asset try/catch and every rating_hint silently becomes 1.0 — the
|
|
invincible-house gotcha in docs/MANUAL.md. It has cost this repo a day; do
|
|
not remove it from any page that dresses a yard.
|
|
-->
|
|
<script type="importmap">
|
|
{ "imports": { "three": "./vendor/three.module.js", "three/addons/": "./vendor/addons/" } }
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="ed-root">
|
|
<div id="ed-viewport">
|
|
<canvas id="ed-canvas"></canvas>
|
|
<div id="ed-toolbar"></div>
|
|
<pre id="ed-readout"></pre>
|
|
<pre id="ed-hint"></pre>
|
|
<div id="ed-boot-error"></div>
|
|
</div>
|
|
<div id="ed-side"></div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import { createEditor } from './js/editor.js';
|
|
|
|
try {
|
|
await createEditor({
|
|
canvas: document.getElementById('ed-canvas'),
|
|
side: document.getElementById('ed-side'),
|
|
toolbar: document.getElementById('ed-toolbar'),
|
|
readout: document.getElementById('ed-readout'),
|
|
hint: document.getElementById('ed-hint'),
|
|
});
|
|
} catch (err) {
|
|
// The editor is a tool, so it fails LOUD and on the glass. index.html's boot
|
|
// failure writes into a hidden #dev div and shows a stranger a blank page
|
|
// (SPRINT14 pool item) — this page does not repeat that.
|
|
const box = document.getElementById('ed-boot-error');
|
|
box.style.display = 'block';
|
|
box.textContent = `EDITOR BOOT FAILED\n\n${err && err.stack ? err.stack : err}`;
|
|
throw err;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|