knosky 0.6.3 → 0.7.0
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 +149 -93
- package/CREDITS.md +14 -14
- package/LICENSE.md +76 -76
- package/LIMITATIONS.md +33 -23
- package/PRIVACY.md +30 -30
- package/README.md +170 -117
- package/SECURITY.md +78 -46
- package/action/post-comment.mjs +94 -89
- package/action.yml +62 -62
- package/bin/knosky.mjs +279 -105
- package/core/CONTRACT.md +70 -70
- package/core/append-only-checkpoint.mjs +215 -0
- package/core/audit-writer.mjs +317 -0
- package/core/benchmark-results.mjs +225 -225
- package/core/bundle.mjs +178 -178
- package/core/churn.mjs +23 -23
- package/core/ci.mjs +268 -268
- package/core/comparison.mjs +189 -189
- package/core/config.mjs +189 -189
- package/core/constants.mjs +13 -13
- package/core/contract.mjs +123 -123
- package/core/cross-repo.mjs +111 -111
- package/core/decision-codes.mjs +92 -0
- package/core/destination.mjs +161 -161
- package/core/district-classification.mjs +111 -0
- package/core/doctor-scorecard.mjs +369 -0
- package/core/domain-store.mjs +347 -0
- package/core/edges.mjs +43 -43
- package/core/escalate.mjs +68 -68
- package/core/freshness.mjs +198 -194
- package/core/fs-indexer.mjs +218 -218
- package/core/key-store.mjs +348 -348
- package/core/layout.mjs +46 -46
- package/core/ledger.mjs +176 -141
- package/core/local-ipc-identity.mjs +500 -0
- package/core/lod.mjs +155 -155
- package/core/mode-b.mjs +410 -0
- package/core/multi-model-benchmark.mjs +405 -405
- package/core/net-lockdown.mjs +421 -0
- package/core/onboarding.mjs +223 -223
- package/core/operator-auth.mjs +317 -0
- package/core/overlays.mjs +45 -45
- package/core/policy-lattice.mjs +142 -0
- package/core/pr-comment.mjs +198 -198
- package/core/protocol-spec.mjs +460 -460
- package/core/provenance.mjs +320 -0
- package/core/retrieve.mjs +63 -63
- package/core/route.mjs +304 -304
- package/core/schema.mjs +275 -275
- package/core/signing-tiers.mjs +1265 -0
- package/core/swarm-bench.mjs +106 -0
- package/core/swarm-coordinator.mjs +867 -0
- package/core/trust-root-rekey.mjs +410 -0
- package/mcp/server.mjs +264 -108
- package/package.json +56 -46
- package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
- package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
- package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
- package/renderer/art/kenney/sheet_allCars.xml +545 -545
- package/renderer/build-rich.mjs +43 -43
- package/renderer/city.template.html +808 -808
- package/ssot/decision-codes.json +133 -0
- package/ssot/ladder-l0-l3.md +232 -0
- package/ssot/tool-menu.json +130 -0
package/core/lod.mjs
CHANGED
|
@@ -1,155 +1,155 @@
|
|
|
1
|
-
// core/lod.mjs — Level-of-detail, viewport culling, clustering, and streaming
|
|
2
|
-
// utilities for large single-repo city graphs (SAT-447).
|
|
3
|
-
//
|
|
4
|
-
// All functions are pure (no side-effects, no DOM/canvas imports) so they can
|
|
5
|
-
// be used both in the Node.js test harness and inlined into the browser renderer.
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// LOD tier constants
|
|
9
|
-
// Each tier drives rendering fidelity in city.template.html.
|
|
10
|
-
// FULL — full-detail buildings, cars, trees, props, shadows, connections
|
|
11
|
-
// SIMPLE — buildings only (no cars / trees / props / shadows)
|
|
12
|
-
// DOT — each building renders as a coloured dot (very-far-out zoom)
|
|
13
|
-
// CLUSTER— entire district collapses to a single cluster badge
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
export const LOD_FULL = 0; // zoom >= LOD_FULL_THRESHOLD
|
|
16
|
-
export const LOD_SIMPLE = 1; // zoom >= LOD_SIMPLE_THRESHOLD
|
|
17
|
-
export const LOD_DOT = 2; // zoom >= LOD_DOT_THRESHOLD
|
|
18
|
-
export const LOD_CLUSTER = 3; // zoom < LOD_DOT_THRESHOLD
|
|
19
|
-
|
|
20
|
-
// Zoom boundary constants (cam.zoom scale, matching renderer defaults)
|
|
21
|
-
export const LOD_FULL_THRESHOLD = 0.35; // below → drop cars/trees/props/shadows
|
|
22
|
-
export const LOD_SIMPLE_THRESHOLD = 0.12; // below → dots only
|
|
23
|
-
export const LOD_DOT_THRESHOLD = 0.06; // below → cluster badges
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Map a camera zoom value to an LOD tier.
|
|
27
|
-
* @param {number} zoom
|
|
28
|
-
* @returns {0|1|2|3}
|
|
29
|
-
*/
|
|
30
|
-
export function lodForZoom(zoom) {
|
|
31
|
-
if (zoom >= LOD_FULL_THRESHOLD) return LOD_FULL;
|
|
32
|
-
if (zoom >= LOD_SIMPLE_THRESHOLD) return LOD_SIMPLE;
|
|
33
|
-
if (zoom >= LOD_DOT_THRESHOLD) return LOD_DOT;
|
|
34
|
-
return LOD_CLUSTER;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Viewport culling
|
|
39
|
-
// Works in canvas/screen-space: test an iso-world point against a padded
|
|
40
|
-
// screen rectangle. Call once per draw to compute the clip region, then
|
|
41
|
-
// call isVisible() per object.
|
|
42
|
-
//
|
|
43
|
-
// cam = { x, y, zoom } (renderer camera object)
|
|
44
|
-
// cw = canvas CSS width
|
|
45
|
-
// ch = canvas CSS height
|
|
46
|
-
// pad = extra margin in world-units so objects at the edge aren't clipped
|
|
47
|
-
// (default: 2 tile-widths, 64px each → 128)
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Compute an axis-aligned world-space clip rect for the current camera view.
|
|
52
|
-
* Returns { wx0, wy0, wx1, wy1 } in *world* coordinates (pre-camera transform).
|
|
53
|
-
* @param {{ x: number, y: number, zoom: number }} cam
|
|
54
|
-
* @param {number} cw canvas CSS width
|
|
55
|
-
* @param {number} ch canvas CSS height
|
|
56
|
-
* @param {number} [pad=128] extra padding around the edges in world units
|
|
57
|
-
* @returns {{ wx0: number, wy0: number, wx1: number, wy1: number }}
|
|
58
|
-
*/
|
|
59
|
-
export function viewportClip(cam, cw, ch, pad = 128) {
|
|
60
|
-
// Screen corners → world coords: worldX = (screenX - cam.x) / cam.zoom
|
|
61
|
-
const wx0 = (0 - cam.x) / cam.zoom - pad;
|
|
62
|
-
const wy0 = (0 - cam.y) / cam.zoom - pad;
|
|
63
|
-
const wx1 = (cw - cam.x) / cam.zoom + pad;
|
|
64
|
-
const wy1 = (ch - cam.y) / cam.zoom + pad;
|
|
65
|
-
return { wx0, wy0, wx1, wy1 };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Return true if the iso-world point (wx, wy) is inside the clip rect.
|
|
70
|
-
* @param {number} wx
|
|
71
|
-
* @param {number} wy
|
|
72
|
-
* @param {{ wx0: number, wy0: number, wx1: number, wy1: number }} clip
|
|
73
|
-
* @returns {boolean}
|
|
74
|
-
*/
|
|
75
|
-
export function isVisible(wx, wy, clip) {
|
|
76
|
-
return wx >= clip.wx0 && wx <= clip.wx1 && wy >= clip.wy0 && wy <= clip.wy1;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Clustering
|
|
81
|
-
// Group city nodes by their district and compute a single representative
|
|
82
|
-
// "cluster badge" position + count. The badge sits at the
|
|
83
|
-
// district centroid in world-space.
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Build an array of cluster descriptors from the current NODES array.
|
|
88
|
-
* Each cluster has: { districtId, label, color, count, wx, wy }
|
|
89
|
-
*
|
|
90
|
-
* @param {Array<{ d: string, bx: number, by: number }>} nodes renderer-adapted nodes (with bx/by world pos)
|
|
91
|
-
* @param {{ [id: string]: { name: string, color: string } }} dcfg DCFG district config
|
|
92
|
-
* @returns {Array<{ districtId: string, label: string, color: string, count: number, wx: number, wy: number }>}
|
|
93
|
-
*/
|
|
94
|
-
export function buildClusters(nodes, dcfg) {
|
|
95
|
-
const acc = {};
|
|
96
|
-
for (const n of nodes) {
|
|
97
|
-
const k = n.d;
|
|
98
|
-
if (!acc[k]) acc[k] = { sumX: 0, sumY: 0, count: 0 };
|
|
99
|
-
acc[k].sumX += n.bx;
|
|
100
|
-
acc[k].sumY += n.by;
|
|
101
|
-
acc[k].count++;
|
|
102
|
-
}
|
|
103
|
-
return Object.keys(acc).map(k => {
|
|
104
|
-
const a = acc[k];
|
|
105
|
-
const cfg = dcfg[k] || { name: k, color: '#7c83a3' };
|
|
106
|
-
return {
|
|
107
|
-
districtId: k,
|
|
108
|
-
label: cfg.name,
|
|
109
|
-
color: cfg.color,
|
|
110
|
-
count: a.count,
|
|
111
|
-
wx: a.sumX / a.count,
|
|
112
|
-
wy: a.sumY / a.count,
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// Streaming geometry build
|
|
119
|
-
// For large repos (node count >= STREAM_THRESHOLD) instead of building the
|
|
120
|
-
// entire OBJ array synchronously during ingest, the renderer schedules small
|
|
121
|
-
// batches of geometry across animation frames. This function splits a nodes
|
|
122
|
-
// array into fixed-size batches.
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
/** Minimum node count before streaming geometry build is preferred. */
|
|
126
|
-
export const STREAM_THRESHOLD = 400;
|
|
127
|
-
|
|
128
|
-
/** Nodes processed per animation-frame batch. */
|
|
129
|
-
export const STREAM_BATCH_SIZE = 80;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Split an array into batches of `size`.
|
|
133
|
-
* @template T
|
|
134
|
-
* @param {T[]} items
|
|
135
|
-
* @param {number} [size=STREAM_BATCH_SIZE]
|
|
136
|
-
* @returns {T[][]}
|
|
137
|
-
*/
|
|
138
|
-
export function makeBatches(items, size = STREAM_BATCH_SIZE) {
|
|
139
|
-
if (!Array.isArray(items)) throw new TypeError('items must be an array');
|
|
140
|
-
if (!Number.isInteger(size) || size < 1) throw new RangeError('size must be a positive integer');
|
|
141
|
-
const batches = [];
|
|
142
|
-
for (let i = 0; i < items.length; i += size) {
|
|
143
|
-
batches.push(items.slice(i, i + size));
|
|
144
|
-
}
|
|
145
|
-
return batches;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Return true when the node count warrants streaming geometry build.
|
|
150
|
-
* @param {number} nodeCount
|
|
151
|
-
* @returns {boolean}
|
|
152
|
-
*/
|
|
153
|
-
export function shouldStream(nodeCount) {
|
|
154
|
-
return nodeCount >= STREAM_THRESHOLD;
|
|
155
|
-
}
|
|
1
|
+
// core/lod.mjs — Level-of-detail, viewport culling, clustering, and streaming
|
|
2
|
+
// utilities for large single-repo city graphs (SAT-447).
|
|
3
|
+
//
|
|
4
|
+
// All functions are pure (no side-effects, no DOM/canvas imports) so they can
|
|
5
|
+
// be used both in the Node.js test harness and inlined into the browser renderer.
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// LOD tier constants
|
|
9
|
+
// Each tier drives rendering fidelity in city.template.html.
|
|
10
|
+
// FULL — full-detail buildings, cars, trees, props, shadows, connections
|
|
11
|
+
// SIMPLE — buildings only (no cars / trees / props / shadows)
|
|
12
|
+
// DOT — each building renders as a coloured dot (very-far-out zoom)
|
|
13
|
+
// CLUSTER— entire district collapses to a single cluster badge
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export const LOD_FULL = 0; // zoom >= LOD_FULL_THRESHOLD
|
|
16
|
+
export const LOD_SIMPLE = 1; // zoom >= LOD_SIMPLE_THRESHOLD
|
|
17
|
+
export const LOD_DOT = 2; // zoom >= LOD_DOT_THRESHOLD
|
|
18
|
+
export const LOD_CLUSTER = 3; // zoom < LOD_DOT_THRESHOLD
|
|
19
|
+
|
|
20
|
+
// Zoom boundary constants (cam.zoom scale, matching renderer defaults)
|
|
21
|
+
export const LOD_FULL_THRESHOLD = 0.35; // below → drop cars/trees/props/shadows
|
|
22
|
+
export const LOD_SIMPLE_THRESHOLD = 0.12; // below → dots only
|
|
23
|
+
export const LOD_DOT_THRESHOLD = 0.06; // below → cluster badges
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Map a camera zoom value to an LOD tier.
|
|
27
|
+
* @param {number} zoom
|
|
28
|
+
* @returns {0|1|2|3}
|
|
29
|
+
*/
|
|
30
|
+
export function lodForZoom(zoom) {
|
|
31
|
+
if (zoom >= LOD_FULL_THRESHOLD) return LOD_FULL;
|
|
32
|
+
if (zoom >= LOD_SIMPLE_THRESHOLD) return LOD_SIMPLE;
|
|
33
|
+
if (zoom >= LOD_DOT_THRESHOLD) return LOD_DOT;
|
|
34
|
+
return LOD_CLUSTER;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Viewport culling
|
|
39
|
+
// Works in canvas/screen-space: test an iso-world point against a padded
|
|
40
|
+
// screen rectangle. Call once per draw to compute the clip region, then
|
|
41
|
+
// call isVisible() per object.
|
|
42
|
+
//
|
|
43
|
+
// cam = { x, y, zoom } (renderer camera object)
|
|
44
|
+
// cw = canvas CSS width
|
|
45
|
+
// ch = canvas CSS height
|
|
46
|
+
// pad = extra margin in world-units so objects at the edge aren't clipped
|
|
47
|
+
// (default: 2 tile-widths, 64px each → 128)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute an axis-aligned world-space clip rect for the current camera view.
|
|
52
|
+
* Returns { wx0, wy0, wx1, wy1 } in *world* coordinates (pre-camera transform).
|
|
53
|
+
* @param {{ x: number, y: number, zoom: number }} cam
|
|
54
|
+
* @param {number} cw canvas CSS width
|
|
55
|
+
* @param {number} ch canvas CSS height
|
|
56
|
+
* @param {number} [pad=128] extra padding around the edges in world units
|
|
57
|
+
* @returns {{ wx0: number, wy0: number, wx1: number, wy1: number }}
|
|
58
|
+
*/
|
|
59
|
+
export function viewportClip(cam, cw, ch, pad = 128) {
|
|
60
|
+
// Screen corners → world coords: worldX = (screenX - cam.x) / cam.zoom
|
|
61
|
+
const wx0 = (0 - cam.x) / cam.zoom - pad;
|
|
62
|
+
const wy0 = (0 - cam.y) / cam.zoom - pad;
|
|
63
|
+
const wx1 = (cw - cam.x) / cam.zoom + pad;
|
|
64
|
+
const wy1 = (ch - cam.y) / cam.zoom + pad;
|
|
65
|
+
return { wx0, wy0, wx1, wy1 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Return true if the iso-world point (wx, wy) is inside the clip rect.
|
|
70
|
+
* @param {number} wx
|
|
71
|
+
* @param {number} wy
|
|
72
|
+
* @param {{ wx0: number, wy0: number, wx1: number, wy1: number }} clip
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
export function isVisible(wx, wy, clip) {
|
|
76
|
+
return wx >= clip.wx0 && wx <= clip.wx1 && wy >= clip.wy0 && wy <= clip.wy1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Clustering
|
|
81
|
+
// Group city nodes by their district and compute a single representative
|
|
82
|
+
// "cluster badge" position + count. The badge sits at the
|
|
83
|
+
// district centroid in world-space.
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build an array of cluster descriptors from the current NODES array.
|
|
88
|
+
* Each cluster has: { districtId, label, color, count, wx, wy }
|
|
89
|
+
*
|
|
90
|
+
* @param {Array<{ d: string, bx: number, by: number }>} nodes renderer-adapted nodes (with bx/by world pos)
|
|
91
|
+
* @param {{ [id: string]: { name: string, color: string } }} dcfg DCFG district config
|
|
92
|
+
* @returns {Array<{ districtId: string, label: string, color: string, count: number, wx: number, wy: number }>}
|
|
93
|
+
*/
|
|
94
|
+
export function buildClusters(nodes, dcfg) {
|
|
95
|
+
const acc = {};
|
|
96
|
+
for (const n of nodes) {
|
|
97
|
+
const k = n.d;
|
|
98
|
+
if (!acc[k]) acc[k] = { sumX: 0, sumY: 0, count: 0 };
|
|
99
|
+
acc[k].sumX += n.bx;
|
|
100
|
+
acc[k].sumY += n.by;
|
|
101
|
+
acc[k].count++;
|
|
102
|
+
}
|
|
103
|
+
return Object.keys(acc).map(k => {
|
|
104
|
+
const a = acc[k];
|
|
105
|
+
const cfg = dcfg[k] || { name: k, color: '#7c83a3' };
|
|
106
|
+
return {
|
|
107
|
+
districtId: k,
|
|
108
|
+
label: cfg.name,
|
|
109
|
+
color: cfg.color,
|
|
110
|
+
count: a.count,
|
|
111
|
+
wx: a.sumX / a.count,
|
|
112
|
+
wy: a.sumY / a.count,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Streaming geometry build
|
|
119
|
+
// For large repos (node count >= STREAM_THRESHOLD) instead of building the
|
|
120
|
+
// entire OBJ array synchronously during ingest, the renderer schedules small
|
|
121
|
+
// batches of geometry across animation frames. This function splits a nodes
|
|
122
|
+
// array into fixed-size batches.
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/** Minimum node count before streaming geometry build is preferred. */
|
|
126
|
+
export const STREAM_THRESHOLD = 400;
|
|
127
|
+
|
|
128
|
+
/** Nodes processed per animation-frame batch. */
|
|
129
|
+
export const STREAM_BATCH_SIZE = 80;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Split an array into batches of `size`.
|
|
133
|
+
* @template T
|
|
134
|
+
* @param {T[]} items
|
|
135
|
+
* @param {number} [size=STREAM_BATCH_SIZE]
|
|
136
|
+
* @returns {T[][]}
|
|
137
|
+
*/
|
|
138
|
+
export function makeBatches(items, size = STREAM_BATCH_SIZE) {
|
|
139
|
+
if (!Array.isArray(items)) throw new TypeError('items must be an array');
|
|
140
|
+
if (!Number.isInteger(size) || size < 1) throw new RangeError('size must be a positive integer');
|
|
141
|
+
const batches = [];
|
|
142
|
+
for (let i = 0; i < items.length; i += size) {
|
|
143
|
+
batches.push(items.slice(i, i + size));
|
|
144
|
+
}
|
|
145
|
+
return batches;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Return true when the node count warrants streaming geometry build.
|
|
150
|
+
* @param {number} nodeCount
|
|
151
|
+
* @returns {boolean}
|
|
152
|
+
*/
|
|
153
|
+
export function shouldStream(nodeCount) {
|
|
154
|
+
return nodeCount >= STREAM_THRESHOLD;
|
|
155
|
+
}
|