feat(virtual): finish the 3D store port — per-room scoping, decals, portals
Scene was dumping all 3 rooms' racks/crates/lights into one room, and the served decals + portals were never rendered (trapped in booth-room). - /virtual/scene scopes to ONE room (?space=, default = is_default space); filters racks/crates/lights/cameras/portals/decals/covers by space_id - render decals (image + canvas-text planes) and portal doorways; walk into a portal -> loads the linked room - portal links_to_id is the PAIRED portal's id, not a space — resolve through it to the destination room (to_space) - copy the 29 referenced decal webp/svg into webstore/assets (self-contained, served /store/assets); rewrite WP /wp-content/uploads/ paths in the scene API - skip the 17MB logo .glb mesh decal (flat logo-blue.webp covers branding) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ -8,9 +8,18 @@ from .mirror import enrich as mirror_enrich
|
||||
# WowPlatter's payload.php: same scene shape, but from Postgres instead of a full WP bootstrap.
|
||||
router = APIRouter(prefix="/virtual", tags=["virtual-store"])
|
||||
|
||||
# Decal/logo assets were copied out of WP's uploads into webstore/assets (served at /store/assets)
|
||||
# so the store is self-contained — no live-WP dependency. Rewrite the stored WP paths on the way out.
|
||||
_WP_PREFIX = "/wp-content/uploads/"
|
||||
_ASSET_BASE = "/store/assets/"
|
||||
|
||||
async def _all(db, table, where=""):
|
||||
rows = await db.execute(text(f"SELECT * FROM {table} {where}"))
|
||||
|
||||
def _asset(u):
|
||||
return _ASSET_BASE + u[len(_WP_PREFIX):] if isinstance(u, str) and u.startswith(_WP_PREFIX) else u
|
||||
|
||||
|
||||
async def _all(db, table, where="", params=None):
|
||||
rows = await db.execute(text(f"SELECT * FROM {table} {where}"), params or {})
|
||||
return [dict(r) for r in rows.mappings()]
|
||||
|
||||
|
||||
@ -22,31 +31,50 @@ def _attach(recs, meta):
|
||||
|
||||
|
||||
@router.get("/scene")
|
||||
async def scene(db=Depends(get_db)):
|
||||
"""Everything the renderer needs at boot. Records are SLIM — one front cover per crate;
|
||||
the full per-crate list is lazy-loaded on dig via /virtual/crate/{id}/records."""
|
||||
async def scene(space: int | None = None, db=Depends(get_db)):
|
||||
"""Everything the renderer needs to draw ONE room. Pass ?space=<id> to load a linked room
|
||||
(the renderer calls it when you walk through a portal); default = the marked-default space.
|
||||
Records are SLIM — one front cover per crate; full per-crate list lazy-loads via /crate/{id}/records."""
|
||||
spaces = await _all(db, "virtual_space", "WHERE visible='y'")
|
||||
active = spaces[0] if len(spaces) == 1 else next(
|
||||
active = (next((s for s in spaces if s["id"] == space), None) if space else None) or next(
|
||||
(s for s in spaces if s.get("is_default") == "y"), spaces[0] if spaces else None)
|
||||
if not active:
|
||||
return {"space": None, "spaces": spaces, "racks": [], "crates": [], "records": []}
|
||||
sid = active["id"]
|
||||
if active.get("logo_texture"):
|
||||
active["logo_texture"] = _asset(active["logo_texture"])
|
||||
p = {"s": sid}
|
||||
|
||||
# one front cover per crate, restricted to crates in THIS room
|
||||
recs = [dict(r) for r in (await db.execute(text("""
|
||||
SELECT DISTINCT ON (crate_id) crate_id, slot_number, release_id, sku, price, condition
|
||||
FROM inventory
|
||||
WHERE in_stock AND crate_id IS NOT NULL AND release_id IS NOT NULL
|
||||
ORDER BY crate_id, slot_number NULLS LAST"""))).mappings()]
|
||||
SELECT DISTINCT ON (i.crate_id) i.crate_id, i.slot_number, i.release_id, i.sku, i.price, i.condition
|
||||
FROM inventory i JOIN virtual_crate c ON c.id = i.crate_id
|
||||
WHERE i.in_stock AND i.release_id IS NOT NULL AND c.space_id = :s
|
||||
ORDER BY i.crate_id, i.slot_number NULLS LAST"""), p)).mappings()]
|
||||
recs = _attach(recs, await mirror_enrich({r["release_id"] for r in recs}))
|
||||
|
||||
decals = await _all(db, "virtual_decal",
|
||||
"WHERE visible <> 'n' AND object_type='space' AND object_id = :s", p)
|
||||
for d in decals:
|
||||
d["image_url"], d["asset_url"] = _asset(d.get("image_url")), _asset(d.get("asset_url"))
|
||||
|
||||
# links_to_id points at the PAIRED portal, not a space — resolve to the room it lives in.
|
||||
portals = [dict(r) for r in (await db.execute(text("""
|
||||
SELECT p.*, t.space_id AS to_space
|
||||
FROM virtual_portal p LEFT JOIN virtual_portal t ON t.id = p.links_to_id
|
||||
WHERE p.space_id = :s AND p.enabled = 'y'"""), p)).mappings()]
|
||||
|
||||
return {
|
||||
"space": active, "spaces": spaces,
|
||||
"racks": await _all(db, "virtual_rack", "WHERE visible='y'"),
|
||||
"racks": await _all(db, "virtual_rack", "WHERE visible='y' AND space_id = :s", p),
|
||||
"rack_types": await _all(db, "virtual_rack_type"),
|
||||
"rack_type_levels": await _all(db, "virtual_rack_type_level"),
|
||||
"crates": await _all(db, "virtual_crate", "WHERE visible='y'"),
|
||||
"crates": await _all(db, "virtual_crate", "WHERE visible='y' AND space_id = :s", p),
|
||||
"crate_types": await _all(db, "virtual_crate_type"),
|
||||
"cameras": await _all(db, "virtual_camera"),
|
||||
"lights": await _all(db, "virtual_light"),
|
||||
"decals": await _all(db, "virtual_decal", "WHERE visible <> 'n'"),
|
||||
"portals": await _all(db, "virtual_portal"),
|
||||
"cameras": await _all(db, "virtual_camera", "WHERE space_id = :s", p),
|
||||
"lights": await _all(db, "virtual_light", "WHERE space_id = :s", p),
|
||||
"decals": decals,
|
||||
"portals": portals,
|
||||
"records": recs,
|
||||
}
|
||||
|
||||
|
||||
BIN
webstore/assets/2025/12/robot-window.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
40
webstore/assets/wowplatter/aircon.svg
Executable file
@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1140 350"
|
||||
width="1140" height="350"
|
||||
stroke="#222" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
<!-- OUTER BODY -->
|
||||
<rect x="0" y="0" width="1140" height="350" stroke-width="6" rx="20" ry="20"/>
|
||||
|
||||
<!-- VENT LINES (bottom area) -->
|
||||
<g stroke-width="4">
|
||||
<line x1="40" y1="260" x2="1100" y2="260"/>
|
||||
<line x1="40" y1="280" x2="1100" y2="280"/>
|
||||
<line x1="40" y1="300" x2="1100" y2="300"/>
|
||||
<line x1="40" y1="320" x2="1100" y2="320"/>
|
||||
</g>
|
||||
|
||||
<!-- DISPLAY PANEL -->
|
||||
<rect x="940" y="40" width="160" height="100" stroke-width="4" rx="10" ry="10"/>
|
||||
|
||||
<!-- LED 7-segment "24" -->
|
||||
<g stroke-width="5" stroke="#0ff">
|
||||
|
||||
<!-- Digit 2 -->
|
||||
<!-- segments: top, upper-right, middle, lower-left, bottom -->
|
||||
<line x1="960" y1="60" x2="1000" y2="60"/> <!-- top -->
|
||||
<line x1="1000" y1="60" x2="1000" y2="95"/> <!-- upper right -->
|
||||
<line x1="960" y1="95" x2="1000" y2="95"/> <!-- middle -->
|
||||
<line x1="960" y1="95" x2="960" y2="130"/> <!-- lower left -->
|
||||
<line x1="960" y1="130" x2="1000" y2="130"/> <!-- bottom -->
|
||||
|
||||
<!-- Digit 4 -->
|
||||
<!-- segments: upper-left, upper-right, middle, lower-right -->
|
||||
<line x1="1020" y1="60" x2="1020" y2="95"/> <!-- upper left -->
|
||||
<line x1="1060" y1="60" x2="1060" y2="95"/> <!-- upper right -->
|
||||
<line x1="1020" y1="95" x2="1060" y2="95"/> <!-- middle -->
|
||||
<line x1="1060" y1="95" x2="1060" y2="130"/> <!-- lower right -->
|
||||
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
82
webstore/assets/wowplatter/bluequart.svg
Executable file
@ -0,0 +1,82 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="2400" height="4800"
|
||||
viewBox="0 0 2400 4800">
|
||||
|
||||
<defs>
|
||||
<!-- Vertical-ridge pattern for one 1200x1200 tile -->
|
||||
<pattern id="ridge-vertical" patternUnits="userSpaceOnUse" width="1200" height="1200">
|
||||
<line x1="100" y1="0" x2="100" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="200" y1="0" x2="200" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="300" y1="0" x2="300" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="400" y1="0" x2="400" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="500" y1="0" x2="500" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="600" y1="0" x2="600" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="700" y1="0" x2="700" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="800" y1="0" x2="800" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="900" y1="0" x2="900" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="1000" y1="0" x2="1000" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="1100" y1="0" x2="1100" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Horizontal-ridge pattern for one 1200x1200 tile -->
|
||||
<pattern id="ridge-horizontal" patternUnits="userSpaceOnUse" width="1200" height="1200">
|
||||
<line x1="0" y1="100" x2="1200" y2="100" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="200" x2="1200" y2="200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="300" x2="1200" y2="300" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="400" x2="1200" y2="400" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="500" x2="1200" y2="500" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="600" x2="1200" y2="600" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="700" x2="1200" y2="700" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="800" x2="1200" y2="800" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="900" x2="1200" y2="900" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="1000" x2="1200" y2="1000" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="1100" x2="1200" y2="1100" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Background checkerboard pattern for alternating blue/black -->
|
||||
<pattern id="checker-bg" patternUnits="userSpaceOnUse" width="2400" height="2400">
|
||||
|
||||
<rect x="0" y="0" width="1200" height="1200" fill="blue"/>
|
||||
<rect x="1200" y="0" width="1200" height="1200" fill="black"/>
|
||||
<rect x="0" y="1200" width="1200" height="1200" fill="black"/>
|
||||
<rect x="1200" y="1200" width="1200" height="1200" fill="blue"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- 1. Outer border (2400 wide x 4800 high) - Dimensions updated for 2x4 -->
|
||||
<rect x="0" y="0" width="2400" height="4800" stroke="#000" fill="none" stroke-width="8"/>
|
||||
|
||||
<!-- 2. Background Checkerboard Fill - Height updated -->
|
||||
<rect x="0" y="0" width="2400" height="4800" fill="url(#checker-bg)"/>
|
||||
|
||||
<!-- 3. Overlay Ridge Patterns (4 Rows x 2 Columns) - R4 through R7 removed -->
|
||||
<g fill-opacity="0.5">
|
||||
<!-- R0: V H (y=0) -->
|
||||
<rect x="0" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="1200" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
|
||||
<!-- R1: H V (y=1200) -->
|
||||
<rect x="0" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="1200" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
|
||||
<!-- R2: V H (y=2400) -->
|
||||
<rect x="0" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="1200" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
|
||||
<!-- R3: H V (y=3600) -->
|
||||
<rect x="0" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="1200" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
</g>
|
||||
|
||||
<!-- 4. Grid lines - Adjusted to stop at 4800, only 3 horizontal lines remain -->
|
||||
<g stroke="#000" fill="none" stroke-width="8">
|
||||
<!-- Vertical grid line at X=1200 -->
|
||||
<line x1="1200" y1="0" x2="1200" y2="4800"/>
|
||||
|
||||
<!-- Horizontal grid lines -->
|
||||
<line x1="0" y1="1200" x2="2400" y2="1200"/>
|
||||
<line x1="0" y1="2400" x2="2400" y2="2400"/>
|
||||
<line x1="0" y1="3600" x2="2400" y2="3600"/>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
28
webstore/assets/wowplatter/booth.svg
Executable file
@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1860 790"
|
||||
width="930" height="395"
|
||||
stroke="#000" fill="none" stroke-linejoin="miter" stroke-linecap="square">
|
||||
|
||||
<!-- OUTER FRAME (3cm = 30 units thick) -->
|
||||
<rect x="0" y="0" width="1860" height="790" stroke-width="30"/>
|
||||
|
||||
<!-- FRONT OPENING (inner frame) -->
|
||||
<rect x="30" y="30" width="1800" height="730" stroke-width="6"/>
|
||||
|
||||
<!-- DEPTH (20cm = 200 units) -->
|
||||
|
||||
<!-- FRONT CORNERS -->
|
||||
<!-- FL(30,30) FR(1830,30) BL(30,760) BR(1830,760) -->
|
||||
|
||||
<!-- BACK CORNERS -->
|
||||
<!-- FL'(230,230) FR'(1630,230) BL'(230,560) BR'(1630,560) -->
|
||||
|
||||
<!-- Connecting lines (front → back) -->
|
||||
<line x1="30" y1="30" x2="230" y2="230" stroke-width="4"/>
|
||||
<line x1="1830" y1="30" x2="1630" y2="230" stroke-width="4"/>
|
||||
<line x1="30" y1="760" x2="230" y2="560" stroke-width="4"/>
|
||||
<line x1="1830" y1="760" x2="1630" y2="560" stroke-width="4"/>
|
||||
|
||||
<!-- Back rectangle -->
|
||||
<polygon points="230,230 1630,230 1630,560 230,560" stroke-width="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1012 B |
15
webstore/assets/wowplatter/door2.svg
Executable file
@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 820 2200"
|
||||
width="410" height="1100"
|
||||
stroke="#000" fill="none">
|
||||
|
||||
<!-- Outer frame (thin 1cm-ish) -->
|
||||
<rect x="10" y="10" width="800" height="2180" stroke-width="8"/>
|
||||
|
||||
<!-- Door (fills the frame except tiny gap like real door clearance) -->
|
||||
<rect x="20" y="20" width="780" height="2160" stroke-width="4"/>
|
||||
|
||||
<!-- Door knob (center-right, about 1100mm high) -->
|
||||
<circle cx="740" cy="1100" r="20" fill="#000"/>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
82
webstore/assets/wowplatter/purpquart.svg
Executable file
@ -0,0 +1,82 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="2400" height="4800"
|
||||
viewBox="0 0 2400 4800">
|
||||
|
||||
<defs>
|
||||
<!-- Vertical-ridge pattern for one 1200x1200 tile -->
|
||||
<pattern id="ridge-vertical" patternUnits="userSpaceOnUse" width="1200" height="1200">
|
||||
<line x1="100" y1="0" x2="100" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="200" y1="0" x2="200" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="300" y1="0" x2="300" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="400" y1="0" x2="400" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="500" y1="0" x2="500" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="600" y1="0" x2="600" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="700" y1="0" x2="700" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="800" y1="0" x2="800" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="900" y1="0" x2="900" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="1000" y1="0" x2="1000" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="1100" y1="0" x2="1100" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Horizontal-ridge pattern for one 1200x1200 tile -->
|
||||
<pattern id="ridge-horizontal" patternUnits="userSpaceOnUse" width="1200" height="1200">
|
||||
<line x1="0" y1="100" x2="1200" y2="100" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="200" x2="1200" y2="200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="300" x2="1200" y2="300" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="400" x2="1200" y2="400" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="500" x2="1200" y2="500" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="600" x2="1200" y2="600" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="700" x2="1200" y2="700" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="800" x2="1200" y2="800" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="900" x2="1200" y2="900" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="1000" x2="1200" y2="1000" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="1100" x2="1200" y2="1100" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Background checkerboard pattern for alternating purple/black -->
|
||||
<pattern id="checker-bg" patternUnits="userSpaceOnUse" width="2400" height="2400">
|
||||
|
||||
<rect x="0" y="0" width="1200" height="1200" fill="purple"/>
|
||||
<rect x="1200" y="0" width="1200" height="1200" fill="black"/>
|
||||
<rect x="0" y="1200" width="1200" height="1200" fill="black"/>
|
||||
<rect x="1200" y="1200" width="1200" height="1200" fill="purple"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- 1. Outer border (2400 wide x 4800 high) - Dimensions updated for 2x4 -->
|
||||
<rect x="0" y="0" width="2400" height="4800" stroke="#000" fill="none" stroke-width="8"/>
|
||||
|
||||
<!-- 2. Background Checkerboard Fill - Height updated -->
|
||||
<rect x="0" y="0" width="2400" height="4800" fill="url(#checker-bg)"/>
|
||||
|
||||
<!-- 3. Overlay Ridge Patterns (4 Rows x 2 Columns) - R4 through R7 removed -->
|
||||
<g fill-opacity="0.5">
|
||||
<!-- R0: V H (y=0) -->
|
||||
<rect x="0" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="1200" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
|
||||
<!-- R1: H V (y=1200) -->
|
||||
<rect x="0" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="1200" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
|
||||
<!-- R2: V H (y=2400) -->
|
||||
<rect x="0" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="1200" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
|
||||
<!-- R3: H V (y=3600) -->
|
||||
<rect x="0" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="1200" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
</g>
|
||||
|
||||
<!-- 4. Grid lines - Adjusted to stop at 4800, only 3 horizontal lines remain -->
|
||||
<g stroke="#000" fill="none" stroke-width="8">
|
||||
<!-- Vertical grid line at X=1200 -->
|
||||
<line x1="1200" y1="0" x2="1200" y2="4800"/>
|
||||
|
||||
<!-- Horizontal grid lines -->
|
||||
<line x1="0" y1="1200" x2="2400" y2="1200"/>
|
||||
<line x1="0" y1="2400" x2="2400" y2="2400"/>
|
||||
<line x1="0" y1="3600" x2="2400" y2="3600"/>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
104
webstore/assets/wowplatter/rooflong-purp.svg
Executable file
@ -0,0 +1,104 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="9600" height="4800"
|
||||
viewBox="0 0 9600 4800">
|
||||
|
||||
<defs>
|
||||
<!-- Vertical-ridge pattern for one 1200x1200 tile -->
|
||||
<pattern id="ridge-vertical" patternUnits="userSpaceOnUse" width="1200" height="1200">
|
||||
<line x1="100" y1="0" x2="100" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="200" y1="0" x2="200" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="300" y1="0" x2="300" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="400" y1="0" x2="400" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="500" y1="0" x2="500" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="600" y1="0" x2="600" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="700" y1="0" x2="700" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="800" y1="0" x2="800" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="900" y1="0" x2="900" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="1000" y1="0" x2="1000" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="1100" y1="0" x2="1100" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Horizontal-ridge pattern for one 1200x1200 tile -->
|
||||
<pattern id="ridge-horizontal" patternUnits="userSpaceOnUse" width="1200" height="1200">
|
||||
<line x1="0" y1="100" x2="1200" y2="100" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="200" x2="1200" y2="200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="300" x2="1200" y2="300" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="400" x2="1200" y2="400" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="500" x2="1200" y2="500" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="600" x2="1200" y2="600" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="700" x2="1200" y2="700" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="800" x2="1200" y2="800" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="900" x2="1200" y2="900" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="1000" x2="1200" y2="1000" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="1100" x2="1200" y2="1100" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Background checkerboard pattern with purple/black -->
|
||||
<pattern id="checker-bg" patternUnits="userSpaceOnUse" width="2400" height="2400">
|
||||
<rect x="0" y="0" width="1200" height="1200" fill="purple"/>
|
||||
<rect x="1200" y="0" width="1200" height="1200" fill="black"/>
|
||||
<rect x="0" y="1200" width="1200" height="1200" fill="black"/>
|
||||
<rect x="1200" y="1200" width="1200" height="1200" fill="purple"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- outer border -->
|
||||
<rect x="0" y="0" width="9600" height="4800" stroke="#000" fill="none" stroke-width="8"/>
|
||||
|
||||
<!-- background checkerboard -->
|
||||
<rect x="0" y="0" width="9600" height="4800" fill="url(#checker-bg)"/>
|
||||
|
||||
<!-- overlay ridge patterns on top of checkerboard -->
|
||||
<g>
|
||||
<rect x="0" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="1200" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="2400" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="3600" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="4800" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="6000" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="7200" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="8400" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
|
||||
<rect x="0" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="1200" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="2400" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="3600" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="4800" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="6000" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="7200" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="8400" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
|
||||
<rect x="0" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="1200" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="2400" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="3600" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="4800" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="6000" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="7200" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="8400" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
|
||||
<rect x="0" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="1200" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="2400" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="3600" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="4800" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="6000" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="7200" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="8400" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
</g>
|
||||
|
||||
<!-- grid lines -->
|
||||
<g stroke="#000" fill="none" stroke-width="8">
|
||||
<line x1="1200" y1="0" x2="1200" y2="4800"/>
|
||||
<line x1="2400" y1="0" x2="2400" y2="4800"/>
|
||||
<line x1="3600" y1="0" x2="3600" y2="4800"/>
|
||||
<line x1="4800" y1="0" x2="4800" y2="4800"/>
|
||||
<line x1="6000" y1="0" x2="6000" y2="4800"/>
|
||||
<line x1="7200" y1="0" x2="7200" y2="4800"/>
|
||||
<line x1="8400" y1="0" x2="8400" y2="4800"/>
|
||||
<line x1="0" y1="1200" x2="9600" y2="1200"/>
|
||||
<line x1="0" y1="2400" x2="9600" y2="2400"/>
|
||||
<line x1="0" y1="3600" x2="9600" y2="3600"/>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
104
webstore/assets/wowplatter/rooflong.svg
Executable file
@ -0,0 +1,104 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="9600" height="4800"
|
||||
viewBox="0 0 9600 4800">
|
||||
|
||||
<defs>
|
||||
<!-- Vertical-ridge pattern for one 1200x1200 tile -->
|
||||
<pattern id="ridge-vertical" patternUnits="userSpaceOnUse" width="1200" height="1200">
|
||||
<line x1="100" y1="0" x2="100" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="200" y1="0" x2="200" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="300" y1="0" x2="300" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="400" y1="0" x2="400" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="500" y1="0" x2="500" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="600" y1="0" x2="600" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="700" y1="0" x2="700" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="800" y1="0" x2="800" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="900" y1="0" x2="900" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="1000" y1="0" x2="1000" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="1100" y1="0" x2="1100" y2="1200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Horizontal-ridge pattern for one 1200x1200 tile -->
|
||||
<pattern id="ridge-horizontal" patternUnits="userSpaceOnUse" width="1200" height="1200">
|
||||
<line x1="0" y1="100" x2="1200" y2="100" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="200" x2="1200" y2="200" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="300" x2="1200" y2="300" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="400" x2="1200" y2="400" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="500" x2="1200" y2="500" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="600" x2="1200" y2="600" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="700" x2="1200" y2="700" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="800" x2="1200" y2="800" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="900" x2="1200" y2="900" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="1000" x2="1200" y2="1000" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
<line x1="0" y1="1100" x2="1200" y2="1100" stroke="#000" stroke-width="8" stroke-linecap="butt"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Background checkerboard pattern for alternating blue/black -->
|
||||
<pattern id="checker-bg" patternUnits="userSpaceOnUse" width="2400" height="2400">
|
||||
<rect x="0" y="0" width="1200" height="1200" fill="blue"/>
|
||||
<rect x="1200" y="0" width="1200" height="1200" fill="black"/>
|
||||
<rect x="0" y="1200" width="1200" height="1200" fill="black"/>
|
||||
<rect x="1200" y="1200" width="1200" height="1200" fill="blue"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- outer border -->
|
||||
<rect x="0" y="0" width="9600" height="4800" stroke="#000" fill="none" stroke-width="8"/>
|
||||
|
||||
<!-- background checkerboard -->
|
||||
<rect x="0" y="0" width="9600" height="4800" fill="url(#checker-bg)"/>
|
||||
|
||||
<!-- overlay ridge patterns on top of checkerboard -->
|
||||
<g>
|
||||
<rect x="0" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="1200" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="2400" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="3600" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="4800" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="6000" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="7200" y="0" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="8400" y="0" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
|
||||
<rect x="0" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="1200" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="2400" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="3600" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="4800" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="6000" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="7200" y="1200" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="8400" y="1200" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
|
||||
<rect x="0" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="1200" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="2400" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="3600" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="4800" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="6000" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="7200" y="2400" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="8400" y="2400" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
|
||||
<rect x="0" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="1200" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="2400" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="3600" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="4800" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="6000" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
<rect x="7200" y="3600" width="1200" height="1200" fill="url(#ridge-horizontal)"/>
|
||||
<rect x="8400" y="3600" width="1200" height="1200" fill="url(#ridge-vertical)"/>
|
||||
</g>
|
||||
|
||||
<!-- grid lines -->
|
||||
<g stroke="#000" fill="none" stroke-width="8">
|
||||
<line x1="1200" y1="0" x2="1200" y2="4800"/>
|
||||
<line x1="2400" y1="0" x2="2400" y2="4800"/>
|
||||
<line x1="3600" y1="0" x2="3600" y2="4800"/>
|
||||
<line x1="4800" y1="0" x2="4800" y2="4800"/>
|
||||
<line x1="6000" y1="0" x2="6000" y2="4800"/>
|
||||
<line x1="7200" y1="0" x2="7200" y2="4800"/>
|
||||
<line x1="8400" y1="0" x2="8400" y2="4800"/>
|
||||
<line x1="0" y1="1200" x2="9600" y2="1200"/>
|
||||
<line x1="0" y1="2400" x2="9600" y2="2400"/>
|
||||
<line x1="0" y1="3600" x2="9600" y2="3600"/>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
BIN
webstore/assets/wowplatter/webp/banksia.webp
Executable file
|
After Width: | Height: | Size: 40 KiB |
BIN
webstore/assets/wowplatter/webp/boothsticker.webp
Executable file
|
After Width: | Height: | Size: 31 KiB |
BIN
webstore/assets/wowplatter/webp/curtains.webp
Executable file
|
After Width: | Height: | Size: 84 KiB |
BIN
webstore/assets/wowplatter/webp/euc.webp
Executable file
|
After Width: | Height: | Size: 36 KiB |
BIN
webstore/assets/wowplatter/webp/foam.webp
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
webstore/assets/wowplatter/webp/gallaxx.webp
Executable file
|
After Width: | Height: | Size: 27 KiB |
BIN
webstore/assets/wowplatter/webp/godzillaa.webp
Executable file
|
After Width: | Height: | Size: 150 KiB |
BIN
webstore/assets/wowplatter/webp/kangpaww.webp
Executable file
|
After Width: | Height: | Size: 44 KiB |
BIN
webstore/assets/wowplatter/webp/logo-blue.webp
Executable file
|
After Width: | Height: | Size: 40 KiB |
BIN
webstore/assets/wowplatter/webp/logo-mrp.webp
Executable file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
webstore/assets/wowplatter/webp/mecha.webp
Executable file
|
After Width: | Height: | Size: 38 KiB |
BIN
webstore/assets/wowplatter/webp/mons-stat.webp
Executable file
|
After Width: | Height: | Size: 33 KiB |
BIN
webstore/assets/wowplatter/webp/purple.webp
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
webstore/assets/wowplatter/webp/purplestrip.webp
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
webstore/assets/wowplatter/webp/redd.webp
Executable file
|
After Width: | Height: | Size: 26 KiB |
BIN
webstore/assets/wowplatter/webp/robo-stat.webp
Executable file
|
After Width: | Height: | Size: 49 KiB |
BIN
webstore/assets/wowplatter/webp/rugg.webp
Executable file
|
After Width: | Height: | Size: 30 KiB |
BIN
webstore/assets/wowplatter/webp/sparky-3.webp
Executable file
|
After Width: | Height: | Size: 10 KiB |
BIN
webstore/assets/wowplatter/webp/tents.webp
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
webstore/assets/wowplatter/webp/tyle.webp
Executable file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
webstore/assets/wowplatter/webp/walkbot.webp
Executable file
|
After Width: | Height: | Size: 41 KiB |
@ -64,7 +64,9 @@ addEventListener('keyup', e => keys[e.code] = false);
|
||||
const vel = new THREE.Vector3();
|
||||
|
||||
// --- build the scene from /virtual/scene ---
|
||||
const crateMeshes = [];
|
||||
// One room at a time. roomGroup holds everything space-scoped so a portal walk-through clears
|
||||
// it and rebuilds the linked room. crateMeshes/portals reset per load.
|
||||
let crateMeshes = [], portals = [], roomGroup = null, switching = false;
|
||||
const texLoader = new THREE.TextureLoader();
|
||||
texLoader.crossOrigin = 'anonymous';
|
||||
|
||||
@ -72,30 +74,49 @@ function material(color, fallback) {
|
||||
return new THREE.MeshStandardMaterial({ color: new THREE.Color(color || fallback), roughness: 0.85 });
|
||||
}
|
||||
|
||||
fetch('/virtual/scene').then(r => r.json()).then(data => {
|
||||
// text → CanvasTexture (shared by text decals + portal labels)
|
||||
function textTexture(text, { color = '#fff', bg = null, font = 64, pad = 24 } = {}) {
|
||||
const lines = String(text).split('\n');
|
||||
const cv = document.createElement('canvas'); const ctx = cv.getContext('2d');
|
||||
ctx.font = `600 ${font}px system-ui, sans-serif`;
|
||||
const w = Math.max(...lines.map(l => ctx.measureText(l).width)) + pad * 2;
|
||||
const lh = font * 1.25;
|
||||
cv.width = Math.ceil(w); cv.height = Math.ceil(lines.length * lh + pad * 2);
|
||||
ctx.font = `600 ${font}px system-ui, sans-serif`; ctx.textBaseline = 'top';
|
||||
if (bg) { ctx.fillStyle = bg; ctx.fillRect(0, 0, cv.width, cv.height); }
|
||||
ctx.fillStyle = color;
|
||||
lines.forEach((l, i) => ctx.fillText(l, pad, pad + i * lh));
|
||||
const tx = new THREE.CanvasTexture(cv); tx.colorSpace = THREE.SRGBColorSpace;
|
||||
return { tx, aspect: cv.width / cv.height };
|
||||
}
|
||||
|
||||
function buildScene(data) {
|
||||
if (roomGroup) scene.remove(roomGroup);
|
||||
roomGroup = new THREE.Group(); scene.add(roomGroup);
|
||||
crateMeshes = []; portals = [];
|
||||
const sp = data.space || {};
|
||||
const W = num(sp.room_width, 12), D = num(sp.room_depth, 12), H = num(sp.ceiling_height, 3.2);
|
||||
|
||||
// room
|
||||
const floor = new THREE.Mesh(new THREE.PlaneGeometry(W, D), material(sp.floor_color, '#2a2a30'));
|
||||
floor.rotation.x = -Math.PI / 2; scene.add(floor);
|
||||
floor.rotation.x = -Math.PI / 2; roomGroup.add(floor);
|
||||
const ceil = new THREE.Mesh(new THREE.PlaneGeometry(W, D), material(sp.ceiling_color, '#1a1a1f'));
|
||||
ceil.rotation.x = Math.PI / 2; ceil.position.y = H; scene.add(ceil);
|
||||
ceil.rotation.x = Math.PI / 2; ceil.position.y = H; roomGroup.add(ceil);
|
||||
const wallMat = material(sp.wall_color, '#3a3a42');
|
||||
const walls = [[0, -D / 2, 0], [0, D / 2, Math.PI], [-W / 2, 0, Math.PI / 2], [W / 2, 0, -Math.PI / 2]];
|
||||
walls.forEach(([x, z, ry], i) => {
|
||||
const w = (i < 2) ? W : D;
|
||||
const m = new THREE.Mesh(new THREE.PlaneGeometry(w, H), wallMat.clone());
|
||||
m.position.set(x, H / 2, z); m.rotation.y = ry; scene.add(m);
|
||||
m.position.set(x, H / 2, z); m.rotation.y = ry; roomGroup.add(m);
|
||||
});
|
||||
|
||||
// lights (data-driven + a soft ambient so it's never pitch black)
|
||||
scene.add(new THREE.AmbientLight(0xffffff, num(sp.ambient_light_intensity, 0.6)));
|
||||
roomGroup.add(new THREE.AmbientLight(0xffffff, num(sp.ambient_light_intensity, 0.6)));
|
||||
(data.lights || []).forEach(L => {
|
||||
const pl = new THREE.PointLight(new THREE.Color(L.color || '#ffffff'), num(L.intensity, 0.8), num(L.distance, 0) || 0);
|
||||
pl.position.set(num(L.pos_x), num(L.pos_y, H - 0.3), num(L.pos_z)); scene.add(pl);
|
||||
pl.position.set(num(L.pos_x), num(L.pos_y, H - 0.3), num(L.pos_z)); roomGroup.add(pl);
|
||||
});
|
||||
if (!(data.lights || []).length) { const d = new THREE.DirectionalLight(0xffffff, 0.7); d.position.set(3, 6, 2); scene.add(d); }
|
||||
if (!(data.lights || []).length) { const d = new THREE.DirectionalLight(0xffffff, 0.7); d.position.set(3, 6, 2); roomGroup.add(d); }
|
||||
|
||||
// racks (box per rack, sized by its rack_type)
|
||||
const rackTypes = Object.fromEntries((data.rack_types || []).map(t => [t.id, t]));
|
||||
@ -105,7 +126,7 @@ fetch('/virtual/scene').then(r => r.json()).then(data => {
|
||||
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, dp), material(t.material_color, '#5a4634'));
|
||||
m.position.set(num(rk.pos_x), num(rk.pos_y, h / 2), num(rk.pos_z));
|
||||
m.rotation.set(rad(num(rk.rotation_x)), rad(num(rk.rotation_y)), rad(num(rk.rotation_z)));
|
||||
scene.add(m);
|
||||
roomGroup.add(m);
|
||||
});
|
||||
|
||||
// crates (box per crate, sized by crate_type) + front-cover texture
|
||||
@ -126,13 +147,52 @@ fetch('/virtual/scene').then(r => r.json()).then(data => {
|
||||
m.position.set(num(cr.pos_x), num(cr.pos_y, h / 2), num(cr.pos_z));
|
||||
m.rotation.set(rad(num(cr.rotation_x)), rad(num(cr.rotation_y)), rad(num(cr.rotation_z)));
|
||||
m.userData = { crateId: cr.id, name: cr.label_text || cr.name };
|
||||
scene.add(m); crateMeshes.push(m);
|
||||
roomGroup.add(m); crateMeshes.push(m);
|
||||
});
|
||||
|
||||
// decals — wall art / signage as textured planes (image) or canvas text. mesh decals skipped.
|
||||
(data.decals || []).forEach(d => {
|
||||
const w = num(d.width, 1), h = num(d.height, 1), op = num(d.opacity, 1);
|
||||
let mat;
|
||||
if (d.content_type === 'text' && d.text_value) {
|
||||
const { tx } = textTexture(d.text_value, { color: d.text_color || '#fff', bg: d.background_color || null });
|
||||
mat = new THREE.MeshBasicMaterial({ map: tx, transparent: true, opacity: op, side: THREE.DoubleSide });
|
||||
} else if (d.content_type === 'image' && (d.image_url || d.asset_url)) {
|
||||
mat = new THREE.MeshBasicMaterial({ transparent: true, opacity: op, side: THREE.DoubleSide });
|
||||
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)));
|
||||
roomGroup.add(m);
|
||||
});
|
||||
|
||||
// portals — doorways to linked rooms. A translucent labelled curtain you walk through.
|
||||
(data.portals || []).forEach(p => {
|
||||
if (!p.links_to_id) return;
|
||||
const w = num(p.width, 1.6), h = 2.2;
|
||||
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));
|
||||
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 });
|
||||
});
|
||||
|
||||
// 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));
|
||||
stat.innerHTML = `<b>${(data.racks||[]).length}</b> racks · <b>${(data.crates||[]).length}</b> crates · <b>${(data.records||[]).length}</b> covers`;
|
||||
}).catch(e => { stat.textContent = 'scene load failed: ' + e; });
|
||||
stat.innerHTML = `<b>${sp.name || 'store'}</b> · ${(data.racks||[]).length} racks · ${(data.crates||[]).length} crates · ${(data.records||[]).length} covers`;
|
||||
}
|
||||
|
||||
function loadSpace(id) {
|
||||
switching = true;
|
||||
const q = id ? `?space=${id}` : '';
|
||||
return fetch('/virtual/scene' + q).then(r => r.json())
|
||||
.then(data => { buildScene(data); switching = false; })
|
||||
.catch(e => { stat.textContent = 'scene load failed: ' + e; switching = false; });
|
||||
}
|
||||
loadSpace();
|
||||
|
||||
// --- dig: click a crate (raycast from screen centre) ---
|
||||
const ray = new THREE.Raycaster();
|
||||
@ -148,6 +208,7 @@ renderer.domElement.addEventListener('click', () => {
|
||||
|
||||
// --- render loop ---
|
||||
const clock = new THREE.Clock();
|
||||
let portalArmed = false; // must be clear of every portal before one can fire (no bounce-loop if you spawn on a doorway)
|
||||
function tick() {
|
||||
requestAnimationFrame(tick);
|
||||
const dt = Math.min(clock.getDelta(), 0.1);
|
||||
@ -160,6 +221,15 @@ function tick() {
|
||||
controls.moveRight(vel.x * speed);
|
||||
controls.moveForward(-vel.z * speed);
|
||||
}
|
||||
// walk into a portal → load the linked room (must have stepped clear of all portals first)
|
||||
if (!switching) {
|
||||
let near = false;
|
||||
for (const p of portals) {
|
||||
const dx = camera.position.x - p.x, dz = camera.position.z - p.z;
|
||||
if (dx * dx + dz * dz < 0.9 * 0.9) { near = true; if (portalArmed) { portalArmed = false; loadSpace(p.to); } break; }
|
||||
}
|
||||
if (!near) portalArmed = true;
|
||||
}
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||