From 44517d4930c991e7157dcafa607e5a1fb5f28c00 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 27 Jun 2026 12:23:18 +1000 Subject: [PATCH] fix(3d): decals snap to their preferred_wall (ported WowPlatter snapToWallAndOrient) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The store renderer placed every decal at raw pos_x/y/z with raw rotation, ignoring preferred_wall/snap/ object_type — so wall art (pos with one in-plane axis, perpendicular=0, rotation 0) floated in mid-room on the wrong axis. Ported WowPlatter public/js/virtual/decals.js snapToWallAndOrient: a space decal (rotation all-zero or snap=wall) keeps its in-plane axis, the perpendicular axis is pushed flat to its named wall (north z=-D/2 yaw0 · south z=D/2 yawpi · east x=W/2 yaw-pi/2 · west x=-W/2 yaw+pi/2 · floor/ceiling lie flat), yawed to face into the room; non-zero rotation = free manual placement, respected. Wall conventions already matched the room's walls. Fixes booth (31 decals) + entry-hall (6) automatically. Verified the snap math against real data (all land on-axis, in-bounds, facing in). Still TODO: crate_type(5)/rack(4) logo decals (backend only fetches object_type=space; need the face-snap port) + hiphop room has no decals authored. Co-Authored-By: Claude Opus 4.8 --- webstore/index.html | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/webstore/index.html b/webstore/index.html index 3aca9b9..8b50003 100644 --- a/webstore/index.html +++ b/webstore/index.html @@ -262,6 +262,43 @@ function buildScene(data) { }); // decals — wall art / signage as textured planes (image) or canvas text. mesh decals skipped. + // Placement is wall-RELATIVE (ported from WowPlatter public/js/virtual/decals.js snapToWallAndOrient): + // a space decal keeps its in-plane axis, the perpendicular axis is pushed flat to its preferred_wall, + // and the plane is yawed to face into the room (floor/ceiling lie flat). Data has pos with one axis 0 + // + all-zero rotation = "snap me to the wall"; a non-zero rotation or snap≠wall = free manual placement. + const wallOf = p => { p = String(p || '').trim().toLowerCase(); + if (p === 'north' || p === 'front') return 'north'; + if (p === 'south' || p === 'back') return 'south'; + if (p === 'east' || p === 'right') return 'east'; + if (p === 'west' || p === 'left') return 'west'; + if (p === 'floor' || p === 'ceiling') return p; + return null; }; + function placeDecal(m, d) { + const rx = num(d.rotation_x), ry = num(d.rotation_y), rz = num(d.rotation_z); + const isSpace = String(d.object_type || 'space').toLowerCase() === 'space'; + const snapMode = String(d.snap || '').toLowerCase(); + if (!(isSpace && (snapMode === 'wall' || (rx === 0 && ry === 0 && rz === 0)))) { + m.position.set(num(d.pos_x), num(d.pos_y, 1.5), num(d.pos_z)); + m.rotation.set(rad(rx), rad(ry), rad(rz)); return; + } + const eps = 0.001; + let x = num(d.pos_x), y = num(d.pos_y, H * 0.5), z = num(d.pos_z); + y = Math.max(Math.min(y, H - 0.05), 0.05); + let wall = wallOf(d.preferred_wall || d.wall); + if (!wall) { // no wall set → nearest one + const a = [['north', Math.abs(z + D / 2)], ['south', Math.abs(z - D / 2)], + ['east', Math.abs(x - W / 2)], ['west', Math.abs(x + W / 2)]]; + a.sort((p, q) => p[1] - q[1]); wall = a[0][0]; + } + let yaw = 0, pitch = 0; + if (wall === 'north') { z = -D / 2 + eps; yaw = 0; } + else if (wall === 'south') { z = D / 2 - eps; yaw = Math.PI; } + else if (wall === 'east') { x = W / 2 - eps; yaw = -Math.PI / 2; } + else if (wall === 'west') { x = -W / 2 + eps; yaw = Math.PI / 2; } + else if (wall === 'floor') { y = 0.02; pitch = -Math.PI / 2; } + else if (wall === 'ceiling') { y = H - eps; pitch = Math.PI / 2; } + m.position.set(x, y, z); m.rotation.set(pitch, yaw, 0); + } (data.decals || []).forEach(d => { const w = num(d.width, 1), h = num(d.height, 1), op = num(d.opacity, 1); let mat; @@ -273,8 +310,7 @@ function buildScene(data) { texLoader.load(d.image_url || d.asset_url, tx => { tx.colorSpace = THREE.SRGBColorSpace; mat.map = tx; mat.needsUpdate = true; }, undefined, () => {}); } else { return; } // mesh / empty — skipped (ponytail: 17MB logo .glb left out; webp art covers the look) const m = new THREE.Mesh(new THREE.PlaneGeometry(w, h), mat); - m.position.set(num(d.pos_x), num(d.pos_y, 1.5), num(d.pos_z)); - m.rotation.set(rad(num(d.rotation_x)), rad(num(d.rotation_y)), rad(num(d.rotation_z))); + placeDecal(m, d); roomGroup.add(m); });