Layout editor: table dimensions — orange outline, cm labels, Table panel
This commit is contained in:
@@ -39,6 +39,7 @@ app.innerHTML = `${nav('layout')}
|
||||
<button data-add="building">+ Building</button>
|
||||
<button data-add="spawn">+ Spawn</button>
|
||||
<button data-add="path">+ Path</button>
|
||||
<button id="tableBtn">Table…</button>
|
||||
<button id="topView">Top view</button>
|
||||
<button onclick="location.href='/admin/plan.html'">Print plan ↗</button>
|
||||
<span style="flex:1"></span>
|
||||
@@ -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 = '<em style="opacity:.6">Tap an object to select it, then drag the gizmo arrows (X red, Y green = height, Z blue). All values in cm.</em>'; return; }
|
||||
if (sel.kind === 'table') {
|
||||
let h = `<h2>table dimensions</h2>`;
|
||||
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 += `<p class="badge">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.</p>`;
|
||||
side.innerHTML = h;
|
||||
return;
|
||||
}
|
||||
let h = `<h2>${sel.kind} ${sel.index}</h2>`;
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user