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);
});