fix(store): WowPlatter coordinate convention in webstore renderer
- racks/portals/camera corner-relative (worldX = -W/2 + x); attach_wall snap + clamp - crates parent to their rack_level (slot-grid or local pos offset), facing composed on rack - free crates / decals / lights stay centered - fixes the corner pile-up; booth-room overhead render verified Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fb533a7701
commit
0767778931
@ -50,6 +50,11 @@ Hierarchy: **Space → Racks(positioned) → Levels(`rack_level_index`) → Crat
|
||||
Genre/style: `virtual_rack.genre_ids`/`style_ids` are comma-sep id lists; crate-level tags via a tag table
|
||||
(WowPlatter `wowplatter_get_crate_tags`). Confirm the migrated tag table name in P2.
|
||||
|
||||
**Props vs stock (important, every phase):** `virtual_rack_type.rack_purpose` ∈ `stock` (real record racks)
|
||||
or `prop` (3D scenery — COLUMNs, AIRCON, STATION, DOOR, couch, trolley…). The functional navigator must
|
||||
**filter `rack_purpose <> 'prop'`** everywhere (store-layout already does). `type` ∈ rack/bench/shelf/'' is the
|
||||
geometry kind, NOT the stock/prop distinction — don't filter on `type`.
|
||||
|
||||
---
|
||||
|
||||
## 3. The geometry, decoded (the tricky x/y/z)
|
||||
|
||||
@ -36,6 +36,9 @@ import { createDig } from './dig.js';
|
||||
|
||||
const num = (v, d = 0) => (v == null || isNaN(+v) ? d : +v);
|
||||
const rad = THREE.MathUtils.degToRad;
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
// WowPlatter facing convention (utils.js directionYaw): front/forward=0, back=π, left=−π/2, right=+π/2
|
||||
const dirYaw = d => { d = String(d || '').toLowerCase(); return d === 'back' ? Math.PI : d === 'left' ? -Math.PI / 2 : d === 'right' ? Math.PI / 2 : 0; };
|
||||
const stat = document.getElementById('stat');
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('c'), antialias: true });
|
||||
@ -185,25 +188,75 @@ function buildScene(data) {
|
||||
});
|
||||
if (!(data.lights || []).length) { const d = new THREE.DirectionalLight(0xffffff, 0.7); d.position.set(3, 6, 2); roomGroup.add(d); }
|
||||
|
||||
// racks — proper shelving (bench / rack / cupboard) from rack_type + its levels
|
||||
// racks + crates — WowPlatter coordinate convention (racks.js / crates.js):
|
||||
// • racks: pos_x/pos_z are CORNER-relative (0..room) → centered scene space; facing = rack.direction + rotation_y; attach_wall snaps to a wall
|
||||
// • rack crates: CHILD of the rack, dropped into their rack_level's slot grid (cell 0.34m, row-major); facing = crate.direction + rotation_y (composes on the rack's)
|
||||
// • free crates (no rack): pos_x/pos_z are CENTERED, like decals/lights
|
||||
const rackTypes = Object.fromEntries((data.rack_types || []).map(t => [t.id, t]));
|
||||
const crateTypes = Object.fromEntries((data.crate_types || []).map(t => [t.id, t]));
|
||||
const levelsByType = {}; (data.rack_type_levels || []).forEach(l => { (levelsByType[l.rack_type_id] ||= []).push(l); });
|
||||
const frontByCrate = Object.fromEntries((data.records || []).filter(r => r.thumb).map(r => [r.crate_id, r]));
|
||||
const cratesByRack = {}; (data.crates || []).forEach(c => { if (c.rack_id != null) (cratesByRack[c.rack_id] ||= []).push(c); });
|
||||
|
||||
function placeCrateOnRack(rackGroup, levels, rackW, rackD, rackTopY, cr) {
|
||||
const t = crateTypes[cr.crate_type_id] || {};
|
||||
const cw = num(t.width, 0.34), ch = num(t.height, 0.2), cd = num(t.depth, 0.53);
|
||||
const lvl = levels.find(l => cr.rack_type_level_id != null && +l.id === +cr.rack_type_level_id)
|
||||
|| levels.find(l => cr.rack_level_index != null && +l.level_index === +cr.rack_level_index)
|
||||
|| levels[0] || null;
|
||||
const levelW = lvl ? (num(lvl.width, 0) || rackW) : rackW;
|
||||
const levelD = lvl ? (num(lvl.depth, 0) || rackD) : rackD;
|
||||
const levelY = lvl ? num(lvl.y_pos, 0) : rackTopY; // shelf-surface height above floor
|
||||
let lx, lz;
|
||||
if (cr.slot_number != null && +cr.slot_number > 0) { // slot → grid cell
|
||||
const cell = cw > 0.1 ? cw : 0.34;
|
||||
const cols = Math.max(1, Math.floor(levelW / cell)), rows = Math.max(1, Math.round(levelD / cell));
|
||||
const csx = levelW / cols, csz = levelD / rows, si = +cr.slot_number - 1;
|
||||
lx = -levelW / 2 + csx / 2 + (si % cols) * csx;
|
||||
lz = -levelD / 2 + csz / 2 + Math.floor(si / cols) * csz;
|
||||
} else { lx = num(cr.pos_x); lz = num(cr.pos_z); }
|
||||
lx = clamp(lx, -(levelW / 2 - cw / 2), levelW / 2 - cw / 2);
|
||||
lz = clamp(lz, -(levelD / 2 - cd / 2), levelD / 2 - cd / 2);
|
||||
const g = buildCrate(t, frontByCrate[cr.id]);
|
||||
g.position.set(lx, levelY + ch / 2, lz);
|
||||
g.rotation.set(rad(num(cr.rotation_x)), dirYaw(cr.direction) + rad(num(cr.rotation_y)), rad(num(cr.rotation_z)));
|
||||
g.userData = { crateId: cr.id, name: cr.label_text || cr.name };
|
||||
rackGroup.add(g); crateMeshes.push(g);
|
||||
}
|
||||
|
||||
(data.racks || []).forEach(rk => {
|
||||
const t = rackTypes[rk.rack_type_id] || {};
|
||||
const g = buildRack(t, levelsByType[rk.rack_type_id] || []);
|
||||
g.position.set(num(rk.pos_x), num(rk.pos_y, num(t.height, 0.9) / 2), num(rk.pos_z));
|
||||
g.rotation.set(rad(num(rk.rotation_x)), rad(num(rk.rotation_y)), rad(num(rk.rotation_z)));
|
||||
roomGroup.add(g);
|
||||
const w = num(rk.width, num(t.width, 1)), h = num(rk.height, num(t.height, 0.9)), dp = num(rk.depth, num(t.depth, 0.5));
|
||||
const halfX = w / 2, halfZ = dp / 2, rpx = num(rk.pos_x), rpz = num(rk.pos_z);
|
||||
let yaw = dirYaw(rk.direction) + rad(num(rk.rotation_y));
|
||||
const attach = String(rk.attach_wall || '').toLowerCase();
|
||||
let cx, cz;
|
||||
if (attach === 'north') { yaw = rad(num(rk.rotation_y)); cz = -D / 2 + halfZ + Math.max(0, rpz); cx = -W / 2 + halfX + rpx; }
|
||||
else if (attach === 'south') { yaw = Math.PI + rad(num(rk.rotation_y)); cz = D / 2 - halfZ - Math.max(0, rpz); cx = -W / 2 + halfX + rpx; }
|
||||
else if (attach === 'east') { yaw = -Math.PI / 2 + rad(num(rk.rotation_y)); cx = W / 2 - halfZ - Math.max(0, rpz); cz = -D / 2 + halfX + rpx; }
|
||||
else if (attach === 'west') { yaw = Math.PI / 2 + rad(num(rk.rotation_y)); cx = -W / 2 + halfZ + Math.max(0, rpz); cz = -D / 2 + halfX + rpx; }
|
||||
else { cx = -W / 2 + rpx + halfX * Math.cos(yaw) - halfZ * Math.sin(yaw); cz = -D / 2 + rpz + halfX * Math.sin(yaw) + halfZ * Math.cos(yaw); }
|
||||
const wallish = attach === 'east' || attach === 'west';
|
||||
cx = clamp(cx, -W / 2 + (wallish ? halfZ : halfX), W / 2 - (wallish ? halfZ : halfX));
|
||||
cz = clamp(cz, -D / 2 + (wallish ? halfX : halfZ), D / 2 - (wallish ? halfX : halfZ));
|
||||
|
||||
const rackGroup = new THREE.Group();
|
||||
rackGroup.position.set(cx, num(rk.pos_y, 0), cz);
|
||||
rackGroup.rotation.set(rad(num(rk.rotation_x)), yaw, rad(num(rk.rotation_z)));
|
||||
const levels = levelsByType[rk.rack_type_id] || [];
|
||||
const geo = buildRack({ ...t, width: w, height: h, depth: dp }, levels);
|
||||
geo.position.y = h / 2; rackGroup.add(geo); // bottom on the floor
|
||||
roomGroup.add(rackGroup);
|
||||
(cratesByRack[rk.id] || []).forEach(cr => placeCrateOnRack(rackGroup, levels, w, dp, h, cr));
|
||||
});
|
||||
|
||||
// crates — open record bins (floor + short front + sloped sides) with the front cover laid in
|
||||
const crateTypes = Object.fromEntries((data.crate_types || []).map(t => [t.id, t]));
|
||||
const frontByCrate = Object.fromEntries((data.records || []).filter(r => r.thumb).map(r => [r.crate_id, r]));
|
||||
(data.crates || []).forEach(cr => {
|
||||
// free-standing crates (no rack) — centered coords like decals/lights
|
||||
(data.crates || []).filter(c => c.rack_id == null).forEach(cr => {
|
||||
const t = crateTypes[cr.crate_type_id] || {};
|
||||
const cw = num(t.width, 0.34), cd = num(t.depth, 0.53);
|
||||
const g = buildCrate(t, frontByCrate[cr.id]);
|
||||
g.position.set(num(cr.pos_x), num(cr.pos_y, num(t.height, 0.2) / 2), num(cr.pos_z));
|
||||
g.rotation.set(rad(num(cr.rotation_x)), rad(num(cr.rotation_y)), rad(num(cr.rotation_z)));
|
||||
g.position.set(clamp(num(cr.pos_x), -W / 2 + cw / 2, W / 2 - cw / 2), num(cr.pos_y, num(t.height, 0.2) / 2), clamp(num(cr.pos_z), -D / 2 + cd / 2, D / 2 - cd / 2));
|
||||
g.rotation.set(rad(num(cr.rotation_x)), dirYaw(cr.direction) + rad(num(cr.rotation_y)), rad(num(cr.rotation_z)));
|
||||
g.userData = { crateId: cr.id, name: cr.label_text || cr.name };
|
||||
roomGroup.add(g); crateMeshes.push(g);
|
||||
});
|
||||
@ -232,14 +285,15 @@ function buildScene(data) {
|
||||
const { tx } = textTexture(p.label || 'door', { color: '#fff', bg: '#ff5db1', font: 48 });
|
||||
const mat = new THREE.MeshBasicMaterial({ map: tx, transparent: true, opacity: 0.82, side: THREE.DoubleSide });
|
||||
const m = new THREE.Mesh(new THREE.PlaneGeometry(w, h), mat);
|
||||
m.position.set(num(p.x), h / 2, num(p.z));
|
||||
const wx = -W / 2 + num(p.x), wz = -D / 2 + num(p.z); // portal x/z are corner-relative
|
||||
m.position.set(wx, h / 2, wz);
|
||||
m.rotation.y = rad(num(p.facing_deg));
|
||||
roomGroup.add(m);
|
||||
if (p.to_space) portals.push({ x: num(p.x), z: num(p.z), to: p.to_space, label: p.label });
|
||||
if (p.to_space) portals.push({ x: wx, z: wz, to: p.to_space, label: p.label });
|
||||
});
|
||||
|
||||
// spawn at the configured camera position if present
|
||||
if (sp.camera_pos_x != null) camera.position.set(num(sp.camera_pos_x), num(sp.camera_pos_y, 1.6), num(sp.camera_pos_z, 3));
|
||||
// spawn at the configured camera position (corner-relative) if present
|
||||
if (sp.camera_pos_x != null) camera.position.set(-W / 2 + num(sp.camera_pos_x), num(sp.camera_pos_y, 1.6), -D / 2 + num(sp.camera_pos_z));
|
||||
stat.innerHTML = `<b>${sp.name || 'store'}</b> · ${(data.racks||[]).length} racks · ${(data.crates||[]).length} crates · ${(data.records||[]).length} covers`;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user