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 = `
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 = `