From e23c308249f8caee9e92595bacbe28a5c9c6aaa5 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Thu, 23 Jul 2026 15:21:07 +1000 Subject: [PATCH] =?UTF-8?q?Layout=20editor:=20table=20dimensions=20?= =?UTF-8?q?=E2=80=94=20orange=20outline,=20cm=20labels,=20Table=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/admin/layout.html | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/public/admin/layout.html b/public/admin/layout.html index e8925e8..b811d31 100644 --- a/public/admin/layout.html +++ b/public/admin/layout.html @@ -39,6 +39,7 @@ app.innerHTML = `${nav('layout')} + @@ -58,6 +59,7 @@ app.innerHTML = `${nav('layout')} let scene = await api('/api/scene'); scene.anchors ||= []; scene.buildings ||= []; scene.spawns ||= []; scene.paths ||= []; +scene.table = Object.assign({ width: 120, depth: 100, offsetX: 0, offsetZ: 0, show: true }, scene.table || {}); // ---------- three.js setup ---------- const cvs = document.getElementById('gl'); @@ -108,8 +110,38 @@ const matSpawn = new THREE.MeshStandardMaterial({ color: 0xb57f0b }); const matPath = new THREE.LineBasicMaterial({ color: 0xc77dff }); const matPathPt = new THREE.MeshStandardMaterial({ color: 0xc77dff }); +const tableGroup = new THREE.Group(); scn.add(tableGroup); +function buildTable() { + tableGroup.clear(); + const t = scene.table; + if (!t || t.show === false) return; + const x0 = t.offsetX || 0, z0 = t.offsetZ || 0, w = t.width, d = t.depth; + // surface tint + const top = new THREE.Mesh(new THREE.PlaneGeometry(w, d), + new THREE.MeshBasicMaterial({ color: 0xffb84d, transparent: true, opacity: 0.07, side: THREE.DoubleSide, depthWrite: false })); + top.rotation.x = -Math.PI / 2; + top.position.set(x0 + w / 2, -0.2, z0 + d / 2); + tableGroup.add(top); + // edge outline + const pts = [[x0, z0], [x0 + w, z0], [x0 + w, z0 + d], [x0, z0 + d], [x0, z0]] + .map(([x, z]) => new THREE.Vector3(x, 0.3, z)); + tableGroup.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), + new THREE.LineBasicMaterial({ color: 0xffb84d }))); + // corner posts + for (const [x, z] of [[x0, z0], [x0 + w, z0], [x0 + w, z0 + d], [x0, z0 + d]]) { + const c = new THREE.Mesh(new THREE.CylinderGeometry(1, 1, 3, 8), + new THREE.MeshBasicMaterial({ color: 0xffb84d })); + c.position.set(x, 1.5, z); + tableGroup.add(c); + } + // dimension labels: width along the front (S) edge, depth along the left (W) edge + const wl = textSprite(`${w} cm`, '#ffb84d'); wl.position.set(x0 + w / 2, 5, z0 - 7); tableGroup.add(wl); + const dl2 = textSprite(`${d} cm`, '#ffb84d'); dl2.position.set(x0 - 9, 5, z0 + d / 2); tableGroup.add(dl2); +} + function buildAll() { gizmo.detach(); + buildTable(); objRoot.clear(); scene.anchors.forEach((a, i) => { const size = (a.sizeMM || 60) / 10; // cm @@ -176,12 +208,14 @@ function buildAll() { } function dataOf(s) { + if (s.kind === 'table') return scene.table; if (s.kind === 'pathpoint') return scene.paths[s.index]; return ({ anchor: scene.anchors, building: scene.buildings, spawn: scene.spawns })[s.kind][s.index]; } function reselect() { if (!sel) return; + if (sel.kind === 'table') return; const o = objRoot.children.find(c => c.userData.kind === sel.kind && c.userData.index === sel.index && (sel.sub === undefined || c.userData.sub === sel.sub)); if (o) { sel.obj3d = o; gizmo.attach(o); } else { sel = null; gizmo.detach(); } } @@ -264,6 +298,20 @@ function panel(rebuild = true) { if (!rebuild) return; const o = sel && dataOf(sel); if (!o) { side.innerHTML = 'Tap an object to select it, then drag the gizmo arrows (X red, Y green = height, Z blue). All values in cm.'; return; } + if (sel.kind === 'table') { + let h = `

table dimensions

`; + h += field('width (cm)', o.width, v => o.width = v, { step: 1 }); + h += field('depth (cm)', o.depth, v => o.depth = v, { step: 1 }); + h += field('offset x', o.offsetX || 0, v => o.offsetX = v, { step: 1 }); + h += field('offset z', o.offsetZ || 0, v => o.offsetZ = v, { step: 1 }); + h += field('show', o.show === false ? 'no' : 'yes', v => o.show = v === 'yes', { select: ['yes', 'no'] }); + h += `

Orange outline = your physical table, measured from the world origin + (front-left corner, N = +Z = back). Width runs W→E, depth runs S→N. Offsets shift the + table if your origin isn't at its corner. Saved with the layout; also printed on the + placement plan so you can sanity-check everything fits before taping down.

`; + side.innerHTML = h; + return; + } let h = `

${sel.kind} ${sel.index}

`; if (sel.kind === 'anchor') { h += field('marker ID', o.markerId, v => o.markerId = v | 0, { step: 1 }); @@ -335,6 +383,9 @@ function wirePathExtras() { } // ---------- toolbar ---------- +document.getElementById('tableBtn').onclick = () => { + sel = { kind: 'table' }; gizmo.detach(); panel(); +}; document.getElementById('topView').onclick = () => { cam.position.set(orbit.target.x, 320, orbit.target.z + 0.01); cam.lookAt(orbit.target);