← back to Dw Catalog 3d
Live 3D layered catalog model (vendor towers x status layers) + scene data
09b5d67b8a481a6dc2daccaf356e96b83120695a · 2026-06-26 08:09:34 -0700 · Steve
Files touched
A .gitignoreA public/index.htmlA public/scene.json
Diff
commit 09b5d67b8a481a6dc2daccaf356e96b83120695a
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jun 26 08:09:34 2026 -0700
Live 3D layered catalog model (vendor towers x status layers) + scene data
---
.gitignore | 3 +
public/index.html | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++
public/scene.json | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 386 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c45938
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+*.log
+.DS_Store
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..b200292
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,189 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8"/>
+<meta name="viewport" content="width=device-width,initial-scale=1"/>
+<title>DW Catalog · Live 3D Layered Model</title>
+<style>
+ :root{ --bg:#0c0f15; --panel:#141925cc; --fg:#E6E8EC; --muted:#8A93A2; --gold:#C8A24B;
+ --active:#70AD47; --staged:#E0A33E; --draft:#5B9BD5; --archived:#5A6472; --deleted:#E74C3C; }
+ *{box-sizing:border-box} html,body{margin:0;height:100%;overflow:hidden;background:var(--bg);
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;color:var(--fg)}
+ #c{position:fixed;inset:0;display:block}
+ .hud{position:fixed;z-index:10;pointer-events:none}
+ #title{top:18px;left:24px}
+ #title h1{margin:0;font-size:20px;letter-spacing:.5px;font-weight:800}
+ #title h1 .g{color:var(--gold)}
+ #title p{margin:3px 0 0;font-size:12px;color:var(--muted);font-style:italic}
+ #legend{top:18px;right:24px;background:var(--panel);backdrop-filter:blur(8px);border:1px solid #2A2F3A;
+ border-radius:14px;padding:14px 16px;pointer-events:auto;min-width:230px}
+ #legend h3{margin:0 0 10px;font-size:11px;letter-spacing:1.5px;color:var(--muted);text-transform:uppercase}
+ .row{display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:9px;cursor:pointer;
+ user-select:none;transition:background .15s}
+ .row:hover{background:#1d2230}
+ .row.off{opacity:.34}
+ .sw{width:15px;height:15px;border-radius:4px;flex:0 0 auto;box-shadow:0 0 10px currentColor}
+ .row .nm{font-size:13px;font-weight:600;flex:1}
+ .row .ct{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
+ #hint{bottom:16px;left:24px;font-size:11px;color:var(--muted)}
+ #tooltip{position:fixed;z-index:20;pointer-events:none;background:#0e1320ee;border:1px solid var(--gold);
+ border-radius:10px;padding:10px 12px;font-size:12px;display:none;min-width:170px;
+ box-shadow:0 8px 30px #000a}
+ #tooltip .v{font-weight:800;font-size:13px;margin-bottom:6px;color:var(--gold)}
+ #tooltip .l{display:flex;justify-content:space-between;gap:14px;line-height:1.55}
+ #tooltip .l b{font-variant-numeric:tabular-nums}
+ .dot{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px;vertical-align:middle}
+ #scaleToggle{margin-top:12px;display:flex;gap:6px}
+ #scaleToggle button{flex:1;pointer-events:auto;background:#1a2030;color:var(--fg);border:1px solid #2A2F3A;
+ border-radius:8px;padding:6px;font-size:11px;cursor:pointer;font-weight:600}
+ #scaleToggle button.sel{background:var(--gold);color:#11141A;border-color:var(--gold)}
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<div id="title" class="hud">
+ <h1>DESIGNER WALLCOVERINGS · <span class="g">LIVE 3D CATALOG</span></h1>
+ <p>vendor towers · stacked status layers · drag to orbit · scroll to zoom · click a layer</p>
+</div>
+<div id="legend" class="hud">
+ <h3>Status Layers</h3>
+ <div id="rows"></div>
+ <div id="scaleToggle"><button data-s="sqrt">√ scale</button><button data-s="log" class="sel">log scale</button><button data-s="lin">linear</button></div>
+</div>
+<div id="hint" class="hud">24 vendors · ~169k products · source: dw_unified · toggle layers in the legend ↗</div>
+<div id="tooltip"></div>
+
+<script type="importmap">
+{ "imports": {
+ "three":"https://unpkg.com/three@0.160.0/build/three.module.js",
+ "three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"
+}}
+</script>
+<script type="module">
+import * as THREE from 'three';
+import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+
+const LAYERS=[
+ {key:'active', name:'Active', color:0x70AD47},
+ {key:'staged', name:'Staged', color:0xE0A33E},
+ {key:'draft', name:'Draft', color:0x5B9BD5},
+ {key:'archived',name:'Archived', color:0x5A6472},
+ {key:'deleted', name:'Deleted', color:0xE74C3C},
+];
+const HEX=k=>'#'+LAYERS.find(l=>l.key===k).color.toString(16).padStart(6,'0');
+const visible=Object.fromEntries(LAYERS.map(l=>[l.key,true]));
+let scaleMode='log';
+
+const scene=new THREE.Scene();
+scene.background=new THREE.Color(0x0c0f15);
+scene.fog=new THREE.FogExp2(0x0c0f15,0.012);
+
+const camera=new THREE.PerspectiveCamera(52,innerWidth/innerHeight,0.1,2000);
+camera.position.set(58,46,74);
+
+const renderer=new THREE.WebGLRenderer({canvas:document.getElementById('c'),antialias:true});
+renderer.setSize(innerWidth,innerHeight); renderer.setPixelRatio(Math.min(devicePixelRatio,2));
+
+const controls=new OrbitControls(camera,renderer.domElement);
+controls.enableDamping=true; controls.dampingFactor=.07; controls.target.set(0,7,0);
+controls.autoRotate=true; controls.autoRotateSpeed=0.45;
+controls.maxPolarAngle=Math.PI*0.49;
+
+scene.add(new THREE.AmbientLight(0xffffff,0.55));
+const key=new THREE.DirectionalLight(0xffffff,0.9); key.position.set(20,40,20); scene.add(key);
+const gold=new THREE.PointLight(0xC8A24B,0.7,200); gold.position.set(-20,25,-10); scene.add(gold);
+
+// gold grid floor
+const grid=new THREE.GridHelper(120,40,0xC8A24B,0x232a36);
+grid.material.transparent=true; grid.material.opacity=0.22; scene.add(grid);
+
+const towers=new THREE.Group(); scene.add(towers);
+const labelGroup=new THREE.Group(); scene.add(labelGroup);
+let DATA=[]; const meshIndex=[];
+
+function scaleFn(v){
+ if(v<=0) return 0;
+ if(scaleMode==='sqrt') return Math.sqrt(v)*0.42;
+ if(scaleMode==='log') return Math.log1p(v)*1.7;
+ return v*0.0016;
+}
+function makeLabel(text){
+ const cv=document.createElement('canvas'); const s=2; cv.width=256*s; cv.height=64*s;
+ const ctx=cv.getContext('2d'); ctx.scale(s,s);
+ ctx.fillStyle='rgba(14,19,32,0.0)'; ctx.fillRect(0,0,256,64);
+ ctx.font='600 22px -apple-system,Helvetica,Arial'; ctx.fillStyle='#E6E8EC';
+ ctx.textAlign='center'; ctx.textBaseline='middle';
+ ctx.fillText(text.length>18?text.slice(0,17)+'…':text,128,32);
+ const tex=new THREE.CanvasTexture(cv); tex.anisotropy=4;
+ const sp=new THREE.Sprite(new THREE.SpriteMaterial({map:tex,transparent:true,depthWrite:false}));
+ sp.scale.set(9,2.25,1); return sp;
+}
+
+function build(){
+ towers.clear(); labelGroup.clear(); meshIndex.length=0;
+ const N=DATA.length, cols=Math.ceil(Math.sqrt(N)), gap=11;
+ DATA.forEach((d,i)=>{
+ const cx=(i%cols-(cols-1)/2)*gap;
+ const cz=(Math.floor(i/cols)-(Math.ceil(N/cols)-1)/2)*gap;
+ let y=0;
+ LAYERS.forEach(L=>{
+ const val=d[L.key]||0; if(val<=0||!visible[L.key]) return;
+ const h=Math.max(scaleFn(val),0.12);
+ const geo=new THREE.BoxGeometry(5.4,h,5.4);
+ const mat=new THREE.MeshStandardMaterial({color:L.color,emissive:L.color,
+ emissiveIntensity:0.32,metalness:0.3,roughness:0.45,transparent:true,opacity:0.88});
+ const m=new THREE.Mesh(geo,mat);
+ m.position.set(cx,y+h/2,cz);
+ m.userData={vendor:d.vendor,layer:L.name,val,color:L.color};
+ towers.add(m); meshIndex.push(m);
+ // wire glow edges
+ const eg=new THREE.LineSegments(new THREE.EdgesGeometry(geo),
+ new THREE.LineBasicMaterial({color:L.color,transparent:true,opacity:0.5}));
+ eg.position.copy(m.position); towers.add(eg);
+ y+=h+0.04;
+ });
+ const lab=makeLabel(d.vendor); lab.position.set(cx,y+1.8,cz); labelGroup.add(lab);
+ });
+}
+
+function buildLegend(){
+ const rows=document.getElementById('rows'); rows.innerHTML='';
+ LAYERS.forEach(L=>{
+ const tot=DATA.reduce((s,d)=>s+(d[L.key]||0),0);
+ const el=document.createElement('div'); el.className='row'; el.dataset.k=L.key;
+ el.innerHTML=`<span class="sw" style="background:${HEX(L.key)};color:${HEX(L.key)}"></span>
+ <span class="nm">${L.name}</span><span class="ct">${tot.toLocaleString()}</span>`;
+ el.onclick=()=>{visible[L.key]=!visible[L.key]; el.classList.toggle('off',!visible[L.key]); build();};
+ rows.appendChild(el);
+ });
+}
+document.querySelectorAll('#scaleToggle button').forEach(b=>b.onclick=()=>{
+ document.querySelectorAll('#scaleToggle button').forEach(x=>x.classList.remove('sel'));
+ b.classList.add('sel'); scaleMode=b.dataset.s; build();
+});
+
+// hover tooltip via raycaster
+const ray=new THREE.Raycaster(), mouse=new THREE.Vector2(), tip=document.getElementById('tooltip');
+addEventListener('mousemove',e=>{
+ mouse.x=(e.clientX/innerWidth)*2-1; mouse.y=-(e.clientY/innerHeight)*2+1;
+ ray.setFromCamera(mouse,camera);
+ const hit=ray.intersectObjects(meshIndex)[0];
+ if(hit){
+ const d=DATA.find(x=>x.vendor===hit.object.userData.vendor);
+ tip.style.display='block'; tip.style.left=(e.clientX+16)+'px'; tip.style.top=(e.clientY+14)+'px';
+ tip.innerHTML=`<div class="v">${d.vendor}</div>`+LAYERS.map(L=>
+ `<div class="l"><span><span class="dot" style="background:${HEX(L.key)}"></span>${L.name}</span><b>${(d[L.key]||0).toLocaleString()}</b></div>`).join('');
+ } else tip.style.display='none';
+});
+
+fetch('scene.json?_='+Date.now()).then(r=>r.json()).then(d=>{
+ DATA=d.sort((a,b)=>(b.active+b.archived)-(a.active+a.archived));
+ buildLegend(); build();
+});
+
+addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();
+ renderer.setSize(innerWidth,innerHeight);});
+(function loop(){requestAnimationFrame(loop);controls.update();renderer.render(scene,camera);})();
+</script>
+</body>
+</html>
diff --git a/public/scene.json b/public/scene.json
new file mode 100644
index 0000000..11be047
--- /dev/null
+++ b/public/scene.json
@@ -0,0 +1,194 @@
+[
+ {
+ "vendor": "Phillipe Romano",
+ "active": 10446,
+ "staged": 3,
+ "draft": 3497,
+ "archived": 4909,
+ "deleted": 1835
+ },
+ {
+ "vendor": "Scalamandre Fabrics",
+ "active": 1,
+ "staged": 0,
+ "draft": 0,
+ "archived": 10177,
+ "deleted": 0
+ },
+ {
+ "vendor": "Kravet",
+ "active": 2227,
+ "staged": 4,
+ "draft": 0,
+ "archived": 2675,
+ "deleted": 2718
+ },
+ {
+ "vendor": "Hollywood Wallcoverings",
+ "active": 3155,
+ "staged": 0,
+ "draft": 52,
+ "archived": 2433,
+ "deleted": 1
+ },
+ {
+ "vendor": "Schumacher",
+ "active": 2340,
+ "staged": 0,
+ "draft": 50,
+ "archived": 2790,
+ "deleted": 17
+ },
+ {
+ "vendor": "DW Bespoke Studio",
+ "active": 921,
+ "staged": 0,
+ "draft": 3,
+ "archived": 1407,
+ "deleted": 2632
+ },
+ {
+ "vendor": "Schumacher Fabrics",
+ "active": 0,
+ "staged": 0,
+ "draft": 0,
+ "archived": 4802,
+ "deleted": 45
+ },
+ {
+ "vendor": "Phillip Jeffries",
+ "active": 2473,
+ "staged": 0,
+ "draft": 30,
+ "archived": 584,
+ "deleted": 1291
+ },
+ {
+ "vendor": "Clarke And Clarke",
+ "active": 3573,
+ "staged": 0,
+ "draft": 4,
+ "archived": 350,
+ "deleted": 170
+ },
+ {
+ "vendor": "Malibu Wallpaper",
+ "active": 2657,
+ "staged": 0,
+ "draft": 884,
+ "archived": 490,
+ "deleted": 15
+ },
+ {
+ "vendor": "Thibaut",
+ "active": 2055,
+ "staged": 1143,
+ "draft": 713,
+ "archived": 1106,
+ "deleted": 66
+ },
+ {
+ "vendor": "Lee Jofa",
+ "active": 2152,
+ "staged": 0,
+ "draft": 3,
+ "archived": 1540,
+ "deleted": 187
+ },
+ {
+ "vendor": "Jeffrey Stevens",
+ "active": 1498,
+ "staged": 0,
+ "draft": 7,
+ "archived": 2304,
+ "deleted": 4
+ },
+ {
+ "vendor": "Rebel Walls",
+ "active": 3613,
+ "staged": 0,
+ "draft": 0,
+ "archived": 108,
+ "deleted": 0
+ },
+ {
+ "vendor": "Brunschwig & Fils",
+ "active": 2761,
+ "staged": 0,
+ "draft": 4,
+ "archived": 606,
+ "deleted": 1
+ },
+ {
+ "vendor": "JF FABRICS at DW",
+ "active": 0,
+ "staged": 0,
+ "draft": 0,
+ "archived": 3240,
+ "deleted": 0
+ },
+ {
+ "vendor": "Fentucci",
+ "active": 358,
+ "staged": 0,
+ "draft": 3,
+ "archived": 2589,
+ "deleted": 279
+ },
+ {
+ "vendor": "Graham & Brown",
+ "active": 405,
+ "staged": 1720,
+ "draft": 0,
+ "archived": 1274,
+ "deleted": 1207
+ },
+ {
+ "vendor": "Koroseal",
+ "active": 2351,
+ "staged": 233,
+ "draft": 1,
+ "archived": 196,
+ "deleted": 272
+ },
+ {
+ "vendor": "JF Fabrics",
+ "active": 0,
+ "staged": 0,
+ "draft": 0,
+ "archived": 2643,
+ "deleted": 0
+ },
+ {
+ "vendor": "Designer Wallcoverings",
+ "active": 1532,
+ "staged": 0,
+ "draft": 38,
+ "archived": 692,
+ "deleted": 140
+ },
+ {
+ "vendor": "Los Angeles Fabrics",
+ "active": 1840,
+ "staged": 0,
+ "draft": 1,
+ "archived": 525,
+ "deleted": 0
+ },
+ {
+ "vendor": "Gaston Y Daniela",
+ "active": 2151,
+ "staged": 0,
+ "draft": 2,
+ "archived": 140,
+ "deleted": 11
+ },
+ {
+ "vendor": "Schumacher Wallpaper",
+ "active": 0,
+ "staged": 0,
+ "draft": 0,
+ "archived": 2057,
+ "deleted": 0
+ }
+]
\ No newline at end of file
(oldest)
·
back to Dw Catalog 3d
·
Add room/product thumbnails to tower tooltips; pm2-served; F a09d7ff →