@reqlan/analytical 0.5.4 → 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/README.md +22 -3
- package/out/analysis/git-dates-analyser.js +41 -12
- package/out/analysis/git-dates-analyser.js.map +1 -1
- package/out/core/analytical-store.d.ts +9 -2
- package/out/core/analytical-store.js +7 -5
- package/out/core/analytical-store.js.map +1 -1
- package/out/core/application-memory.d.ts +17 -2
- package/out/core/application-memory.js +23 -5
- package/out/core/application-memory.js.map +1 -1
- package/out/core/base-discovery.d.ts +40 -0
- package/out/core/base-discovery.js +166 -0
- package/out/core/base-discovery.js.map +1 -0
- package/out/core/context-model.d.ts +17 -0
- package/out/core/context-model.js.map +1 -1
- package/out/core/create-base.d.ts +11 -0
- package/out/core/create-base.js +33 -0
- package/out/core/create-base.js.map +1 -0
- package/out/core/rqignore.d.ts +25 -0
- package/out/core/rqignore.js +149 -0
- package/out/core/rqignore.js.map +1 -0
- package/out/core/types.d.ts +5 -1
- package/out/core/types.js.map +1 -1
- package/out/create-runtime.d.ts +29 -4
- package/out/create-runtime.js +47 -8
- package/out/create-runtime.js.map +1 -1
- package/out/export/html-export-assets.d.ts +2 -2
- package/out/export/html-export-assets.js +259 -239
- package/out/export/html-export-assets.js.map +1 -1
- package/out/graph/physics-core.d.ts +141 -0
- package/out/graph/physics-core.js +237 -0
- package/out/graph/physics-core.js.map +1 -0
- package/out/headless-index-service.d.ts +6 -23
- package/out/headless-index-service.js +5 -171
- package/out/headless-index-service.js.map +1 -1
- package/out/index-store/base-registry.d.ts +57 -0
- package/out/index-store/base-registry.js +192 -0
- package/out/index-store/base-registry.js.map +1 -0
- package/out/index-store/index-diagnostics-store.d.ts +72 -0
- package/out/index-store/index-diagnostics-store.js +238 -0
- package/out/index-store/index-diagnostics-store.js.map +1 -0
- package/out/index-store/index-file-error.d.ts +16 -0
- package/out/index-store/index-file-error.js +27 -0
- package/out/index-store/index-file-error.js.map +1 -0
- package/out/index-store/index-parse-issues.d.ts +9 -0
- package/out/index-store/index-parse-issues.js +62 -0
- package/out/index-store/index-parse-issues.js.map +1 -0
- package/out/index-store/index-status.d.ts +20 -0
- package/out/index-store/index-status.js +2 -0
- package/out/index-store/index-status.js.map +1 -0
- package/out/index-store/overview-coverage.d.ts +23 -0
- package/out/index-store/overview-coverage.js +254 -0
- package/out/index-store/overview-coverage.js.map +1 -0
- package/out/index-store/schema.d.ts +1 -1
- package/out/index-store/schema.js +4 -1
- package/out/index-store/schema.js.map +1 -1
- package/out/index-store/sqlite-store.d.ts +31 -2
- package/out/index-store/sqlite-store.js +152 -13
- package/out/index-store/sqlite-store.js.map +1 -1
- package/out/index-store/webview-table-queries.d.ts +65 -1
- package/out/index-store/webview-table-queries.js +188 -26
- package/out/index-store/webview-table-queries.js.map +1 -1
- package/out/index-store/workspace-index-file.d.ts +27 -0
- package/out/index-store/workspace-index-file.js +98 -0
- package/out/index-store/workspace-index-file.js.map +1 -0
- package/out/index-store/workspace-index-sync.d.ts +53 -0
- package/out/index-store/workspace-index-sync.js +164 -0
- package/out/index-store/workspace-index-sync.js.map +1 -0
- package/out/index-store/workspace-index.d.ts +80 -0
- package/out/index-store/workspace-index.js +525 -0
- package/out/index-store/workspace-index.js.map +1 -0
- package/out/index-store/workspace-mtime.d.ts +14 -0
- package/out/index-store/workspace-mtime.js +56 -0
- package/out/index-store/workspace-mtime.js.map +1 -0
- package/out/index.d.ts +25 -6
- package/out/index.js +12 -3
- package/out/index.js.map +1 -1
- package/package.json +7 -2
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/** Embed shared physics core into the classic-script export app.js (strip ESM exports). */
|
|
3
|
+
function embedPhysicsCoreSource() {
|
|
4
|
+
return readFileSync(new URL('../graph/physics-core.js', import.meta.url), 'utf8')
|
|
5
|
+
.replace(/^export\s+/gm, '');
|
|
6
|
+
}
|
|
1
7
|
export const SHARED_STYLES = `
|
|
2
8
|
:root {
|
|
3
9
|
color-scheme: dark;
|
|
@@ -263,56 +269,18 @@ body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is
|
|
|
263
269
|
background: linear-gradient(90deg, var(--accent-dim), var(--accent-strong));
|
|
264
270
|
min-width: 0;
|
|
265
271
|
}
|
|
266
|
-
.graph-root
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
fill: var(--accent); stroke: color-mix(in srgb, var(--fg) 35%, transparent); stroke-width: 1.25;
|
|
276
|
-
transition: fill 180ms ease, stroke 180ms ease, r 180ms ease;
|
|
277
|
-
}
|
|
278
|
-
.graph-root .external circle { fill: var(--rust-muted); stroke: color-mix(in srgb, var(--rust) 50%, transparent); }
|
|
279
|
-
.graph-root .ideaset circle { fill: color-mix(in srgb, #d18616 78%, white); stroke: #d18616; }
|
|
280
|
-
.graph-root .subject circle {
|
|
281
|
-
fill: var(--rust);
|
|
282
|
-
stroke: var(--accent-strong);
|
|
283
|
-
stroke-width: 2.75;
|
|
284
|
-
animation: subject-pulse 2.4s ease-in-out infinite;
|
|
285
|
-
}
|
|
286
|
-
.graph-root text {
|
|
287
|
-
fill: var(--fg); font-size: 11px; paint-order: stroke;
|
|
288
|
-
stroke: var(--bg); stroke-width: 3px; stroke-linejoin: round;
|
|
289
|
-
pointer-events: none;
|
|
290
|
-
}
|
|
291
|
-
.graph-root .subject text {
|
|
292
|
-
fill: var(--accent-strong); font-size: 12.5px; font-weight: 600;
|
|
293
|
-
}
|
|
294
|
-
.graph-root .node:hover circle { stroke: var(--accent-strong); stroke-width: 2; }
|
|
295
|
-
.graph-root .graph-label {
|
|
296
|
-
width: 160px;
|
|
297
|
-
color: var(--fg);
|
|
298
|
-
font: 11px/1.3 Inter, system-ui, sans-serif;
|
|
299
|
-
text-align: center;
|
|
300
|
-
word-break: break-word;
|
|
301
|
-
overflow-wrap: anywhere;
|
|
302
|
-
pointer-events: none;
|
|
303
|
-
user-select: none;
|
|
304
|
-
}
|
|
305
|
-
.graph-root .graph-label strong { display: block; font-weight: 600; }
|
|
306
|
-
.graph-root .graph-label .meta { display: block; color: var(--muted); font-size: 10px; margin-top: 0.15rem; }
|
|
307
|
-
.graph-root .graph-label .attrs { display: block; color: var(--rust); font-size: 9.5px; margin-top: 0.1rem; }
|
|
308
|
-
.graph-root .subject .graph-label strong { color: var(--accent-strong); }
|
|
309
|
-
.graph-root svg { touch-action: none; cursor: grab; }
|
|
310
|
-
.graph-root svg.is-panning { cursor: grabbing; }
|
|
311
|
-
.graph-root .node { cursor: pointer; }
|
|
312
|
-
@keyframes subject-pulse {
|
|
313
|
-
0%, 100% { filter: drop-shadow(0 0 0 transparent); }
|
|
314
|
-
50% { filter: drop-shadow(0 0 8px color-mix(in srgb, var(--rust) 55%, transparent)); }
|
|
272
|
+
.graph-root canvas {
|
|
273
|
+
display: block;
|
|
274
|
+
width: 100%;
|
|
275
|
+
min-height: 620px;
|
|
276
|
+
background: var(--bg);
|
|
277
|
+
border-radius: 10px;
|
|
278
|
+
border: 1px solid var(--line);
|
|
279
|
+
touch-action: none;
|
|
280
|
+
cursor: grab;
|
|
315
281
|
}
|
|
282
|
+
.graph-root canvas.is-panning { cursor: grabbing; }
|
|
283
|
+
.graph-root canvas.is-dragging-node { cursor: pointer; }
|
|
316
284
|
.search-results { display: grid; gap: 0.7rem; margin-top: 1rem; }
|
|
317
285
|
.search-result:hover { border-color: var(--accent-dim); }
|
|
318
286
|
.hidden { display: none !important; }
|
|
@@ -384,6 +352,8 @@ pre.code-like {
|
|
|
384
352
|
}
|
|
385
353
|
`;
|
|
386
354
|
export const APP_JS = `
|
|
355
|
+
${embedPhysicsCoreSource()}
|
|
356
|
+
|
|
387
357
|
const searchIndexByRoot = new WeakMap();
|
|
388
358
|
|
|
389
359
|
function exportRootPrefix(root) {
|
|
@@ -658,26 +628,13 @@ function wireTables(root) {
|
|
|
658
628
|
}
|
|
659
629
|
}
|
|
660
630
|
|
|
661
|
-
|
|
662
|
-
let hash = 0;
|
|
663
|
-
for (let index = 0; index < seed.length; index += 1) {
|
|
664
|
-
hash = (hash * 31 + seed.charCodeAt(index)) | 0;
|
|
665
|
-
}
|
|
666
|
-
return ((hash >>> 0) % 6283) / 1000;
|
|
667
|
-
}
|
|
631
|
+
const EXPORT_PHYSICS_SETTINGS = ReqlanGraphPhysics.DEFAULT_PHYSICS_SETTINGS;
|
|
668
632
|
|
|
669
|
-
//
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
linkDistance: 120,
|
|
675
|
-
damping: 0.5,
|
|
676
|
-
maxVelocity: 10,
|
|
677
|
-
minSeparation: 24,
|
|
678
|
-
restSpeed: 0.02,
|
|
679
|
-
restTicks: 90
|
|
680
|
-
};
|
|
633
|
+
// Tight visual radii for denser canvas packing (labels still show full names).
|
|
634
|
+
const GRAPH_NODE_RADIUS = 7;
|
|
635
|
+
const GRAPH_SUBJECT_RADIUS = 10;
|
|
636
|
+
const GRAPH_LABEL_MAX_WIDTH = 132;
|
|
637
|
+
const GRAPH_HIT_PAD = 6;
|
|
681
638
|
|
|
682
639
|
function wireGraph(root) {
|
|
683
640
|
const graphRoots = root.querySelectorAll('[data-graph-json]');
|
|
@@ -718,9 +675,10 @@ function wireGraph(root) {
|
|
|
718
675
|
let simEdges = [];
|
|
719
676
|
const positions = new Map();
|
|
720
677
|
const velocities = new Map();
|
|
721
|
-
let
|
|
722
|
-
let
|
|
723
|
-
let
|
|
678
|
+
let canvas;
|
|
679
|
+
let ctx;
|
|
680
|
+
let cssWidth = 1200;
|
|
681
|
+
let cssHeight = 640;
|
|
724
682
|
const pinnedIds = new Set();
|
|
725
683
|
let view = { x: 0, y: 0, w: 1200, h: 640 };
|
|
726
684
|
let userViewport = false;
|
|
@@ -729,6 +687,8 @@ function wireGraph(root) {
|
|
|
729
687
|
let panMode = false;
|
|
730
688
|
let panOrigin = null;
|
|
731
689
|
let suppressClickUntil = 0;
|
|
690
|
+
let hoverNodeId = null;
|
|
691
|
+
let pulsePhase = 0;
|
|
732
692
|
|
|
733
693
|
function resolveGraphNodeUrl(pageUrl) {
|
|
734
694
|
let cleaned = String(pageUrl || '').replace(/^\\.\\//, '');
|
|
@@ -738,14 +698,6 @@ function wireGraph(root) {
|
|
|
738
698
|
return resolveExportUrl(root, cleaned);
|
|
739
699
|
}
|
|
740
700
|
|
|
741
|
-
function escapeXml(value) {
|
|
742
|
-
return String(value || '')
|
|
743
|
-
.replace(/&/g, '&')
|
|
744
|
-
.replace(/</g, '<')
|
|
745
|
-
.replace(/>/g, '>')
|
|
746
|
-
.replace(/"/g, '"');
|
|
747
|
-
}
|
|
748
|
-
|
|
749
701
|
function formatNodeMeta(node) {
|
|
750
702
|
const bits = [];
|
|
751
703
|
if (node.status) bits.push(String(node.status));
|
|
@@ -760,11 +712,15 @@ function wireGraph(root) {
|
|
|
760
712
|
return keys.length ? keys.map(key => '@' + key).join(' ') : '';
|
|
761
713
|
}
|
|
762
714
|
|
|
715
|
+
function nodeRadius(node) {
|
|
716
|
+
return node.isSubject || node.id === graph.centerId ? GRAPH_SUBJECT_RADIUS : GRAPH_NODE_RADIUS;
|
|
717
|
+
}
|
|
718
|
+
|
|
763
719
|
function seedMissingPositions(nodes, width, height, centerId) {
|
|
764
720
|
const cx = width / 2;
|
|
765
721
|
const cy = height / 2;
|
|
766
722
|
const subject = nodes.find(node => node.isSubject || node.id === centerId);
|
|
767
|
-
const radius = Math.min(width, height) * 0.
|
|
723
|
+
const radius = Math.min(width, height) * 0.22;
|
|
768
724
|
nodes.forEach((node, index) => {
|
|
769
725
|
if (positions.has(node.id)) return;
|
|
770
726
|
if (subject && node.id === subject.id) {
|
|
@@ -772,7 +728,7 @@ function wireGraph(root) {
|
|
|
772
728
|
return;
|
|
773
729
|
}
|
|
774
730
|
const angle = (Math.PI * 2 * index) / Math.max(nodes.length, 1) - Math.PI / 2;
|
|
775
|
-
const ring = radius * (0.
|
|
731
|
+
const ring = radius * (0.7 + (index % 3) * 0.12);
|
|
776
732
|
positions.set(node.id, {
|
|
777
733
|
x: cx + Math.cos(angle) * ring,
|
|
778
734
|
y: cy + Math.sin(angle) * ring
|
|
@@ -790,7 +746,6 @@ function wireGraph(root) {
|
|
|
790
746
|
}
|
|
791
747
|
|
|
792
748
|
function physicsStep(nodes, edges) {
|
|
793
|
-
const settings = EXPORT_PHYSICS_SETTINGS;
|
|
794
749
|
const count = nodes.length;
|
|
795
750
|
if (count === 0) return 0;
|
|
796
751
|
const ids = new Array(count);
|
|
@@ -798,90 +753,27 @@ function wireGraph(root) {
|
|
|
798
753
|
const ys = new Float64Array(count);
|
|
799
754
|
const fxs = new Float64Array(count);
|
|
800
755
|
const fys = new Float64Array(count);
|
|
801
|
-
const indexById = new Map();
|
|
802
756
|
for (let i = 0; i < count; i += 1) {
|
|
803
757
|
const node = nodes[i];
|
|
804
758
|
const pos = positions.get(node.id) || { x: 0, y: 0 };
|
|
805
759
|
ids[i] = node.id;
|
|
806
760
|
xs[i] = pos.x;
|
|
807
761
|
ys[i] = pos.y;
|
|
808
|
-
indexById.set(node.id, i);
|
|
809
|
-
}
|
|
810
|
-
let centroidX = 0;
|
|
811
|
-
let centroidY = 0;
|
|
812
|
-
for (let i = 0; i < count; i += 1) {
|
|
813
|
-
centroidX += xs[i];
|
|
814
|
-
centroidY += ys[i];
|
|
815
|
-
}
|
|
816
|
-
centroidX /= count;
|
|
817
|
-
centroidY /= count;
|
|
818
|
-
for (let i = 0; i < count; i += 1) {
|
|
819
|
-
fxs[i] -= settings.gravity * (xs[i] - centroidX);
|
|
820
|
-
fys[i] -= settings.gravity * (ys[i] - centroidY);
|
|
821
|
-
}
|
|
822
|
-
const minSeparationSq = settings.minSeparation * settings.minSeparation;
|
|
823
|
-
for (let i = 0; i < count; i += 1) {
|
|
824
|
-
for (let j = i + 1; j < count; j += 1) {
|
|
825
|
-
let dx = xs[j] - xs[i];
|
|
826
|
-
let dy = ys[j] - ys[i];
|
|
827
|
-
let distSq = dx * dx + dy * dy;
|
|
828
|
-
if (distSq < 1e-6) {
|
|
829
|
-
const angle = hashAngle(ids[i] + ids[j]);
|
|
830
|
-
dx = Math.cos(angle);
|
|
831
|
-
dy = Math.sin(angle);
|
|
832
|
-
distSq = 1;
|
|
833
|
-
}
|
|
834
|
-
const clampedSq = Math.max(distSq, minSeparationSq);
|
|
835
|
-
const dist = Math.sqrt(distSq);
|
|
836
|
-
const force = settings.repulsion / clampedSq;
|
|
837
|
-
const fx = (dx / dist) * force;
|
|
838
|
-
const fy = (dy / dist) * force;
|
|
839
|
-
fxs[i] -= fx;
|
|
840
|
-
fys[i] -= fy;
|
|
841
|
-
fxs[j] += fx;
|
|
842
|
-
fys[j] += fy;
|
|
843
|
-
}
|
|
844
762
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const fy = (dy / dist) * force;
|
|
856
|
-
fxs[sourceIndex] += fx;
|
|
857
|
-
fys[sourceIndex] += fy;
|
|
858
|
-
fxs[targetIndex] -= fx;
|
|
859
|
-
fys[targetIndex] -= fy;
|
|
860
|
-
}
|
|
861
|
-
let speedSum = 0;
|
|
862
|
-
let movingCount = 0;
|
|
763
|
+
const averageSpeed = ReqlanGraphPhysics.stepPhysics({
|
|
764
|
+
ids,
|
|
765
|
+
xs,
|
|
766
|
+
ys,
|
|
767
|
+
fxs,
|
|
768
|
+
fys,
|
|
769
|
+
velocities,
|
|
770
|
+
pinnedIds,
|
|
771
|
+
edges
|
|
772
|
+
}, EXPORT_PHYSICS_SETTINGS);
|
|
863
773
|
for (let i = 0; i < count; i += 1) {
|
|
864
|
-
|
|
865
|
-
if (pinnedIds.has(id)) {
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
const velocity = velocities.get(id) || { vx: 0, vy: 0 };
|
|
869
|
-
let vx = (velocity.vx + fxs[i]) * settings.damping;
|
|
870
|
-
let vy = (velocity.vy + fys[i]) * settings.damping;
|
|
871
|
-
const speed = Math.sqrt(vx * vx + vy * vy);
|
|
872
|
-
if (speed > settings.maxVelocity) {
|
|
873
|
-
const scale = settings.maxVelocity / speed;
|
|
874
|
-
vx *= scale;
|
|
875
|
-
vy *= scale;
|
|
876
|
-
}
|
|
877
|
-
velocity.vx = vx;
|
|
878
|
-
velocity.vy = vy;
|
|
879
|
-
velocities.set(id, velocity);
|
|
880
|
-
positions.set(id, { x: xs[i] + vx, y: ys[i] + vy });
|
|
881
|
-
speedSum += Math.min(speed, settings.maxVelocity);
|
|
882
|
-
movingCount += 1;
|
|
774
|
+
positions.set(ids[i], { x: xs[i], y: ys[i] });
|
|
883
775
|
}
|
|
884
|
-
return
|
|
776
|
+
return averageSpeed;
|
|
885
777
|
}
|
|
886
778
|
|
|
887
779
|
function contentBounds() {
|
|
@@ -889,11 +781,14 @@ function wireGraph(root) {
|
|
|
889
781
|
let minY = Infinity;
|
|
890
782
|
let maxX = -Infinity;
|
|
891
783
|
let maxY = -Infinity;
|
|
892
|
-
for (const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
784
|
+
for (const node of simNodes) {
|
|
785
|
+
const pos = positions.get(node.id);
|
|
786
|
+
if (!pos) continue;
|
|
787
|
+
const r = nodeRadius(node);
|
|
788
|
+
minX = Math.min(minX, pos.x - r);
|
|
789
|
+
minY = Math.min(minY, pos.y - r);
|
|
790
|
+
maxX = Math.max(maxX, pos.x + r);
|
|
791
|
+
maxY = Math.max(maxY, pos.y + r + 54);
|
|
897
792
|
}
|
|
898
793
|
if (!Number.isFinite(minX)) {
|
|
899
794
|
return { minX: 0, minY: 0, maxX: 1200, maxY: 640 };
|
|
@@ -901,53 +796,155 @@ function wireGraph(root) {
|
|
|
901
796
|
return { minX, minY, maxX, maxY };
|
|
902
797
|
}
|
|
903
798
|
|
|
904
|
-
function applyView() {
|
|
905
|
-
if (!svg) return;
|
|
906
|
-
svg.setAttribute('viewBox', \`\${view.x} \${view.y} \${view.w} \${view.h}\`);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
799
|
function fitView(force = false) {
|
|
910
|
-
if (userViewport && !force)
|
|
911
|
-
applyView();
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
800
|
+
if (userViewport && !force) return;
|
|
914
801
|
const bounds = contentBounds();
|
|
915
|
-
const pad =
|
|
802
|
+
const pad = 56;
|
|
916
803
|
view = {
|
|
917
804
|
x: bounds.minX - pad,
|
|
918
805
|
y: bounds.minY - pad,
|
|
919
806
|
w: Math.max(360, bounds.maxX - bounds.minX + pad * 2),
|
|
920
|
-
h: Math.max(360, bounds.maxY - bounds.minY + pad * 2
|
|
807
|
+
h: Math.max(360, bounds.maxY - bounds.minY + pad * 2)
|
|
921
808
|
};
|
|
922
809
|
userViewport = false;
|
|
923
|
-
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function wrapLines(text, maxWidth, font) {
|
|
813
|
+
if (!text) return [];
|
|
814
|
+
ctx.font = font;
|
|
815
|
+
const words = String(text).split(/\\s+/);
|
|
816
|
+
const lines = [];
|
|
817
|
+
let current = '';
|
|
818
|
+
for (const word of words) {
|
|
819
|
+
const next = current ? \`\${current} \${word}\` : word;
|
|
820
|
+
if (ctx.measureText(next).width <= maxWidth) {
|
|
821
|
+
current = next;
|
|
822
|
+
} else {
|
|
823
|
+
if (current) lines.push(current);
|
|
824
|
+
if (ctx.measureText(word).width <= maxWidth) {
|
|
825
|
+
current = word;
|
|
826
|
+
} else {
|
|
827
|
+
let chunk = '';
|
|
828
|
+
for (const ch of word) {
|
|
829
|
+
const trial = chunk + ch;
|
|
830
|
+
if (ctx.measureText(trial).width <= maxWidth) chunk = trial;
|
|
831
|
+
else {
|
|
832
|
+
if (chunk) lines.push(chunk);
|
|
833
|
+
chunk = ch;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
current = chunk;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (current) lines.push(current);
|
|
841
|
+
return lines;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function nodeFill(node) {
|
|
845
|
+
if (node.isSubject || node.id === graph.centerId) return '#b85c38';
|
|
846
|
+
if (node.isExternal) return '#8a4530';
|
|
847
|
+
if (node.kind === 'ideaset') return '#e0a24a';
|
|
848
|
+
return '#07a0e5';
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function nodeStroke(node, hover) {
|
|
852
|
+
if (hover) return '#0bbefb';
|
|
853
|
+
if (node.isSubject || node.id === graph.centerId) return '#0bbefb';
|
|
854
|
+
if (node.isExternal) return 'rgba(184,92,56,0.55)';
|
|
855
|
+
if (node.kind === 'ideaset') return '#d18616';
|
|
856
|
+
return 'rgba(235,228,222,0.35)';
|
|
924
857
|
}
|
|
925
858
|
|
|
926
859
|
function paint() {
|
|
927
|
-
if (!
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
860
|
+
if (!canvas || !ctx) return;
|
|
861
|
+
if (!userViewport) fitView(false);
|
|
862
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
|
863
|
+
const nextW = Math.max(1, Math.floor(cssWidth * dpr));
|
|
864
|
+
const nextH = Math.max(1, Math.floor(cssHeight * dpr));
|
|
865
|
+
if (canvas.width !== nextW || canvas.height !== nextH) {
|
|
866
|
+
canvas.width = nextW;
|
|
867
|
+
canvas.height = nextH;
|
|
935
868
|
}
|
|
936
|
-
|
|
869
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
870
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
871
|
+
ctx.fillStyle = '#14100e';
|
|
872
|
+
ctx.fillRect(0, 0, cssWidth, cssHeight);
|
|
873
|
+
|
|
874
|
+
const sx = cssWidth / view.w;
|
|
875
|
+
const sy = cssHeight / view.h;
|
|
876
|
+
ctx.save();
|
|
877
|
+
ctx.translate(-view.x * sx, -view.y * sy);
|
|
878
|
+
ctx.scale(sx, sy);
|
|
879
|
+
|
|
880
|
+
ctx.lineWidth = 1.1 / Math.max(sx, sy);
|
|
881
|
+
ctx.strokeStyle = 'rgba(61,47,40,0.85)';
|
|
882
|
+
ctx.globalAlpha = 0.85;
|
|
883
|
+
for (const edge of simEdges) {
|
|
937
884
|
const source = positions.get(edge.sourceId);
|
|
938
885
|
const target = positions.get(edge.targetId);
|
|
939
886
|
if (!source || !target) continue;
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
line.setAttribute('opacity', '0.85');
|
|
887
|
+
ctx.beginPath();
|
|
888
|
+
ctx.moveTo(source.x, source.y);
|
|
889
|
+
ctx.lineTo(target.x, target.y);
|
|
890
|
+
ctx.stroke();
|
|
945
891
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
892
|
+
ctx.globalAlpha = 1;
|
|
893
|
+
|
|
894
|
+
pulsePhase = (pulsePhase + 0.035) % (Math.PI * 2);
|
|
895
|
+
for (const node of simNodes) {
|
|
896
|
+
const pos = positions.get(node.id);
|
|
897
|
+
if (!pos) continue;
|
|
898
|
+
const r = nodeRadius(node);
|
|
899
|
+
const hover = node.id === hoverNodeId || node.id === dragNodeId;
|
|
900
|
+
const isSubject = node.isSubject || node.id === graph.centerId;
|
|
901
|
+
if (isSubject) {
|
|
902
|
+
const glow = 4 + Math.sin(pulsePhase) * 3;
|
|
903
|
+
ctx.beginPath();
|
|
904
|
+
ctx.arc(pos.x, pos.y, r + glow, 0, Math.PI * 2);
|
|
905
|
+
ctx.fillStyle = 'rgba(184,92,56,0.22)';
|
|
906
|
+
ctx.fill();
|
|
907
|
+
}
|
|
908
|
+
ctx.beginPath();
|
|
909
|
+
ctx.arc(pos.x, pos.y, r, 0, Math.PI * 2);
|
|
910
|
+
ctx.fillStyle = nodeFill(node);
|
|
911
|
+
ctx.fill();
|
|
912
|
+
ctx.lineWidth = (isSubject ? 2.2 : hover ? 2 : 1.15) / Math.max(sx, sy);
|
|
913
|
+
ctx.strokeStyle = nodeStroke(node, hover);
|
|
914
|
+
ctx.stroke();
|
|
915
|
+
|
|
916
|
+
const meta = formatNodeMeta(node);
|
|
917
|
+
const attrs = formatNodeAttrs(node);
|
|
918
|
+
const nameFont = isSubject ? '600 11px Inter, system-ui, sans-serif' : '600 10px Inter, system-ui, sans-serif';
|
|
919
|
+
const metaFont = '9.5px Inter, system-ui, sans-serif';
|
|
920
|
+
const nameLines = wrapLines(node.name, GRAPH_LABEL_MAX_WIDTH, nameFont);
|
|
921
|
+
const metaLines = wrapLines(meta, GRAPH_LABEL_MAX_WIDTH, metaFont);
|
|
922
|
+
const attrLines = wrapLines(attrs, GRAPH_LABEL_MAX_WIDTH, metaFont);
|
|
923
|
+
let ty = pos.y + r + 12;
|
|
924
|
+
ctx.textAlign = 'center';
|
|
925
|
+
ctx.textBaseline = 'top';
|
|
926
|
+
ctx.fillStyle = isSubject ? '#0bbefb' : '#ebe4de';
|
|
927
|
+
ctx.font = nameFont;
|
|
928
|
+
for (const line of nameLines) {
|
|
929
|
+
ctx.lineWidth = 3 / Math.max(sx, sy);
|
|
930
|
+
ctx.strokeStyle = '#14100e';
|
|
931
|
+
ctx.strokeText(line, pos.x, ty);
|
|
932
|
+
ctx.fillText(line, pos.x, ty);
|
|
933
|
+
ty += 12;
|
|
934
|
+
}
|
|
935
|
+
ctx.fillStyle = '#a89488';
|
|
936
|
+
ctx.font = metaFont;
|
|
937
|
+
for (const line of metaLines) {
|
|
938
|
+
ctx.fillText(line, pos.x, ty);
|
|
939
|
+
ty += 11;
|
|
940
|
+
}
|
|
941
|
+
ctx.fillStyle = '#b85c38';
|
|
942
|
+
for (const line of attrLines) {
|
|
943
|
+
ctx.fillText(line, pos.x, ty);
|
|
944
|
+
ty += 11;
|
|
945
|
+
}
|
|
950
946
|
}
|
|
947
|
+
ctx.restore();
|
|
951
948
|
}
|
|
952
949
|
|
|
953
950
|
function stopLoop() {
|
|
@@ -961,7 +958,10 @@ function wireGraph(root) {
|
|
|
961
958
|
if (animationFrame) return;
|
|
962
959
|
animationFrame = requestAnimationFrame(() => {
|
|
963
960
|
animationFrame = 0;
|
|
964
|
-
if (!livePhysics)
|
|
961
|
+
if (!livePhysics) {
|
|
962
|
+
paint();
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
965
|
const averageSpeed = physicsStep(simNodes, simEdges);
|
|
966
966
|
paint();
|
|
967
967
|
if (averageSpeed < EXPORT_PHYSICS_SETTINGS.restSpeed) {
|
|
@@ -982,7 +982,7 @@ function wireGraph(root) {
|
|
|
982
982
|
}
|
|
983
983
|
|
|
984
984
|
function batchSettle(nodes, edges) {
|
|
985
|
-
const iterations = Math.min(
|
|
985
|
+
const iterations = Math.min(96, 36 + Math.floor(nodes.length * 0.45));
|
|
986
986
|
for (let i = 0; i < iterations; i += 1) {
|
|
987
987
|
physicsStep(nodes, edges);
|
|
988
988
|
}
|
|
@@ -993,6 +993,34 @@ function wireGraph(root) {
|
|
|
993
993
|
calmTicks = EXPORT_PHYSICS_SETTINGS.restTicks;
|
|
994
994
|
}
|
|
995
995
|
|
|
996
|
+
function hitTest(point) {
|
|
997
|
+
let best = null;
|
|
998
|
+
let bestDist = Infinity;
|
|
999
|
+
for (const node of simNodes) {
|
|
1000
|
+
const pos = positions.get(node.id);
|
|
1001
|
+
if (!pos) continue;
|
|
1002
|
+
const dx = point.x - pos.x;
|
|
1003
|
+
const dy = point.y - pos.y;
|
|
1004
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1005
|
+
const threshold = nodeRadius(node) + GRAPH_HIT_PAD;
|
|
1006
|
+
if (dist <= threshold && dist < bestDist) {
|
|
1007
|
+
best = node;
|
|
1008
|
+
bestDist = dist;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return best;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function resizeCanvas() {
|
|
1015
|
+
const rect = element.getBoundingClientRect();
|
|
1016
|
+
cssWidth = Math.max(320, Math.floor(rect.width || 1200));
|
|
1017
|
+
cssHeight = Math.max(620, Math.min(1400, 220 + simNodes.length * 18));
|
|
1018
|
+
if (canvas) {
|
|
1019
|
+
canvas.style.width = '100%';
|
|
1020
|
+
canvas.style.height = \`\${cssHeight}px\`;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
996
1024
|
function mountGraph(filteredNodes, filteredEdges) {
|
|
997
1025
|
stopLoop();
|
|
998
1026
|
simNodes = filteredNodes;
|
|
@@ -1000,44 +1028,16 @@ function wireGraph(root) {
|
|
|
1000
1028
|
const validIds = new Set(filteredNodes.map(node => node.id));
|
|
1001
1029
|
pruneState(validIds);
|
|
1002
1030
|
const width = 1200;
|
|
1003
|
-
const height = Math.max(640, Math.min(1400, 220 + filteredNodes.length *
|
|
1031
|
+
const height = Math.max(640, Math.min(1400, 220 + filteredNodes.length * 18));
|
|
1004
1032
|
seedMissingPositions(filteredNodes, width, height, graph.centerId);
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
1010
|
-
svg.appendChild(line);
|
|
1011
|
-
edgeEls.push({ edge, line });
|
|
1012
|
-
}
|
|
1013
|
-
nodeEls = [];
|
|
1014
|
-
for (const node of filteredNodes) {
|
|
1015
|
-
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
1016
|
-
const classes = ['node'];
|
|
1017
|
-
if (node.isExternal) classes.push('external');
|
|
1018
|
-
if (node.kind === 'ideaset') classes.push('ideaset');
|
|
1019
|
-
if (node.isSubject || node.id === graph.centerId) classes.push('subject');
|
|
1020
|
-
group.setAttribute('class', classes.join(' '));
|
|
1021
|
-
group.dataset.nodeId = node.id;
|
|
1022
|
-
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
1023
|
-
circle.setAttribute('r', node.isSubject || node.id === graph.centerId ? '18' : '13');
|
|
1024
|
-
const label = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
|
|
1025
|
-
label.setAttribute('width', '160');
|
|
1026
|
-
label.setAttribute('height', '96');
|
|
1027
|
-
const meta = formatNodeMeta(node);
|
|
1028
|
-
const attrs = formatNodeAttrs(node);
|
|
1029
|
-
label.innerHTML = \`<div xmlns="http://www.w3.org/1999/xhtml" class="graph-label"><strong>\${escapeXml(node.name)}</strong>\${meta ? \`<span class="meta">\${escapeXml(meta)}</span>\` : ''}\${attrs ? \`<span class="attrs">\${escapeXml(attrs)}</span>\` : ''}</div>\`;
|
|
1030
|
-
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
1031
|
-
title.textContent = [node.name, meta, attrs].filter(Boolean).join('\\n');
|
|
1032
|
-
group.appendChild(title);
|
|
1033
|
-
group.appendChild(circle);
|
|
1034
|
-
group.appendChild(label);
|
|
1035
|
-
svg.appendChild(group);
|
|
1036
|
-
nodeEls.push({ node, circle, label, group });
|
|
1037
|
-
}
|
|
1033
|
+
canvas = document.createElement('canvas');
|
|
1034
|
+
canvas.setAttribute('role', 'img');
|
|
1035
|
+
canvas.setAttribute('aria-label', \`Requirement graph with \${filteredNodes.length} nodes\`);
|
|
1036
|
+
ctx = canvas.getContext('2d');
|
|
1038
1037
|
element.innerHTML = '';
|
|
1039
|
-
element.appendChild(
|
|
1040
|
-
|
|
1038
|
+
element.appendChild(canvas);
|
|
1039
|
+
resizeCanvas();
|
|
1040
|
+
wireViewport(canvas);
|
|
1041
1041
|
if (statusText) {
|
|
1042
1042
|
statusText.textContent = \`\${filteredNodes.length} nodes, \${filteredEdges.length} edges\`;
|
|
1043
1043
|
}
|
|
@@ -1052,7 +1052,7 @@ function wireGraph(root) {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
1054
|
function clientToGraph(event) {
|
|
1055
|
-
const rect =
|
|
1055
|
+
const rect = canvas.getBoundingClientRect();
|
|
1056
1056
|
const x = view.x + ((event.clientX - rect.left) / rect.width) * view.w;
|
|
1057
1057
|
const y = view.y + ((event.clientY - rect.top) / rect.height) * view.h;
|
|
1058
1058
|
return { x, y };
|
|
@@ -1069,19 +1069,21 @@ function wireGraph(root) {
|
|
|
1069
1069
|
view.x += before.x - after.x;
|
|
1070
1070
|
view.y += before.y - after.y;
|
|
1071
1071
|
userViewport = true;
|
|
1072
|
-
|
|
1072
|
+
paint();
|
|
1073
1073
|
}, { passive: false });
|
|
1074
1074
|
|
|
1075
1075
|
target.addEventListener('pointerdown', (event) => {
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
dragNodeId =
|
|
1076
|
+
const point = clientToGraph(event);
|
|
1077
|
+
const hit = hitTest(point);
|
|
1078
|
+
if (hit) {
|
|
1079
|
+
dragNodeId = hit.id;
|
|
1080
1080
|
dragMoved = false;
|
|
1081
|
-
pinnedIds.add(
|
|
1082
|
-
velocities.set(
|
|
1081
|
+
pinnedIds.add(hit.id);
|
|
1082
|
+
velocities.set(hit.id, { vx: 0, vy: 0 });
|
|
1083
|
+
target.classList.add('is-dragging-node');
|
|
1083
1084
|
target.setPointerCapture(event.pointerId);
|
|
1084
1085
|
event.preventDefault();
|
|
1086
|
+
paint();
|
|
1085
1087
|
return;
|
|
1086
1088
|
}
|
|
1087
1089
|
panMode = true;
|
|
@@ -1107,11 +1109,19 @@ function wireGraph(root) {
|
|
|
1107
1109
|
view.x = panOrigin.viewX - dx;
|
|
1108
1110
|
view.y = panOrigin.viewY - dy;
|
|
1109
1111
|
userViewport = true;
|
|
1110
|
-
|
|
1112
|
+
paint();
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const hit = hitTest(clientToGraph(event));
|
|
1116
|
+
const nextHover = hit?.id || null;
|
|
1117
|
+
if (nextHover !== hoverNodeId) {
|
|
1118
|
+
hoverNodeId = nextHover;
|
|
1119
|
+
paint();
|
|
1111
1120
|
}
|
|
1112
1121
|
});
|
|
1113
1122
|
|
|
1114
1123
|
target.addEventListener('pointerup', (event) => {
|
|
1124
|
+
target.classList.remove('is-dragging-node');
|
|
1115
1125
|
if (dragNodeId) {
|
|
1116
1126
|
const node = simNodes.find(item => item.id === dragNodeId);
|
|
1117
1127
|
const moved = dragMoved;
|
|
@@ -1119,6 +1129,7 @@ function wireGraph(root) {
|
|
|
1119
1129
|
dragNodeId = null;
|
|
1120
1130
|
pinnedIds.delete(nodeId);
|
|
1121
1131
|
wake();
|
|
1132
|
+
paint();
|
|
1122
1133
|
if (!moved && node?.pageUrl && Date.now() > suppressClickUntil) {
|
|
1123
1134
|
window.location.href = resolveGraphNodeUrl(node.pageUrl);
|
|
1124
1135
|
} else if (moved) {
|
|
@@ -1134,6 +1145,7 @@ function wireGraph(root) {
|
|
|
1134
1145
|
});
|
|
1135
1146
|
|
|
1136
1147
|
target.addEventListener('pointercancel', () => {
|
|
1148
|
+
target.classList.remove('is-dragging-node');
|
|
1137
1149
|
if (dragNodeId) {
|
|
1138
1150
|
pinnedIds.delete(dragNodeId);
|
|
1139
1151
|
dragNodeId = null;
|
|
@@ -1142,6 +1154,12 @@ function wireGraph(root) {
|
|
|
1142
1154
|
panMode = false;
|
|
1143
1155
|
panOrigin = null;
|
|
1144
1156
|
target.classList.remove('is-panning');
|
|
1157
|
+
paint();
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
window.addEventListener('resize', () => {
|
|
1161
|
+
resizeCanvas();
|
|
1162
|
+
paint();
|
|
1145
1163
|
});
|
|
1146
1164
|
}
|
|
1147
1165
|
|
|
@@ -1193,10 +1211,12 @@ function wireGraph(root) {
|
|
|
1193
1211
|
wake();
|
|
1194
1212
|
} else {
|
|
1195
1213
|
stopLoop();
|
|
1214
|
+
paint();
|
|
1196
1215
|
}
|
|
1197
1216
|
});
|
|
1198
1217
|
fitButton?.addEventListener('click', () => {
|
|
1199
1218
|
fitView(true);
|
|
1219
|
+
paint();
|
|
1200
1220
|
});
|
|
1201
1221
|
resetButton?.addEventListener('click', () => {
|
|
1202
1222
|
if (searchInput) searchInput.value = '';
|