knosky 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +25 -1
- package/SECURITY.md +14 -0
- package/action/post-comment.mjs +89 -0
- package/action.yml +62 -0
- package/core/benchmark-results.mjs +225 -0
- package/core/bundle.mjs +7 -1
- package/core/comparison.mjs +189 -0
- package/core/constants.mjs +13 -0
- package/core/cross-repo.mjs +111 -0
- package/core/escalate.mjs +68 -0
- package/core/freshness.mjs +194 -0
- package/core/fs-indexer.mjs +6 -0
- package/core/key-store.mjs +348 -0
- package/core/ledger.mjs +141 -0
- package/core/lod.mjs +155 -0
- package/core/multi-model-benchmark.mjs +405 -0
- package/core/onboarding.mjs +223 -0
- package/core/protocol-spec.mjs +460 -0
- package/core/route.mjs +5 -0
- package/core/schema.mjs +26 -1
- package/package.json +3 -1
- package/renderer/city.template.html +130 -21
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
<div id="boot">Building the city…</div>
|
|
115
115
|
<header></header>
|
|
116
116
|
<aside id="nav">
|
|
117
|
-
<div class="navbrand"><svg class="logo" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg" aria-label="
|
|
117
|
+
<div class="navbrand"><svg class="logo" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg" aria-label="KnoSky">
|
|
118
118
|
<defs><radialGradient id="og" cx="50%" cy="50%" r="50%">
|
|
119
119
|
<stop offset="0%" stop-color="#f0b454" stop-opacity=".55"/><stop offset="55%" stop-color="#e89a2e" stop-opacity=".14"/><stop offset="100%" stop-color="#e89a2e" stop-opacity="0"/>
|
|
120
120
|
</radialGradient></defs>
|
|
@@ -171,6 +171,27 @@
|
|
|
171
171
|
let CITY={nodes:[],generated_at:""};
|
|
172
172
|
let GD="";
|
|
173
173
|
function hashCode(s){let h=2166136261;s=String(s);for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}return (h>>>0);}
|
|
174
|
+
|
|
175
|
+
// ---------- LOD / clustering / viewport-culling / streaming (SAT-447) ----------
|
|
176
|
+
// Tier constants (mirror core/lod.mjs — kept inline so the renderer stays self-contained)
|
|
177
|
+
const LOD_FULL=0,LOD_SIMPLE=1,LOD_DOT=2,LOD_CLUSTER=3;
|
|
178
|
+
// Zoom thresholds: below each boundary the next LOD tier kicks in
|
|
179
|
+
const LOD_FULL_THRESH=0.35,LOD_SIMPLE_THRESH=0.12,LOD_DOT_THRESH=0.06;
|
|
180
|
+
// Streaming: repos with this many nodes get progressive OBJ population across rAF frames
|
|
181
|
+
const STREAM_THRESH=400,STREAM_BATCH=80;
|
|
182
|
+
function lodForZoom(z){if(z>=LOD_FULL_THRESH)return LOD_FULL;if(z>=LOD_SIMPLE_THRESH)return LOD_SIMPLE;if(z>=LOD_DOT_THRESH)return LOD_DOT;return LOD_CLUSTER;}
|
|
183
|
+
// Current-frame LOD tier + axis-aligned world-space clip rect (updated once per render)
|
|
184
|
+
let _lod=LOD_FULL,_clip={wx0:-1e9,wy0:-1e9,wx1:1e9,wy1:1e9};
|
|
185
|
+
function updateLodAndClip(){
|
|
186
|
+
_lod=lodForZoom(cam.zoom);
|
|
187
|
+
const pad=128; // world-unit margin so tiles at the edge aren't clipped
|
|
188
|
+
_clip={wx0:(0-cam.x)/cam.zoom-pad,wy0:(0-cam.y)/cam.zoom-pad,
|
|
189
|
+
wx1:(innerWidth-cam.x)/cam.zoom+pad,wy1:(innerHeight-cam.y)/cam.zoom+pad};}
|
|
190
|
+
function inView(wx,wy){return wx>=_clip.wx0&&wx<=_clip.wx1&&wy>=_clip.wy0&&wy<=_clip.wy1;}
|
|
191
|
+
// District cluster descriptors — one entry per district, built at ingest time
|
|
192
|
+
let CLUSTERS=[];
|
|
193
|
+
function buildClustersNow(){CLUSTERS=[];const acc={};for(const n of NODES){const k=n.d;if(!acc[k])acc[k]={sumX:0,sumY:0,count:0};acc[k].sumX+=n.bx;acc[k].sumY+=n.by;acc[k].count++;}Object.keys(acc).forEach(k=>{const a=acc[k];const cfg=DCFG[k]||{name:k,color:'#7c83a3'};CLUSTERS.push({d:k,label:cfg.name,color:cfg.color,count:a.count,wx:a.sumX/a.count,wy:a.sumY/a.count});});}
|
|
194
|
+
|
|
174
195
|
const TW=64,TH=32;
|
|
175
196
|
// central relative-size system (multiples of a tile) — tune here to keep everything proportional
|
|
176
197
|
const SCALE={building:1.5,tree:0.7,car:0.6,fountain:0.5,bench:0.34,lamp:0.62};
|
|
@@ -229,19 +250,43 @@
|
|
|
229
250
|
const TOWER=[0,1,2,21,23],HOUSE=[16,18,17,19,20];
|
|
230
251
|
let OBJ=[];
|
|
231
252
|
let ROADS=[],roadSet=new Set();
|
|
253
|
+
// Streaming geometry build state: when the node count is large, OBJ is populated
|
|
254
|
+
// incrementally across animation frames so the page stays responsive.
|
|
255
|
+
let _streamPending=null,_streamDone=false;
|
|
256
|
+
function _applyNodeGeom(n){const h=hashCode(n.id);n.gx=n._gx;n.gy=n._gy;
|
|
257
|
+
n.scale=0.62+((h>>2)%36)/100;n.h=1+((h>>7)%6)+(((h>>11)%5===0)?3:0);n.bright=0.9+((h>>17)%20)/100;
|
|
258
|
+
const w=isoToWorld(n.gx,n.gy);n.bx=w.x;n.by=w.y;
|
|
259
|
+
const pool=n.kind==='decision'?TOWER:HOUSE;n.cell=pool[(h>>5)%pool.length];n.bscale=0.97+((h>>9)%10)/100;n.bv=(n.kind==='decision'?(h%6):6+(h%10));}
|
|
232
260
|
function buildGeometry(){
|
|
233
261
|
LAY=buildLayout(NODES);IB=LAY.IB;
|
|
234
|
-
NODES.forEach(n=>{const h=hashCode(n.id);n.gx=n._gx;n.gy=n._gy;
|
|
235
|
-
n.scale=0.62+((h>>2)%36)/100;n.h=1+((h>>7)%6)+(((h>>11)%5===0)?3:0);n.bright=0.9+((h>>17)%20)/100;
|
|
236
|
-
const w=isoToWorld(n.gx,n.gy);n.bx=w.x;n.by=w.y;
|
|
237
|
-
const pool=n.kind==='decision'?TOWER:HOUSE;n.cell=pool[(h>>5)%pool.length];n.bscale=0.97+((h>>9)%10)/100;n.bv=(n.kind==='decision'?(h%6):6+(h%10));});
|
|
238
|
-
OBJ=[];
|
|
239
|
-
NODES.forEach(n=>{if(n.gx!=null)OBJ.push({k:'b',gx:n.gx,gy:n.gy,n});});
|
|
240
|
-
TILES.forEach(t=>{if(t.type==='tree')OBJ.push({k:'t',gx:t.gx,gy:t.gy,seed:hashCode('t'+t.gx+'_'+t.gy)});});
|
|
241
|
-
PROPS.forEach(p=>OBJ.push({k:'p',gx:p.gx,gy:p.gy,type:p.type}));
|
|
242
|
-
OBJ.sort((a,b)=>(a.gx+a.gy)-(b.gx+b.gy)||a.gx-b.gx);
|
|
243
262
|
ROADS=TILES.filter(t=>t.type==='road');roadSet=new Set(ROADS.map(t=>t.gx+","+t.gy));
|
|
244
|
-
|
|
263
|
+
// Always apply geometry attributes synchronously so bx/by are ready for clustering
|
|
264
|
+
NODES.forEach(_applyNodeGeom);
|
|
265
|
+
if(NODES.length>=STREAM_THRESH){
|
|
266
|
+
// Large repo: populate OBJ skeleton synchronously (buildings only) so first frames
|
|
267
|
+
// can render clusters + initial buildings, then stream trees/props over rAF batches.
|
|
268
|
+
OBJ=[];
|
|
269
|
+
NODES.forEach(n=>{if(n.gx!=null)OBJ.push({k:'b',gx:n.gx,gy:n.gy,n});});
|
|
270
|
+
OBJ.sort((a,b)=>(a.gx+a.gy)-(b.gx+b.gy)||a.gx-b.gx);
|
|
271
|
+
// Queue tree/prop batches for streaming
|
|
272
|
+
const extras=[];
|
|
273
|
+
TILES.forEach(t=>{if(t.type==='tree')extras.push({k:'t',gx:t.gx,gy:t.gy,seed:hashCode('t'+t.gx+'_'+t.gy)});});
|
|
274
|
+
PROPS.forEach(p=>extras.push({k:'p',gx:p.gx,gy:p.gy,type:p.type}));
|
|
275
|
+
_streamPending=extras;_streamDone=false;
|
|
276
|
+
_scheduleStream();
|
|
277
|
+
}else{
|
|
278
|
+
OBJ=[];
|
|
279
|
+
NODES.forEach(n=>{if(n.gx!=null)OBJ.push({k:'b',gx:n.gx,gy:n.gy,n});});
|
|
280
|
+
TILES.forEach(t=>{if(t.type==='tree')OBJ.push({k:'t',gx:t.gx,gy:t.gy,seed:hashCode('t'+t.gx+'_'+t.gy)});});
|
|
281
|
+
PROPS.forEach(p=>OBJ.push({k:'p',gx:p.gx,gy:p.gy,type:p.type}));
|
|
282
|
+
OBJ.sort((a,b)=>(a.gx+a.gy)-(b.gx+b.gy)||a.gx-b.gx);
|
|
283
|
+
_streamPending=null;_streamDone=true;}}
|
|
284
|
+
function _scheduleStream(){if(!_streamPending||!_streamPending.length){_streamDone=true;return;}
|
|
285
|
+
requestAnimationFrame(()=>{if(!_streamPending)return;
|
|
286
|
+
const batch=_streamPending.splice(0,STREAM_BATCH);
|
|
287
|
+
for(const o of batch)OBJ.push(o);
|
|
288
|
+
OBJ.sort((a,b)=>(a.gx+a.gy)-(b.gx+b.gy)||a.gx-b.gx);
|
|
289
|
+
if(_streamPending.length)_scheduleStream();else{_streamDone=true;_streamPending=null;}});}
|
|
245
290
|
function roadNbrs(gx,gy){const o=[];[[1,0],[-1,0],[0,1],[0,-1]].forEach(d=>{if(roadSet.has((gx+d[0])+","+(gy+d[1])))o.push(d);});return o;}
|
|
246
291
|
|
|
247
292
|
// ---------- tree sprite-sheet constants (legacy 7.5MB sheet fallback removed; dedicated trees.png is the live path) ----------
|
|
@@ -412,7 +457,7 @@
|
|
|
412
457
|
|
|
413
458
|
// ---------- ground ----------
|
|
414
459
|
const GRASS="#3c6a3a";
|
|
415
|
-
function drawGround(){for(const t of TILES){const w=isoToWorld(t.gx,t.gy);diamondPath(w.x,w.y,TW/2,TH/2);let col;
|
|
460
|
+
function drawGround(){for(const t of TILES){const w=isoToWorld(t.gx,t.gy);if(!inView(w.x,w.y))continue;diamondPath(w.x,w.y,TW/2,TH/2);let col;
|
|
416
461
|
if(t.type==='road')col=PAL.night?[26,27,38]:[64,66,76];
|
|
417
462
|
else if(t.type==='park')col=PAL.night?[54,60,44]:[150,156,118];
|
|
418
463
|
else if(t.type==='plaza')col=PAL.night?[64,61,51]:[120,114,92];
|
|
@@ -426,7 +471,7 @@
|
|
|
426
471
|
if(vr){ctx.moveTo(w.x-TW/4,w.y+TH/4);ctx.lineTo(w.x+TW/4,w.y-TH/4);}
|
|
427
472
|
ctx.stroke();ctx.setLineDash([]);}
|
|
428
473
|
}}
|
|
429
|
-
function drawShadows(){ctx.fillStyle="rgba(0,0,0,.22)";for(const o of OBJ){if(o.k!=='b')continue;const n=o.n;if(filteredHidden.has(n.id))continue;const w=isoToWorld(n.gx,n.gy);diamondPath(w.x+8,w.y+5,TW*0.46*n.bscale,TH*0.46*n.bscale);ctx.fill();}}
|
|
474
|
+
function drawShadows(){if(_lod>=LOD_SIMPLE)return;ctx.fillStyle="rgba(0,0,0,.22)";for(const o of OBJ){if(o.k!=='b')continue;const n=o.n;if(filteredHidden.has(n.id))continue;const w=isoToWorld(n.gx,n.gy);if(!inView(w.x,w.y))continue;diamondPath(w.x+8,w.y+5,TW*0.46*n.bscale,TH*0.46*n.bscale);ctx.fill();}}
|
|
430
475
|
|
|
431
476
|
// ---------- buildings (sprite blit, day/night/dim, + vector fallback) ----------
|
|
432
477
|
// Legacy raster-sprite path (unused in the default Kenney CC0 / --vector modes); bottom-anchored to the tile.
|
|
@@ -492,10 +537,28 @@
|
|
|
492
537
|
ctx.fillStyle="#5b3d1f";ctx.fillRect(w.x-1.5,w.y-6,3,7);ctx.fillStyle=(seed%3?"#3e7a3c":"#4f8a44");
|
|
493
538
|
ctx.beginPath();ctx.arc(w.x,w.y-10,s,0,7);ctx.arc(w.x-s*0.5,w.y-6,s*0.7,0,7);ctx.arc(w.x+s*0.5,w.y-6,s*0.7,0,7);ctx.fill();}
|
|
494
539
|
|
|
495
|
-
// ---------- flat dotted connections ----------
|
|
540
|
+
// ---------- flat dotted connections (same-repo) + arced cross-repo boundary edges ----------
|
|
541
|
+
// CROSS_REPO_SET holds "fromId\x00toId" pairs populated during ingest() for O(1) lookup.
|
|
542
|
+
let CROSS_REPO_SET=new Set();
|
|
543
|
+
function repoOf(n){const p=n&&n.prov;if(!p)return null;if(typeof p.repo==='string'&&p.repo.length)return p.repo;if(typeof p.store==='string'&&p.store.length)return p.store;return null;}
|
|
544
|
+
function buildCrossRepoSet(nodes){CROSS_REPO_SET=new Set();const ob=Object.fromEntries(nodes.map(n=>[n.id,n]));for(const n of nodes){const r1=repoOf(n);if(!r1)continue;for(const tid of(n.links||[])){const m=ob[tid];if(!m)continue;const r2=repoOf(m);if(r2&&r1!==r2)CROSS_REPO_SET.add(n.id+'\x00'+tid);}}}
|
|
545
|
+
function isCrossRepoLink(fromId,toId){return CROSS_REPO_SET.has(fromId+'\x00'+toId);}
|
|
546
|
+
// Draw a cross-repo edge as a raised arc with a portal glow (visually distinct from same-repo dotted lines).
|
|
547
|
+
function drawCrossRepoEdge(A,B){const mx=(A[0]+B[0])/2,my=(A[1]+B[1])/2,lift=Math.hypot(B[0]-A[0],B[1]-A[1])*0.28;
|
|
548
|
+
// shadow halo
|
|
549
|
+
ctx.save();ctx.globalAlpha=0.18;ctx.strokeStyle="#c680ff";ctx.lineWidth=7;ctx.setLineDash([]);
|
|
550
|
+
ctx.beginPath();ctx.moveTo(A[0],A[1]);ctx.quadraticCurveTo(mx,my-lift,B[0],B[1]);ctx.stroke();ctx.restore();
|
|
551
|
+
// main arc
|
|
552
|
+
ctx.strokeStyle="rgba(198,128,255,.92)";ctx.lineWidth=2;ctx.setLineDash([5,4]);
|
|
553
|
+
ctx.beginPath();ctx.moveTo(A[0],A[1]);ctx.quadraticCurveTo(mx,my-lift,B[0],B[1]);ctx.stroke();
|
|
554
|
+
// destination portal ring
|
|
555
|
+
ctx.setLineDash([]);ctx.fillStyle="rgba(220,180,255,.28)";ctx.beginPath();ctx.arc(B[0],B[1],8,0,7);ctx.fill();
|
|
556
|
+
ctx.strokeStyle="rgba(198,128,255,.9)";ctx.lineWidth=1.5;ctx.beginPath();ctx.arc(B[0],B[1],4.2,0,7);ctx.stroke();
|
|
557
|
+
ctx.fillStyle="#e8c0ff";ctx.beginPath();ctx.arc(B[0],B[1],2.0,0,7);ctx.fill();}
|
|
496
558
|
function drawConnections(id){const n=byId[id];if(!n)return;const A=[n.bx,n.by];
|
|
497
559
|
ctx.save();ctx.setLineDash([2,6]);ctx.lineCap="round";ctx.lineWidth=2;
|
|
498
560
|
n.links.forEach(tid=>{const m=byId[tid];if(!m)return;const B=[m.bx,m.by];
|
|
561
|
+
if(isCrossRepoLink(id,tid)){drawCrossRepoEdge(A,B);ctx.setLineDash([2,6]);return;}
|
|
499
562
|
ctx.strokeStyle="rgba(255,240,180,.9)";ctx.beginPath();ctx.moveTo(A[0],A[1]);ctx.lineTo(B[0],B[1]);ctx.stroke();
|
|
500
563
|
ctx.setLineDash([]);ctx.fillStyle="#ffe9a6";ctx.beginPath();ctx.arc(B[0],B[1],3.2,0,7);ctx.fill();
|
|
501
564
|
ctx.fillStyle="rgba(255,233,166,.28)";ctx.beginPath();ctx.arc(B[0],B[1],7,0,7);ctx.fill();ctx.setLineDash([2,6]);});
|
|
@@ -503,7 +566,9 @@
|
|
|
503
566
|
|
|
504
567
|
// ---------- churn heat: ground glow under recently-changed files (D-155, file-level metadata only) ----------
|
|
505
568
|
function drawChurn(){
|
|
569
|
+
if(_lod>=LOD_SIMPLE)return; // skip at low zoom — churn glow is too small to read
|
|
506
570
|
for(const n of NODES){ const ch=n.churn; if(!ch||!ch.b||ch.b<2||filteredHidden.has(n.id)) continue;
|
|
571
|
+
if(!inView(n.bx,n.by))continue;
|
|
507
572
|
const col = ch.b>=3 ? 'rgba(242,104,63,' : 'rgba(232,178,74,';
|
|
508
573
|
ctx.fillStyle=col+'0.20)'; diamondPath(n.bx,n.by,TW*0.62,TH*0.62); ctx.fill();
|
|
509
574
|
ctx.fillStyle=col+'0.32)'; diamondPath(n.bx,n.by,TW*0.40,TH*0.40); ctx.fill();
|
|
@@ -573,24 +638,67 @@
|
|
|
573
638
|
if(flip){ctx.save();ctx.translate(x*2,0);ctx.scale(-1,1);ctx.drawImage(VEH,sx,sy,vCW,vCH,dx,dy,dW,dH);ctx.restore();}
|
|
574
639
|
else ctx.drawImage(VEH,sx,sy,vCW,vCH,dx,dy,dW,dH);}
|
|
575
640
|
|
|
641
|
+
// ---------- cluster badges (LOD_CLUSTER tier — entire districts collapsed to one badge) ----------
|
|
642
|
+
function drawClusterBadge(cl){
|
|
643
|
+
const x=cl.wx,y=cl.wy;
|
|
644
|
+
if(!inView(x,y))return;
|
|
645
|
+
if(focusK&&cl.d!==focusK)return;
|
|
646
|
+
const R=28,FONT_C="'Plus Jakarta Sans',system-ui,sans-serif";
|
|
647
|
+
// outer glow
|
|
648
|
+
ctx.save();
|
|
649
|
+
ctx.globalAlpha=0.22;
|
|
650
|
+
ctx.beginPath();ctx.arc(x,y,R*1.6,0,7);ctx.fillStyle=cl.color;ctx.fill();
|
|
651
|
+
ctx.globalAlpha=1;
|
|
652
|
+
// filled circle
|
|
653
|
+
ctx.beginPath();ctx.arc(x,y,R,0,7);ctx.fillStyle=cl.color;ctx.fill();
|
|
654
|
+
// border ring
|
|
655
|
+
ctx.lineWidth=2;ctx.strokeStyle="rgba(255,255,255,.45)";ctx.stroke();
|
|
656
|
+
// count label
|
|
657
|
+
const label=cl.count>=1000?(Math.round(cl.count/100)/10)+'k':String(cl.count);
|
|
658
|
+
ctx.fillStyle="#0a0a12";ctx.textAlign="center";ctx.textBaseline="middle";ctx.font="700 13px "+FONT_C;
|
|
659
|
+
ctx.fillText(label,x,y);
|
|
660
|
+
// district name beneath
|
|
661
|
+
ctx.globalAlpha=0.75;ctx.fillStyle=cl.color;ctx.font="600 10px "+FONT_C;
|
|
662
|
+
ctx.fillText(cl.label,x,y+R+12);
|
|
663
|
+
ctx.restore();}
|
|
664
|
+
|
|
576
665
|
// ---------- render ----------
|
|
577
666
|
let focusK=null,filteredHidden=new Set(),selectedId=null,connSet=new Set();
|
|
578
|
-
function render(){NOW=performance.now()-T0;moveSky();ctx.setTransform(DPR,0,0,DPR,0,0);drawBackdrop();
|
|
667
|
+
function render(){NOW=performance.now()-T0;updateLodAndClip();moveSky();ctx.setTransform(DPR,0,0,DPR,0,0);drawBackdrop();
|
|
579
668
|
ctx.save();ctx.translate(cam.x,cam.y);ctx.scale(cam.zoom,cam.zoom);
|
|
580
669
|
ctx.globalAlpha=Math.min(1,NOW/450);
|
|
670
|
+
|
|
671
|
+
// LOD_CLUSTER: render one badge per district, skip all expensive geometry
|
|
672
|
+
if(_lod===LOD_CLUSTER){
|
|
673
|
+
drawIslandShadow();drawSlab();
|
|
674
|
+
CLUSTERS.forEach(drawClusterBadge);
|
|
675
|
+
ctx.globalAlpha=1;ctx.restore();return;}
|
|
676
|
+
|
|
581
677
|
drawIslandShadow();drawSlab();drawGround();drawCloudShadows();drawShadows();drawChurn();
|
|
582
|
-
|
|
678
|
+
|
|
679
|
+
// At LOD_SIMPLE / LOD_DOT we skip cars, trees, props to keep frame-time low
|
|
680
|
+
const showFull=(_lod===LOD_FULL);
|
|
681
|
+
const list=OBJ.slice();
|
|
682
|
+
if(showFull)drawCars(list);
|
|
583
683
|
list.sort((a,b)=>(a.gx+a.gy)-(b.gx+b.gy)||a.gx-b.gx);
|
|
584
684
|
for(const o of list){
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if(o.k==='
|
|
685
|
+
// Viewport cull: skip objects outside the clip rect
|
|
686
|
+
const {gx,gy}=o;const wc=isoToWorld(gx,gy);if(!inView(wc.x,wc.y))continue;
|
|
687
|
+
if(o.k==='t'){if(showFull)drawTree(gx,gy,o.seed);continue;}
|
|
688
|
+
if(o.k==='p'){if(showFull)drawProp(o.type,gx,gy);continue;}
|
|
689
|
+
if(o.k==='c'){if(showFull){if(window.__KC_KENNEY__&&KEN.ok)drawKenCar(o.wx.x,o.wx.y,o.car);else if(VEH_OK)drawCarSprite(o.wx.x,o.wx.y,o.car.m,o.car.dir);else drawCar(o.wx.x,o.wx.y,o.car.col,o.car.dir);}continue;}
|
|
588
690
|
const n=o.n;const hidden=filteredHidden.has(n.id);
|
|
589
691
|
if(n.id===selectedId)continue;
|
|
590
692
|
const dim=hidden||(selectedId&&!connSet.has(n.id))||(focusK&&n.d!==focusK);
|
|
693
|
+
// LOD_DOT: render buildings as coloured dots instead of full models
|
|
694
|
+
if(_lod===LOD_DOT){
|
|
695
|
+
if(dim)continue;
|
|
696
|
+
ctx.fillStyle=DCFG[n.d]?DCFG[n.d].color:'#7c83a3';
|
|
697
|
+
ctx.beginPath();ctx.arc(wc.x,wc.y,4,0,7);ctx.fill();
|
|
698
|
+
continue;}
|
|
591
699
|
drawBuilding(n,false,dim);}
|
|
592
700
|
if(selectedId){drawConnections(selectedId);const sn=byId[selectedId];if(sn&&!filteredHidden.has(sn.id))drawBuilding(sn,true,false);}
|
|
593
|
-
drawClouds();drawSkyLife();
|
|
701
|
+
if(showFull){drawClouds();drawSkyLife();}
|
|
594
702
|
ctx.globalAlpha=1;ctx.restore();}
|
|
595
703
|
function loop(){requestAnimationFrame(loop);cam.x+=(cam.tx-cam.x)*0.12;cam.y+=(cam.ty-cam.y)*0.12;cam.zoom+=(cam.tz-cam.zoom)*0.12;render();}
|
|
596
704
|
|
|
@@ -666,7 +774,7 @@
|
|
|
666
774
|
document.getElementById('q').addEventListener('input',e=>applySearch(e.target.value.trim()));
|
|
667
775
|
let looping=false;
|
|
668
776
|
function boot(){if(!NODES.length){document.getElementById('boot').textContent="Waiting for city data…";document.getElementById('boot').style.display='';return;}
|
|
669
|
-
document.getElementById('boot').style.display='none';buildChips();buildNav();applySearch(document.getElementById('q').value.trim());fitAll();cam.x=cam.tx;cam.y=cam.ty;cam.zoom=cam.tz;
|
|
777
|
+
document.getElementById('boot').style.display='none';buildClustersNow();buildChips();buildNav();applySearch(document.getElementById('q').value.trim());fitAll();cam.x=cam.tx;cam.y=cam.ty;cam.zoom=cam.tz;
|
|
670
778
|
if(!looping){looping=true;T0=performance.now();loop();}}
|
|
671
779
|
|
|
672
780
|
// ---------- data ingest: receive the authenticated payload over postMessage,
|
|
@@ -681,6 +789,7 @@
|
|
|
681
789
|
byId=Object.fromEntries(NODES.map(n=>[n.id,n]));
|
|
682
790
|
NODES.forEach(n=>n.links=n.links.filter(id=>byId[id]&&id!==n.id));
|
|
683
791
|
buildGeometry();
|
|
792
|
+
buildCrossRepoSet(NODES);
|
|
684
793
|
buildSkyLife();
|
|
685
794
|
buildCars();
|
|
686
795
|
// reset view/selection state for the fresh graph
|