@n-dx/web 0.1.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/LICENSE +96 -0
- package/README.md +5 -0
- package/build.js +243 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +50 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/landing/index.html +325 -0
- package/dist/landing/landing.d.ts +38 -0
- package/dist/landing/landing.js +299 -0
- package/dist/landing/landing.js.map +1 -0
- package/dist/public.d.ts +55 -0
- package/dist/public.js +51 -0
- package/dist/public.js.map +1 -0
- package/dist/schema/features.d.ts +28 -0
- package/dist/schema/features.js +9 -0
- package/dist/schema/features.js.map +1 -0
- package/dist/schema/v1.d.ts +317 -0
- package/dist/schema/v1.js +3 -0
- package/dist/schema/v1.js.map +1 -0
- package/dist/server/aggregation-cache.d.ts +105 -0
- package/dist/server/aggregation-cache.js +163 -0
- package/dist/server/aggregation-cache.js.map +1 -0
- package/dist/server/concurrent-execution-metrics.d.ts +103 -0
- package/dist/server/concurrent-execution-metrics.js +253 -0
- package/dist/server/concurrent-execution-metrics.js.map +1 -0
- package/dist/server/domain-gateway.d.ts +18 -0
- package/dist/server/domain-gateway.js +19 -0
- package/dist/server/domain-gateway.js.map +1 -0
- package/dist/server/incremental-task-usage.d.ts +92 -0
- package/dist/server/incremental-task-usage.js +251 -0
- package/dist/server/incremental-task-usage.js.map +1 -0
- package/dist/server/index.d.ts +39 -0
- package/dist/server/index.js +36 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/port.d.ts +96 -0
- package/dist/server/port.js +134 -0
- package/dist/server/port.js.map +1 -0
- package/dist/server/pr-markdown-refresh-diagnostics.d.ts +59 -0
- package/dist/server/pr-markdown-refresh-diagnostics.js +429 -0
- package/dist/server/pr-markdown-refresh-diagnostics.js.map +1 -0
- package/dist/server/prd-io.d.ts +40 -0
- package/dist/server/prd-io.js +66 -0
- package/dist/server/prd-io.js.map +1 -0
- package/dist/server/process-memory-tracker.d.ts +79 -0
- package/dist/server/process-memory-tracker.js +194 -0
- package/dist/server/process-memory-tracker.js.map +1 -0
- package/dist/server/register-scheduler.d.ts +53 -0
- package/dist/server/register-scheduler.js +36 -0
- package/dist/server/register-scheduler.js.map +1 -0
- package/dist/server/rex-gateway.d.ts +39 -0
- package/dist/server/rex-gateway.js +47 -0
- package/dist/server/rex-gateway.js.map +1 -0
- package/dist/server/routes-adaptive.d.ts +21 -0
- package/dist/server/routes-adaptive.js +659 -0
- package/dist/server/routes-adaptive.js.map +1 -0
- package/dist/server/routes-config.d.ts +47 -0
- package/dist/server/routes-config.js +222 -0
- package/dist/server/routes-config.js.map +1 -0
- package/dist/server/routes-data.d.ts +14 -0
- package/dist/server/routes-data.js +129 -0
- package/dist/server/routes-data.js.map +1 -0
- package/dist/server/routes-features.d.ts +14 -0
- package/dist/server/routes-features.js +245 -0
- package/dist/server/routes-features.js.map +1 -0
- package/dist/server/routes-hench.d.ts +116 -0
- package/dist/server/routes-hench.js +2016 -0
- package/dist/server/routes-hench.js.map +1 -0
- package/dist/server/routes-integrations.d.ts +23 -0
- package/dist/server/routes-integrations.js +277 -0
- package/dist/server/routes-integrations.js.map +1 -0
- package/dist/server/routes-mcp.d.ts +31 -0
- package/dist/server/routes-mcp.js +175 -0
- package/dist/server/routes-mcp.js.map +1 -0
- package/dist/server/routes-notion.d.ts +23 -0
- package/dist/server/routes-notion.js +723 -0
- package/dist/server/routes-notion.js.map +1 -0
- package/dist/server/routes-project.d.ts +47 -0
- package/dist/server/routes-project.js +128 -0
- package/dist/server/routes-project.js.map +1 -0
- package/dist/server/routes-rex/analysis.d.ts +11 -0
- package/dist/server/routes-rex/analysis.js +583 -0
- package/dist/server/routes-rex/analysis.js.map +1 -0
- package/dist/server/routes-rex/execution.d.ts +37 -0
- package/dist/server/routes-rex/execution.js +355 -0
- package/dist/server/routes-rex/execution.js.map +1 -0
- package/dist/server/routes-rex/health.d.ts +8 -0
- package/dist/server/routes-rex/health.js +136 -0
- package/dist/server/routes-rex/health.js.map +1 -0
- package/dist/server/routes-rex/index.d.ts +35 -0
- package/dist/server/routes-rex/index.js +72 -0
- package/dist/server/routes-rex/index.js.map +1 -0
- package/dist/server/routes-rex/items.d.ts +8 -0
- package/dist/server/routes-rex/items.js +359 -0
- package/dist/server/routes-rex/items.js.map +1 -0
- package/dist/server/routes-rex/prune.d.ts +11 -0
- package/dist/server/routes-rex/prune.js +379 -0
- package/dist/server/routes-rex/prune.js.map +1 -0
- package/dist/server/routes-rex/reads.d.ts +9 -0
- package/dist/server/routes-rex/reads.js +119 -0
- package/dist/server/routes-rex/reads.js.map +1 -0
- package/dist/server/routes-rex/requirements.d.ts +10 -0
- package/dist/server/routes-rex/requirements.js +408 -0
- package/dist/server/routes-rex/requirements.js.map +1 -0
- package/dist/server/routes-rex/shared.d.ts +37 -0
- package/dist/server/routes-rex/shared.js +73 -0
- package/dist/server/routes-rex/shared.js.map +1 -0
- package/dist/server/routes-search.d.ts +26 -0
- package/dist/server/routes-search.js +82 -0
- package/dist/server/routes-search.js.map +1 -0
- package/dist/server/routes-sourcevision.d.ts +18 -0
- package/dist/server/routes-sourcevision.js +444 -0
- package/dist/server/routes-sourcevision.js.map +1 -0
- package/dist/server/routes-static.d.ts +20 -0
- package/dist/server/routes-static.js +168 -0
- package/dist/server/routes-static.js.map +1 -0
- package/dist/server/routes-status.d.ts +58 -0
- package/dist/server/routes-status.js +191 -0
- package/dist/server/routes-status.js.map +1 -0
- package/dist/server/routes-token-usage.d.ts +63 -0
- package/dist/server/routes-token-usage.js +720 -0
- package/dist/server/routes-token-usage.js.map +1 -0
- package/dist/server/routes-validation.d.ts +10 -0
- package/dist/server/routes-validation.js +365 -0
- package/dist/server/routes-validation.js.map +1 -0
- package/dist/server/routes-workflow.d.ts +15 -0
- package/dist/server/routes-workflow.js +498 -0
- package/dist/server/routes-workflow.js.map +1 -0
- package/dist/server/search-index.d.ts +111 -0
- package/dist/server/search-index.js +348 -0
- package/dist/server/search-index.js.map +1 -0
- package/dist/server/shared-types.d.ts +65 -0
- package/dist/server/shared-types.js +31 -0
- package/dist/server/shared-types.js.map +1 -0
- package/dist/server/start.d.ts +68 -0
- package/dist/server/start.js +568 -0
- package/dist/server/start.js.map +1 -0
- package/dist/server/task-usage.d.ts +17 -0
- package/dist/server/task-usage.js +20 -0
- package/dist/server/task-usage.js.map +1 -0
- package/dist/server/types.d.ts +27 -0
- package/dist/server/types.js +26 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/usage-cleanup-scheduler.d.ts +107 -0
- package/dist/server/usage-cleanup-scheduler.js +232 -0
- package/dist/server/usage-cleanup-scheduler.js.map +1 -0
- package/dist/server/websocket.d.ts +131 -0
- package/dist/server/websocket.js +512 -0
- package/dist/server/websocket.js.map +1 -0
- package/dist/shared/data-files.d.ts +17 -0
- package/dist/shared/data-files.js +18 -0
- package/dist/shared/data-files.js.map +1 -0
- package/dist/shared/index.d.ts +9 -0
- package/dist/shared/index.js +9 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/view-id.d.ts +8 -0
- package/dist/shared/view-id.js +9 -0
- package/dist/shared/view-id.js.map +1 -0
- package/dist/viewer/Hench-F.png +0 -0
- package/dist/viewer/Rex-F.png +0 -0
- package/dist/viewer/SourceVision-F.png +0 -0
- package/dist/viewer/SourceVision.png +0 -0
- package/dist/viewer/api.d.ts +19 -0
- package/dist/viewer/api.js +17 -0
- package/dist/viewer/api.js.map +1 -0
- package/dist/viewer/bootstrap.d.ts +13 -0
- package/dist/viewer/bootstrap.js +40 -0
- package/dist/viewer/bootstrap.js.map +1 -0
- package/dist/viewer/components/active-tasks-panel.d.ts +36 -0
- package/dist/viewer/components/active-tasks-panel.js +183 -0
- package/dist/viewer/components/active-tasks-panel.js.map +1 -0
- package/dist/viewer/components/breadcrumb.d.ts +21 -0
- package/dist/viewer/components/breadcrumb.js +117 -0
- package/dist/viewer/components/breadcrumb.js.map +1 -0
- package/dist/viewer/components/concurrency-panel.d.ts +18 -0
- package/dist/viewer/components/concurrency-panel.js +175 -0
- package/dist/viewer/components/concurrency-panel.js.map +1 -0
- package/dist/viewer/components/config-footer.d.ts +11 -0
- package/dist/viewer/components/config-footer.js +132 -0
- package/dist/viewer/components/config-footer.js.map +1 -0
- package/dist/viewer/components/constants.d.ts +10 -0
- package/dist/viewer/components/constants.js +11 -0
- package/dist/viewer/components/constants.js.map +1 -0
- package/dist/viewer/components/copy-link-button.d.ts +30 -0
- package/dist/viewer/components/copy-link-button.js +74 -0
- package/dist/viewer/components/copy-link-button.js.map +1 -0
- package/dist/viewer/components/crash-recovery-banner.d.ts +28 -0
- package/dist/viewer/components/crash-recovery-banner.js +47 -0
- package/dist/viewer/components/crash-recovery-banner.js.map +1 -0
- package/dist/viewer/components/data-display/collapsible-section.d.ts +14 -0
- package/dist/viewer/components/data-display/collapsible-section.js +66 -0
- package/dist/viewer/components/data-display/collapsible-section.js.map +1 -0
- package/dist/viewer/components/data-display/findings-list.d.ts +13 -0
- package/dist/viewer/components/data-display/findings-list.js +136 -0
- package/dist/viewer/components/data-display/findings-list.js.map +1 -0
- package/dist/viewer/components/data-display/health-gauge.d.ts +37 -0
- package/dist/viewer/components/data-display/health-gauge.js +62 -0
- package/dist/viewer/components/data-display/health-gauge.js.map +1 -0
- package/dist/viewer/components/data-display/mini-charts.d.ts +36 -0
- package/dist/viewer/components/data-display/mini-charts.js +124 -0
- package/dist/viewer/components/data-display/mini-charts.js.map +1 -0
- package/dist/viewer/components/data-display/tree-view.d.ts +18 -0
- package/dist/viewer/components/data-display/tree-view.js +107 -0
- package/dist/viewer/components/data-display/tree-view.js.map +1 -0
- package/dist/viewer/components/data-display/zone-map.d.ts +28 -0
- package/dist/viewer/components/data-display/zone-map.js +197 -0
- package/dist/viewer/components/data-display/zone-map.js.map +1 -0
- package/dist/viewer/components/degradation-banner.d.ts +28 -0
- package/dist/viewer/components/degradation-banner.js +47 -0
- package/dist/viewer/components/degradation-banner.js.map +1 -0
- package/dist/viewer/components/detail-panel.d.ts +12 -0
- package/dist/viewer/components/detail-panel.js +166 -0
- package/dist/viewer/components/detail-panel.js.map +1 -0
- package/dist/viewer/components/elapsed-time.d.ts +38 -0
- package/dist/viewer/components/elapsed-time.js +35 -0
- package/dist/viewer/components/elapsed-time.js.map +1 -0
- package/dist/viewer/components/faq.d.ts +12 -0
- package/dist/viewer/components/faq.js +237 -0
- package/dist/viewer/components/faq.js.map +1 -0
- package/dist/viewer/components/favicon.d.ts +30 -0
- package/dist/viewer/components/favicon.js +90 -0
- package/dist/viewer/components/favicon.js.map +1 -0
- package/dist/viewer/components/guide.d.ts +5 -0
- package/dist/viewer/components/guide.js +116 -0
- package/dist/viewer/components/guide.js.map +1 -0
- package/dist/viewer/components/index.d.ts +37 -0
- package/dist/viewer/components/index.js +46 -0
- package/dist/viewer/components/index.js.map +1 -0
- package/dist/viewer/components/logos.d.ts +42 -0
- package/dist/viewer/components/logos.js +46 -0
- package/dist/viewer/components/logos.js.map +1 -0
- package/dist/viewer/components/memory-panel.d.ts +19 -0
- package/dist/viewer/components/memory-panel.js +181 -0
- package/dist/viewer/components/memory-panel.js.map +1 -0
- package/dist/viewer/components/memory-warning.d.ts +23 -0
- package/dist/viewer/components/memory-warning.js +42 -0
- package/dist/viewer/components/memory-warning.js.map +1 -0
- package/dist/viewer/components/notion-schema-wizard.d.ts +16 -0
- package/dist/viewer/components/notion-schema-wizard.js +263 -0
- package/dist/viewer/components/notion-schema-wizard.js.map +1 -0
- package/dist/viewer/components/polling-suspension-indicator.d.ts +24 -0
- package/dist/viewer/components/polling-suspension-indicator.js +31 -0
- package/dist/viewer/components/polling-suspension-indicator.js.map +1 -0
- package/dist/viewer/components/prd-tree/add-item-form.d.ts +31 -0
- package/dist/viewer/components/prd-tree/add-item-form.js +231 -0
- package/dist/viewer/components/prd-tree/add-item-form.js.map +1 -0
- package/dist/viewer/components/prd-tree/analyze-panel.d.ts +13 -0
- package/dist/viewer/components/prd-tree/analyze-panel.js +245 -0
- package/dist/viewer/components/prd-tree/analyze-panel.js.map +1 -0
- package/dist/viewer/components/prd-tree/batch-import-panel.d.ts +34 -0
- package/dist/viewer/components/prd-tree/batch-import-panel.js +415 -0
- package/dist/viewer/components/prd-tree/batch-import-panel.js.map +1 -0
- package/dist/viewer/components/prd-tree/bulk-actions.d.ts +19 -0
- package/dist/viewer/components/prd-tree/bulk-actions.js +90 -0
- package/dist/viewer/components/prd-tree/bulk-actions.js.map +1 -0
- package/dist/viewer/components/prd-tree/compute.d.ts +32 -0
- package/dist/viewer/components/prd-tree/compute.js +129 -0
- package/dist/viewer/components/prd-tree/compute.js.map +1 -0
- package/dist/viewer/components/prd-tree/delete-confirmation.d.ts +23 -0
- package/dist/viewer/components/prd-tree/delete-confirmation.js +83 -0
- package/dist/viewer/components/prd-tree/delete-confirmation.js.map +1 -0
- package/dist/viewer/components/prd-tree/execution-panel.d.ts +14 -0
- package/dist/viewer/components/prd-tree/execution-panel.js +291 -0
- package/dist/viewer/components/prd-tree/execution-panel.js.map +1 -0
- package/dist/viewer/components/prd-tree/facet-filter.d.ts +40 -0
- package/dist/viewer/components/prd-tree/facet-filter.js +188 -0
- package/dist/viewer/components/prd-tree/facet-filter.js.map +1 -0
- package/dist/viewer/components/prd-tree/index.d.ts +24 -0
- package/dist/viewer/components/prd-tree/index.js +16 -0
- package/dist/viewer/components/prd-tree/index.js.map +1 -0
- package/dist/viewer/components/prd-tree/inline-add-form.d.ts +32 -0
- package/dist/viewer/components/prd-tree/inline-add-form.js +175 -0
- package/dist/viewer/components/prd-tree/inline-add-form.js.map +1 -0
- package/dist/viewer/components/prd-tree/inline-status-picker.d.ts +31 -0
- package/dist/viewer/components/prd-tree/inline-status-picker.js +110 -0
- package/dist/viewer/components/prd-tree/inline-status-picker.js.map +1 -0
- package/dist/viewer/components/prd-tree/lazy-children.d.ts +40 -0
- package/dist/viewer/components/prd-tree/lazy-children.js +73 -0
- package/dist/viewer/components/prd-tree/lazy-children.js.map +1 -0
- package/dist/viewer/components/prd-tree/levels.d.ts +31 -0
- package/dist/viewer/components/prd-tree/levels.js +72 -0
- package/dist/viewer/components/prd-tree/levels.js.map +1 -0
- package/dist/viewer/components/prd-tree/listener-lifecycle.d.ts +105 -0
- package/dist/viewer/components/prd-tree/listener-lifecycle.js +173 -0
- package/dist/viewer/components/prd-tree/listener-lifecycle.js.map +1 -0
- package/dist/viewer/components/prd-tree/merge-preview.d.ts +25 -0
- package/dist/viewer/components/prd-tree/merge-preview.js +181 -0
- package/dist/viewer/components/prd-tree/merge-preview.js.map +1 -0
- package/dist/viewer/components/prd-tree/prd-tree.d.ts +91 -0
- package/dist/viewer/components/prd-tree/prd-tree.js +565 -0
- package/dist/viewer/components/prd-tree/prd-tree.js.map +1 -0
- package/dist/viewer/components/prd-tree/proposal-editor.d.ts +44 -0
- package/dist/viewer/components/prd-tree/proposal-editor.js +438 -0
- package/dist/viewer/components/prd-tree/proposal-editor.js.map +1 -0
- package/dist/viewer/components/prd-tree/prune-confirmation.d.ts +25 -0
- package/dist/viewer/components/prd-tree/prune-confirmation.js +336 -0
- package/dist/viewer/components/prd-tree/prune-confirmation.js.map +1 -0
- package/dist/viewer/components/prd-tree/prune-diff-tree.d.ts +39 -0
- package/dist/viewer/components/prd-tree/prune-diff-tree.js +319 -0
- package/dist/viewer/components/prd-tree/prune-diff-tree.js.map +1 -0
- package/dist/viewer/components/prd-tree/reorganize-panel.d.ts +16 -0
- package/dist/viewer/components/prd-tree/reorganize-panel.js +213 -0
- package/dist/viewer/components/prd-tree/reorganize-panel.js.map +1 -0
- package/dist/viewer/components/prd-tree/smart-add-input.d.ts +19 -0
- package/dist/viewer/components/prd-tree/smart-add-input.js +383 -0
- package/dist/viewer/components/prd-tree/smart-add-input.js.map +1 -0
- package/dist/viewer/components/prd-tree/status-filter.d.ts +40 -0
- package/dist/viewer/components/prd-tree/status-filter.js +131 -0
- package/dist/viewer/components/prd-tree/status-filter.js.map +1 -0
- package/dist/viewer/components/prd-tree/task-detail.d.ts +41 -0
- package/dist/viewer/components/prd-tree/task-detail.js +1205 -0
- package/dist/viewer/components/prd-tree/task-detail.js.map +1 -0
- package/dist/viewer/components/prd-tree/task-utilization.d.ts +7 -0
- package/dist/viewer/components/prd-tree/task-utilization.js +25 -0
- package/dist/viewer/components/prd-tree/task-utilization.js.map +1 -0
- package/dist/viewer/components/prd-tree/tree-differ.d.ts +59 -0
- package/dist/viewer/components/prd-tree/tree-differ.js +200 -0
- package/dist/viewer/components/prd-tree/tree-differ.js.map +1 -0
- package/dist/viewer/components/prd-tree/tree-event-delegate.d.ts +70 -0
- package/dist/viewer/components/prd-tree/tree-event-delegate.js +176 -0
- package/dist/viewer/components/prd-tree/tree-event-delegate.js.map +1 -0
- package/dist/viewer/components/prd-tree/tree-search.d.ts +65 -0
- package/dist/viewer/components/prd-tree/tree-search.js +178 -0
- package/dist/viewer/components/prd-tree/tree-search.js.map +1 -0
- package/dist/viewer/components/prd-tree/tree-utils.d.ts +38 -0
- package/dist/viewer/components/prd-tree/tree-utils.js +107 -0
- package/dist/viewer/components/prd-tree/tree-utils.js.map +1 -0
- package/dist/viewer/components/prd-tree/types.d.ts +93 -0
- package/dist/viewer/components/prd-tree/types.js +16 -0
- package/dist/viewer/components/prd-tree/types.js.map +1 -0
- package/dist/viewer/components/prd-tree/virtual-scroll.d.ts +119 -0
- package/dist/viewer/components/prd-tree/virtual-scroll.js +169 -0
- package/dist/viewer/components/prd-tree/virtual-scroll.js.map +1 -0
- package/dist/viewer/components/progressive-loader.d.ts +114 -0
- package/dist/viewer/components/progressive-loader.js +225 -0
- package/dist/viewer/components/progressive-loader.js.map +1 -0
- package/dist/viewer/components/refresh-queue-status.d.ts +20 -0
- package/dist/viewer/components/refresh-queue-status.js +65 -0
- package/dist/viewer/components/refresh-queue-status.js.map +1 -0
- package/dist/viewer/components/rex-task-link.d.ts +50 -0
- package/dist/viewer/components/rex-task-link.js +218 -0
- package/dist/viewer/components/rex-task-link.js.map +1 -0
- package/dist/viewer/components/search-filter.d.ts +20 -0
- package/dist/viewer/components/search-filter.js +28 -0
- package/dist/viewer/components/search-filter.js.map +1 -0
- package/dist/viewer/components/search-overlay.d.ts +31 -0
- package/dist/viewer/components/search-overlay.js +472 -0
- package/dist/viewer/components/search-overlay.js.map +1 -0
- package/dist/viewer/components/sidebar.d.ts +18 -0
- package/dist/viewer/components/sidebar.js +357 -0
- package/dist/viewer/components/sidebar.js.map +1 -0
- package/dist/viewer/components/status-indicators.d.ts +63 -0
- package/dist/viewer/components/status-indicators.js +136 -0
- package/dist/viewer/components/status-indicators.js.map +1 -0
- package/dist/viewer/components/theme-toggle.d.ts +8 -0
- package/dist/viewer/components/theme-toggle.js +28 -0
- package/dist/viewer/components/theme-toggle.js.map +1 -0
- package/dist/viewer/components/throttle-controls.d.ts +18 -0
- package/dist/viewer/components/throttle-controls.js +304 -0
- package/dist/viewer/components/throttle-controls.js.map +1 -0
- package/dist/viewer/components/ws-health-panel.d.ts +18 -0
- package/dist/viewer/components/ws-health-panel.js +250 -0
- package/dist/viewer/components/ws-health-panel.js.map +1 -0
- package/dist/viewer/components/zone-slideout.d.ts +17 -0
- package/dist/viewer/components/zone-slideout.js +162 -0
- package/dist/viewer/components/zone-slideout.js.map +1 -0
- package/dist/viewer/crash/crash-detector.d.ts +69 -0
- package/dist/viewer/crash/crash-detector.js +239 -0
- package/dist/viewer/crash/crash-detector.js.map +1 -0
- package/dist/viewer/crash/index.d.ts +7 -0
- package/dist/viewer/crash/index.js +8 -0
- package/dist/viewer/crash/index.js.map +1 -0
- package/dist/viewer/deployed-mode.d.ts +37 -0
- package/dist/viewer/deployed-mode.js +94 -0
- package/dist/viewer/deployed-mode.js.map +1 -0
- package/dist/viewer/external.d.ts +17 -0
- package/dist/viewer/external.js +17 -0
- package/dist/viewer/external.js.map +1 -0
- package/dist/viewer/graph/index.d.ts +9 -0
- package/dist/viewer/graph/index.js +12 -0
- package/dist/viewer/graph/index.js.map +1 -0
- package/dist/viewer/graph/physics.d.ts +96 -0
- package/dist/viewer/graph/physics.js +366 -0
- package/dist/viewer/graph/physics.js.map +1 -0
- package/dist/viewer/graph/renderer.d.ts +184 -0
- package/dist/viewer/graph/renderer.js +1438 -0
- package/dist/viewer/graph/renderer.js.map +1 -0
- package/dist/viewer/hooks/index.d.ts +27 -0
- package/dist/viewer/hooks/index.js +28 -0
- package/dist/viewer/hooks/index.js.map +1 -0
- package/dist/viewer/hooks/use-app-data.d.ts +31 -0
- package/dist/viewer/hooks/use-app-data.js +152 -0
- package/dist/viewer/hooks/use-app-data.js.map +1 -0
- package/dist/viewer/hooks/use-crash-recovery.d.ts +50 -0
- package/dist/viewer/hooks/use-crash-recovery.js +76 -0
- package/dist/viewer/hooks/use-crash-recovery.js.map +1 -0
- package/dist/viewer/hooks/use-delete-actions.d.ts +48 -0
- package/dist/viewer/hooks/use-delete-actions.js +103 -0
- package/dist/viewer/hooks/use-delete-actions.js.map +1 -0
- package/dist/viewer/hooks/use-dom-performance-monitor.d.ts +68 -0
- package/dist/viewer/hooks/use-dom-performance-monitor.js +71 -0
- package/dist/viewer/hooks/use-dom-performance-monitor.js.map +1 -0
- package/dist/viewer/hooks/use-facet-state.d.ts +32 -0
- package/dist/viewer/hooks/use-facet-state.js +119 -0
- package/dist/viewer/hooks/use-facet-state.js.map +1 -0
- package/dist/viewer/hooks/use-feature-toggle.d.ts +18 -0
- package/dist/viewer/hooks/use-feature-toggle.js +57 -0
- package/dist/viewer/hooks/use-feature-toggle.js.map +1 -0
- package/dist/viewer/hooks/use-file-edges.d.ts +23 -0
- package/dist/viewer/hooks/use-file-edges.js +221 -0
- package/dist/viewer/hooks/use-file-edges.js.map +1 -0
- package/dist/viewer/hooks/use-graceful-degradation.d.ts +30 -0
- package/dist/viewer/hooks/use-graceful-degradation.js +45 -0
- package/dist/viewer/hooks/use-graceful-degradation.js.map +1 -0
- package/dist/viewer/hooks/use-item-selection.d.ts +49 -0
- package/dist/viewer/hooks/use-item-selection.js +117 -0
- package/dist/viewer/hooks/use-item-selection.js.map +1 -0
- package/dist/viewer/hooks/use-memory-monitor.d.ts +39 -0
- package/dist/viewer/hooks/use-memory-monitor.js +73 -0
- package/dist/viewer/hooks/use-memory-monitor.js.map +1 -0
- package/dist/viewer/hooks/use-pan-zoom.d.ts +33 -0
- package/dist/viewer/hooks/use-pan-zoom.js +110 -0
- package/dist/viewer/hooks/use-pan-zoom.js.map +1 -0
- package/dist/viewer/hooks/use-persistent-filter.d.ts +24 -0
- package/dist/viewer/hooks/use-persistent-filter.js +37 -0
- package/dist/viewer/hooks/use-persistent-filter.js.map +1 -0
- package/dist/viewer/hooks/use-polling-suspension.d.ts +32 -0
- package/dist/viewer/hooks/use-polling-suspension.js +41 -0
- package/dist/viewer/hooks/use-polling-suspension.js.map +1 -0
- package/dist/viewer/hooks/use-polling.d.ts +39 -0
- package/dist/viewer/hooks/use-polling.js +55 -0
- package/dist/viewer/hooks/use-polling.js.map +1 -0
- package/dist/viewer/hooks/use-prd-actions.d.ts +126 -0
- package/dist/viewer/hooks/use-prd-actions.js +250 -0
- package/dist/viewer/hooks/use-prd-actions.js.map +1 -0
- package/dist/viewer/hooks/use-prd-data.d.ts +45 -0
- package/dist/viewer/hooks/use-prd-data.js +159 -0
- package/dist/viewer/hooks/use-prd-data.js.map +1 -0
- package/dist/viewer/hooks/use-prd-deep-link.d.ts +45 -0
- package/dist/viewer/hooks/use-prd-deep-link.js +60 -0
- package/dist/viewer/hooks/use-prd-deep-link.js.map +1 -0
- package/dist/viewer/hooks/use-prd-websocket.d.ts +47 -0
- package/dist/viewer/hooks/use-prd-websocket.js +139 -0
- package/dist/viewer/hooks/use-prd-websocket.js.map +1 -0
- package/dist/viewer/hooks/use-project-metadata.d.ts +25 -0
- package/dist/viewer/hooks/use-project-metadata.js +55 -0
- package/dist/viewer/hooks/use-project-metadata.js.map +1 -0
- package/dist/viewer/hooks/use-project-status.d.ts +60 -0
- package/dist/viewer/hooks/use-project-status.js +133 -0
- package/dist/viewer/hooks/use-project-status.js.map +1 -0
- package/dist/viewer/hooks/use-refresh-throttle.d.ts +45 -0
- package/dist/viewer/hooks/use-refresh-throttle.js +52 -0
- package/dist/viewer/hooks/use-refresh-throttle.js.map +1 -0
- package/dist/viewer/hooks/use-route-state.d.ts +18 -0
- package/dist/viewer/hooks/use-route-state.js +115 -0
- package/dist/viewer/hooks/use-route-state.js.map +1 -0
- package/dist/viewer/hooks/use-subzone-edges.d.ts +21 -0
- package/dist/viewer/hooks/use-subzone-edges.js +147 -0
- package/dist/viewer/hooks/use-subzone-edges.js.map +1 -0
- package/dist/viewer/hooks/use-tab-visibility.d.ts +31 -0
- package/dist/viewer/hooks/use-tab-visibility.js +43 -0
- package/dist/viewer/hooks/use-tab-visibility.js.map +1 -0
- package/dist/viewer/hooks/use-tick.d.ts +43 -0
- package/dist/viewer/hooks/use-tick.js +76 -0
- package/dist/viewer/hooks/use-tick.js.map +1 -0
- package/dist/viewer/hooks/use-toast.d.ts +24 -0
- package/dist/viewer/hooks/use-toast.js +26 -0
- package/dist/viewer/hooks/use-toast.js.map +1 -0
- package/dist/viewer/hooks/use-zone-drag.d.ts +30 -0
- package/dist/viewer/hooks/use-zone-drag.js +60 -0
- package/dist/viewer/hooks/use-zone-drag.js.map +1 -0
- package/dist/viewer/index.html +36 -0
- package/dist/viewer/loader.d.ts +33 -0
- package/dist/viewer/loader.js +195 -0
- package/dist/viewer/loader.js.map +1 -0
- package/dist/viewer/main.d.ts +1 -0
- package/dist/viewer/main.js +121 -0
- package/dist/viewer/main.js.map +1 -0
- package/dist/viewer/messaging/call-rate-limiter.d.ts +50 -0
- package/dist/viewer/messaging/call-rate-limiter.js +103 -0
- package/dist/viewer/messaging/call-rate-limiter.js.map +1 -0
- package/dist/viewer/messaging/fetch-pipeline.d.ts +58 -0
- package/dist/viewer/messaging/fetch-pipeline.js +58 -0
- package/dist/viewer/messaging/fetch-pipeline.js.map +1 -0
- package/dist/viewer/messaging/index.d.ts +43 -0
- package/dist/viewer/messaging/index.js +46 -0
- package/dist/viewer/messaging/index.js.map +1 -0
- package/dist/viewer/messaging/message-coalescer.d.ts +96 -0
- package/dist/viewer/messaging/message-coalescer.js +121 -0
- package/dist/viewer/messaging/message-coalescer.js.map +1 -0
- package/dist/viewer/messaging/message-throttle.d.ts +95 -0
- package/dist/viewer/messaging/message-throttle.js +147 -0
- package/dist/viewer/messaging/message-throttle.js.map +1 -0
- package/dist/viewer/messaging/request-dedup.d.ts +43 -0
- package/dist/viewer/messaging/request-dedup.js +55 -0
- package/dist/viewer/messaging/request-dedup.js.map +1 -0
- package/dist/viewer/messaging/ws-pipeline.d.ts +85 -0
- package/dist/viewer/messaging/ws-pipeline.js +68 -0
- package/dist/viewer/messaging/ws-pipeline.js.map +1 -0
- package/dist/viewer/n-dx.png +0 -0
- package/dist/viewer/performance/dom-performance-monitor.d.ts +157 -0
- package/dist/viewer/performance/dom-performance-monitor.js +341 -0
- package/dist/viewer/performance/dom-performance-monitor.js.map +1 -0
- package/dist/viewer/performance/dom-update-gate.d.ts +122 -0
- package/dist/viewer/performance/dom-update-gate.js +229 -0
- package/dist/viewer/performance/dom-update-gate.js.map +1 -0
- package/dist/viewer/performance/graceful-degradation.d.ts +73 -0
- package/dist/viewer/performance/graceful-degradation.js +152 -0
- package/dist/viewer/performance/graceful-degradation.js.map +1 -0
- package/dist/viewer/performance/index.d.ts +14 -0
- package/dist/viewer/performance/index.js +20 -0
- package/dist/viewer/performance/index.js.map +1 -0
- package/dist/viewer/performance/memory-monitor.d.ts +78 -0
- package/dist/viewer/performance/memory-monitor.js +218 -0
- package/dist/viewer/performance/memory-monitor.js.map +1 -0
- package/dist/viewer/performance/refresh-throttle.d.ts +90 -0
- package/dist/viewer/performance/refresh-throttle.js +266 -0
- package/dist/viewer/performance/refresh-throttle.js.map +1 -0
- package/dist/viewer/performance/response-buffer-gate.d.ts +108 -0
- package/dist/viewer/performance/response-buffer-gate.js +170 -0
- package/dist/viewer/performance/response-buffer-gate.js.map +1 -0
- package/dist/viewer/performance/update-batcher.d.ts +79 -0
- package/dist/viewer/performance/update-batcher.js +119 -0
- package/dist/viewer/performance/update-batcher.js.map +1 -0
- package/dist/viewer/polling/batched-tick-dispatcher.d.ts +83 -0
- package/dist/viewer/polling/batched-tick-dispatcher.js +183 -0
- package/dist/viewer/polling/batched-tick-dispatcher.js.map +1 -0
- package/dist/viewer/polling/index.d.ts +13 -0
- package/dist/viewer/polling/index.js +16 -0
- package/dist/viewer/polling/index.js.map +1 -0
- package/dist/viewer/polling/polling-manager.d.ts +82 -0
- package/dist/viewer/polling/polling-manager.js +254 -0
- package/dist/viewer/polling/polling-manager.js.map +1 -0
- package/dist/viewer/polling/polling-restart.d.ts +45 -0
- package/dist/viewer/polling/polling-restart.js +98 -0
- package/dist/viewer/polling/polling-restart.js.map +1 -0
- package/dist/viewer/polling/polling-state.d.ts +182 -0
- package/dist/viewer/polling/polling-state.js +306 -0
- package/dist/viewer/polling/polling-state.js.map +1 -0
- package/dist/viewer/polling/tab-visibility.d.ts +112 -0
- package/dist/viewer/polling/tab-visibility.js +276 -0
- package/dist/viewer/polling/tab-visibility.js.map +1 -0
- package/dist/viewer/polling/tick-timer.d.ts +70 -0
- package/dist/viewer/polling/tick-timer.js +168 -0
- package/dist/viewer/polling/tick-timer.js.map +1 -0
- package/dist/viewer/polling/tick-visibility-gate.d.ts +92 -0
- package/dist/viewer/polling/tick-visibility-gate.js +146 -0
- package/dist/viewer/polling/tick-visibility-gate.js.map +1 -0
- package/dist/viewer/route-state.d.ts +8 -0
- package/dist/viewer/route-state.js +77 -0
- package/dist/viewer/route-state.js.map +1 -0
- package/dist/viewer/schema-compat.d.ts +17 -0
- package/dist/viewer/schema-compat.js +49 -0
- package/dist/viewer/schema-compat.js.map +1 -0
- package/dist/viewer/types.d.ts +66 -0
- package/dist/viewer/types.js +2 -0
- package/dist/viewer/types.js.map +1 -0
- package/dist/viewer/usage/constants.d.ts +14 -0
- package/dist/viewer/usage/constants.js +14 -0
- package/dist/viewer/usage/constants.js.map +1 -0
- package/dist/viewer/usage/index.d.ts +7 -0
- package/dist/viewer/usage/index.js +8 -0
- package/dist/viewer/usage/index.js.map +1 -0
- package/dist/viewer/utils.d.ts +25 -0
- package/dist/viewer/utils.js +48 -0
- package/dist/viewer/utils.js.map +1 -0
- package/dist/viewer/validate.d.ts +23 -0
- package/dist/viewer/validate.js +275 -0
- package/dist/viewer/validate.js.map +1 -0
- package/dist/viewer/views/analysis.d.ts +10 -0
- package/dist/viewer/views/analysis.js +109 -0
- package/dist/viewer/views/analysis.js.map +1 -0
- package/dist/viewer/views/architecture.d.ts +10 -0
- package/dist/viewer/views/architecture.js +44 -0
- package/dist/viewer/views/architecture.js.map +1 -0
- package/dist/viewer/views/domain-hench.d.ts +13 -0
- package/dist/viewer/views/domain-hench.js +14 -0
- package/dist/viewer/views/domain-hench.js.map +1 -0
- package/dist/viewer/views/domain-rex.d.ts +17 -0
- package/dist/viewer/views/domain-rex.js +18 -0
- package/dist/viewer/views/domain-rex.js.map +1 -0
- package/dist/viewer/views/domain-settings.d.ts +12 -0
- package/dist/viewer/views/domain-settings.js +13 -0
- package/dist/viewer/views/domain-settings.js.map +1 -0
- package/dist/viewer/views/domain-sourcevision.d.ts +20 -0
- package/dist/viewer/views/domain-sourcevision.js +21 -0
- package/dist/viewer/views/domain-sourcevision.js.map +1 -0
- package/dist/viewer/views/enrichment-thresholds.d.ts +11 -0
- package/dist/viewer/views/enrichment-thresholds.js +12 -0
- package/dist/viewer/views/enrichment-thresholds.js.map +1 -0
- package/dist/viewer/views/feature-toggles.d.ts +13 -0
- package/dist/viewer/views/feature-toggles.js +185 -0
- package/dist/viewer/views/feature-toggles.js.map +1 -0
- package/dist/viewer/views/files.d.ts +13 -0
- package/dist/viewer/views/files.js +174 -0
- package/dist/viewer/views/files.js.map +1 -0
- package/dist/viewer/views/graph.d.ts +12 -0
- package/dist/viewer/views/graph.js +316 -0
- package/dist/viewer/views/graph.js.map +1 -0
- package/dist/viewer/views/hench-config.d.ts +39 -0
- package/dist/viewer/views/hench-config.js +473 -0
- package/dist/viewer/views/hench-config.js.map +1 -0
- package/dist/viewer/views/hench-runs.d.ts +19 -0
- package/dist/viewer/views/hench-runs.js +460 -0
- package/dist/viewer/views/hench-runs.js.map +1 -0
- package/dist/viewer/views/hench-templates.d.ts +17 -0
- package/dist/viewer/views/hench-templates.js +262 -0
- package/dist/viewer/views/hench-templates.js.map +1 -0
- package/dist/viewer/views/integration-config.d.ts +73 -0
- package/dist/viewer/views/integration-config.js +524 -0
- package/dist/viewer/views/integration-config.js.map +1 -0
- package/dist/viewer/views/notion-config.d.ts +16 -0
- package/dist/viewer/views/notion-config.js +357 -0
- package/dist/viewer/views/notion-config.js.map +1 -0
- package/dist/viewer/views/overview.d.ts +10 -0
- package/dist/viewer/views/overview.js +187 -0
- package/dist/viewer/views/overview.js.map +1 -0
- package/dist/viewer/views/pr-markdown.d.ts +3 -0
- package/dist/viewer/views/pr-markdown.js +350 -0
- package/dist/viewer/views/pr-markdown.js.map +1 -0
- package/dist/viewer/views/prd.d.ts +34 -0
- package/dist/viewer/views/prd.js +257 -0
- package/dist/viewer/views/prd.js.map +1 -0
- package/dist/viewer/views/problems.d.ts +8 -0
- package/dist/viewer/views/problems.js +50 -0
- package/dist/viewer/views/problems.js.map +1 -0
- package/dist/viewer/views/rex-dashboard.d.ts +14 -0
- package/dist/viewer/views/rex-dashboard.js +334 -0
- package/dist/viewer/views/rex-dashboard.js.map +1 -0
- package/dist/viewer/views/routes.d.ts +8 -0
- package/dist/viewer/views/routes.js +216 -0
- package/dist/viewer/views/routes.js.map +1 -0
- package/dist/viewer/views/sourcevision-tabs.d.ts +18 -0
- package/dist/viewer/views/sourcevision-tabs.js +14 -0
- package/dist/viewer/views/sourcevision-tabs.js.map +1 -0
- package/dist/viewer/views/suggestions.d.ts +8 -0
- package/dist/viewer/views/suggestions.js +36 -0
- package/dist/viewer/views/suggestions.js.map +1 -0
- package/dist/viewer/views/task-audit.d.ts +18 -0
- package/dist/viewer/views/task-audit.js +413 -0
- package/dist/viewer/views/task-audit.js.map +1 -0
- package/dist/viewer/views/token-usage.d.ts +10 -0
- package/dist/viewer/views/token-usage.js +410 -0
- package/dist/viewer/views/token-usage.js.map +1 -0
- package/dist/viewer/views/validation.d.ts +11 -0
- package/dist/viewer/views/validation.js +475 -0
- package/dist/viewer/views/validation.js.map +1 -0
- package/dist/viewer/views/view-registry.d.ts +27 -0
- package/dist/viewer/views/view-registry.js +70 -0
- package/dist/viewer/views/view-registry.js.map +1 -0
- package/dist/viewer/views/workflow-optimization.d.ts +12 -0
- package/dist/viewer/views/workflow-optimization.js +311 -0
- package/dist/viewer/views/workflow-optimization.js.map +1 -0
- package/dist/viewer/views/zone-types.d.ts +69 -0
- package/dist/viewer/views/zone-types.js +5 -0
- package/dist/viewer/views/zone-types.js.map +1 -0
- package/dist/viewer/views/zones.d.ts +50 -0
- package/dist/viewer/views/zones.js +1438 -0
- package/dist/viewer/views/zones.js.map +1 -0
- package/dist/viewer/visualization/colors.d.ts +16 -0
- package/dist/viewer/visualization/colors.js +31 -0
- package/dist/viewer/visualization/colors.js.map +1 -0
- package/dist/viewer/visualization/flow.d.ts +54 -0
- package/dist/viewer/visualization/flow.js +123 -0
- package/dist/viewer/visualization/flow.js.map +1 -0
- package/dist/viewer/visualization/index.d.ts +34 -0
- package/dist/viewer/visualization/index.js +40 -0
- package/dist/viewer/visualization/index.js.map +1 -0
- package/dist/viewer/visualization/metrics.d.ts +8 -0
- package/dist/viewer/visualization/metrics.js +16 -0
- package/dist/viewer/visualization/metrics.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,1438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphRenderer — imperative SVG rendering for the force-directed import graph.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from viewer/views/graph.ts. Owns all DOM manipulation, event
|
|
5
|
+
* handlers, LOD, and physics integration. Uses AbortController for clean
|
|
6
|
+
* teardown of all event listeners.
|
|
7
|
+
*
|
|
8
|
+
* @remarks Class methods (highlightNode, centerOnNode, selectNode, etc.) are
|
|
9
|
+
* exported as part of the GraphRenderer class. Static analysis may flag these
|
|
10
|
+
* as unused because they are called via class instances, not via static imports.
|
|
11
|
+
*/
|
|
12
|
+
import { initZoneClusteredPositions, tick, } from "./physics.js";
|
|
13
|
+
import { basename, truncateFilename } from "../utils.js";
|
|
14
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
15
|
+
// ── GraphRenderer class ──────────────────────────────────────────────────────
|
|
16
|
+
export class GraphRenderer {
|
|
17
|
+
nodes;
|
|
18
|
+
nodeGroups;
|
|
19
|
+
svg;
|
|
20
|
+
g;
|
|
21
|
+
linkElements;
|
|
22
|
+
nodeRadii;
|
|
23
|
+
resolvedLinks;
|
|
24
|
+
nodeEdgeMap;
|
|
25
|
+
ac;
|
|
26
|
+
sim;
|
|
27
|
+
width;
|
|
28
|
+
height;
|
|
29
|
+
// ViewBox state for zoom/pan
|
|
30
|
+
viewX = 0;
|
|
31
|
+
viewY = 0;
|
|
32
|
+
viewW;
|
|
33
|
+
viewH;
|
|
34
|
+
scale = 1;
|
|
35
|
+
// Lifecycle flag — prevents scheduling new animation frames after destroy
|
|
36
|
+
destroyed = false;
|
|
37
|
+
// Selection state — persists until explicitly cleared
|
|
38
|
+
selectedNodeId = null;
|
|
39
|
+
// Pan gesture flag — set true when a pan actually moved the view;
|
|
40
|
+
// zone hull click handlers check this to suppress clicks after panning.
|
|
41
|
+
wasPanning = false;
|
|
42
|
+
// Label management state
|
|
43
|
+
labelsHidden = false; // user toggle
|
|
44
|
+
labelRects; // reused per frame to avoid GC
|
|
45
|
+
tooltip; // shared tooltip element
|
|
46
|
+
// Zone grouping state
|
|
47
|
+
zoneInfos;
|
|
48
|
+
zoneHullGroup; // container for zone hulls
|
|
49
|
+
zoneHullElements = new Map();
|
|
50
|
+
zoneLabelElements = new Map(); // zone label groups (bg + text)
|
|
51
|
+
collapsedZones = new Set();
|
|
52
|
+
zoneNodeIndices = new Map();
|
|
53
|
+
zonesVisible = true;
|
|
54
|
+
onZoneSelect;
|
|
55
|
+
zoneLabelLayer; // separate layer above nodes for zone labels
|
|
56
|
+
constructor(opts) {
|
|
57
|
+
const { svg, nodes, links, width, height, onNodeSelect, onNodeDblClick, onZoneSelect, zoneInfos } = opts;
|
|
58
|
+
this.svg = svg;
|
|
59
|
+
this.nodes = nodes;
|
|
60
|
+
this.width = width;
|
|
61
|
+
this.height = height;
|
|
62
|
+
this.viewW = width;
|
|
63
|
+
this.viewH = height;
|
|
64
|
+
this.nodeGroups = [];
|
|
65
|
+
this.linkElements = [];
|
|
66
|
+
this.nodeRadii = [];
|
|
67
|
+
this.ac = new AbortController();
|
|
68
|
+
this.zoneInfos = zoneInfos;
|
|
69
|
+
this.onZoneSelect = onZoneSelect;
|
|
70
|
+
// Clear existing SVG content and set up root structure
|
|
71
|
+
svg.innerHTML = "";
|
|
72
|
+
this.updateViewBox();
|
|
73
|
+
this.createSvgDefs(svg);
|
|
74
|
+
this.g = document.createElementNS(SVG_NS, "g");
|
|
75
|
+
svg.appendChild(this.g);
|
|
76
|
+
// Build zone → node index map and zone hull/label layers
|
|
77
|
+
this.buildZoneNodeMap();
|
|
78
|
+
this.zoneHullGroup = this.createSvgLayer("zone-hulls");
|
|
79
|
+
this.zoneLabelLayer = document.createElementNS(SVG_NS, "g");
|
|
80
|
+
this.zoneLabelLayer.setAttribute("class", "zone-label-layer");
|
|
81
|
+
this.createZoneHulls();
|
|
82
|
+
// Resolve link references and build adjacency map
|
|
83
|
+
const nodeMap = new Map();
|
|
84
|
+
for (const n of nodes)
|
|
85
|
+
nodeMap.set(n.id, n);
|
|
86
|
+
initZoneClusteredPositions(nodes, width, height);
|
|
87
|
+
this.resolvedLinks = this.resolveGraphLinks(links, nodeMap);
|
|
88
|
+
this.nodeEdgeMap = this.buildAdjacencyMap();
|
|
89
|
+
// Create SVG elements for links and nodes
|
|
90
|
+
this.createLinkElements();
|
|
91
|
+
const nodeLayer = this.createSvgLayer("graph-node-layer");
|
|
92
|
+
this.createNodeElements(nodeLayer);
|
|
93
|
+
this.labelRects = this.allocateLabelRects();
|
|
94
|
+
// Tooltip and top layers
|
|
95
|
+
this.tooltip = this.createTooltipElement();
|
|
96
|
+
this.g.appendChild(this.zoneLabelLayer);
|
|
97
|
+
this.g.appendChild(this.tooltip);
|
|
98
|
+
// Initialize physics simulation
|
|
99
|
+
this.sim = {
|
|
100
|
+
nodes,
|
|
101
|
+
resolvedLinks: this.resolvedLinks,
|
|
102
|
+
width,
|
|
103
|
+
height,
|
|
104
|
+
alpha: { value: 1 },
|
|
105
|
+
frameCount: 0,
|
|
106
|
+
hasFitted: false,
|
|
107
|
+
scale: this.scale,
|
|
108
|
+
nodeRadii: this.nodeRadii,
|
|
109
|
+
};
|
|
110
|
+
// Set up event listeners and start simulation
|
|
111
|
+
this.setupZoom();
|
|
112
|
+
this.setupPanAndDrag(onNodeSelect, onNodeDblClick);
|
|
113
|
+
this.setupHoverHighlighting();
|
|
114
|
+
this.setupLabelTooltips();
|
|
115
|
+
this.setupTouchInteraction(onNodeSelect, onNodeDblClick);
|
|
116
|
+
this.startSimulation();
|
|
117
|
+
}
|
|
118
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
119
|
+
highlightNode(id) {
|
|
120
|
+
// Clear previous highlights
|
|
121
|
+
const existing = this.g.querySelectorAll(".graph-search-ring");
|
|
122
|
+
existing.forEach((el) => el.remove());
|
|
123
|
+
if (!id)
|
|
124
|
+
return;
|
|
125
|
+
const idx = this.nodes.findIndex((n) => n.id === id);
|
|
126
|
+
if (idx < 0)
|
|
127
|
+
return;
|
|
128
|
+
const ns = SVG_NS;
|
|
129
|
+
const ring = document.createElementNS(ns, "circle");
|
|
130
|
+
ring.setAttribute("class", "graph-search-ring");
|
|
131
|
+
ring.setAttribute("r", String(this.nodeRadii[idx] + 4));
|
|
132
|
+
ring.setAttribute("fill", "none");
|
|
133
|
+
ring.setAttribute("stroke", "var(--accent)");
|
|
134
|
+
ring.setAttribute("stroke-width", "2");
|
|
135
|
+
this.nodeGroups[idx].appendChild(ring);
|
|
136
|
+
}
|
|
137
|
+
centerOnNode(id) {
|
|
138
|
+
const idx = this.nodes.findIndex((n) => n.id === id);
|
|
139
|
+
if (idx < 0)
|
|
140
|
+
return;
|
|
141
|
+
const n = this.nodes[idx];
|
|
142
|
+
if (n.x == null || n.y == null)
|
|
143
|
+
return;
|
|
144
|
+
this.viewX = n.x - this.viewW / 2;
|
|
145
|
+
this.viewY = n.y - this.viewH / 2;
|
|
146
|
+
this.updateViewBox();
|
|
147
|
+
}
|
|
148
|
+
/** Select a node and apply persistent highlighting to its connections. */
|
|
149
|
+
selectNode(id) {
|
|
150
|
+
this.clearSelection();
|
|
151
|
+
if (!id)
|
|
152
|
+
return;
|
|
153
|
+
const idx = this.nodes.findIndex((n) => n.id === id);
|
|
154
|
+
if (idx < 0)
|
|
155
|
+
return;
|
|
156
|
+
this.selectedNodeId = id;
|
|
157
|
+
this.applySelectionHighlight(id);
|
|
158
|
+
this.nodeGroups[idx].classList.add("selected");
|
|
159
|
+
}
|
|
160
|
+
/** Clear the current persistent selection. */
|
|
161
|
+
clearSelection() {
|
|
162
|
+
if (!this.selectedNodeId)
|
|
163
|
+
return;
|
|
164
|
+
// Remove selected class from previously selected node
|
|
165
|
+
const prevIdx = this.nodes.findIndex((n) => n.id === this.selectedNodeId);
|
|
166
|
+
if (prevIdx >= 0) {
|
|
167
|
+
this.nodeGroups[prevIdx].classList.remove("selected");
|
|
168
|
+
}
|
|
169
|
+
this.selectedNodeId = null;
|
|
170
|
+
// Reset all opacities
|
|
171
|
+
for (let j = 0; j < this.nodes.length; j++) {
|
|
172
|
+
this.nodeGroups[j].style.opacity = "";
|
|
173
|
+
}
|
|
174
|
+
for (let j = 0; j < this.linkElements.length; j++) {
|
|
175
|
+
this.linkElements[j].style.strokeOpacity = "";
|
|
176
|
+
this.linkElements[j].style.stroke = "";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Toggle label visibility on/off. Returns the new state. */
|
|
180
|
+
toggleLabels() {
|
|
181
|
+
this.labelsHidden = !this.labelsHidden;
|
|
182
|
+
this.updateLOD();
|
|
183
|
+
return !this.labelsHidden; // true = labels visible
|
|
184
|
+
}
|
|
185
|
+
/** Get current label visibility state. */
|
|
186
|
+
get labelsVisible() {
|
|
187
|
+
return !this.labelsHidden;
|
|
188
|
+
}
|
|
189
|
+
/** Toggle zone hull visibility on/off. Returns the new state. */
|
|
190
|
+
toggleZones() {
|
|
191
|
+
this.zonesVisible = !this.zonesVisible;
|
|
192
|
+
this.zoneHullGroup.style.display = this.zonesVisible ? "" : "none";
|
|
193
|
+
this.zoneLabelLayer.style.display = this.zonesVisible ? "" : "none";
|
|
194
|
+
return this.zonesVisible;
|
|
195
|
+
}
|
|
196
|
+
/** Get current zone visibility state. */
|
|
197
|
+
get zonesGroupsVisible() {
|
|
198
|
+
return this.zonesVisible;
|
|
199
|
+
}
|
|
200
|
+
/** Collapse a zone group — hides member nodes/edges and shows a summary node. */
|
|
201
|
+
collapseZone(zoneId) {
|
|
202
|
+
if (this.collapsedZones.has(zoneId))
|
|
203
|
+
return;
|
|
204
|
+
this.collapsedZones.add(zoneId);
|
|
205
|
+
this.applyZoneCollapse(zoneId);
|
|
206
|
+
}
|
|
207
|
+
/** Expand a previously collapsed zone group. */
|
|
208
|
+
expandZone(zoneId) {
|
|
209
|
+
if (!this.collapsedZones.has(zoneId))
|
|
210
|
+
return;
|
|
211
|
+
this.collapsedZones.delete(zoneId);
|
|
212
|
+
this.applyZoneExpand(zoneId);
|
|
213
|
+
}
|
|
214
|
+
/** Toggle a zone between collapsed and expanded. Returns true if now collapsed. */
|
|
215
|
+
toggleZoneCollapse(zoneId) {
|
|
216
|
+
if (this.collapsedZones.has(zoneId)) {
|
|
217
|
+
this.expandZone(zoneId);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
this.collapseZone(zoneId);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Check if a zone is currently collapsed. */
|
|
226
|
+
isZoneCollapsed(zoneId) {
|
|
227
|
+
return this.collapsedZones.has(zoneId);
|
|
228
|
+
}
|
|
229
|
+
/** Zoom in by the given factor (default 1.25 = 25% closer). Zooms toward center. */
|
|
230
|
+
zoomIn(factor = 1.25) {
|
|
231
|
+
this.applyZoomFromCenter(1 / factor);
|
|
232
|
+
}
|
|
233
|
+
/** Zoom out by the given factor (default 1.25 = 25% further). Zooms from center. */
|
|
234
|
+
zoomOut(factor = 1.25) {
|
|
235
|
+
this.applyZoomFromCenter(factor);
|
|
236
|
+
}
|
|
237
|
+
/** Reset the viewport to fit all content. */
|
|
238
|
+
resetView() {
|
|
239
|
+
this.fitToContent();
|
|
240
|
+
this.updateLOD();
|
|
241
|
+
}
|
|
242
|
+
destroy() {
|
|
243
|
+
this.destroyed = true;
|
|
244
|
+
this.ac.abort();
|
|
245
|
+
// Stop the physics simulation so no more ticks are scheduled
|
|
246
|
+
this.sim.alpha.value = 0;
|
|
247
|
+
// Release large data structures for GC
|
|
248
|
+
this.nodeEdgeMap.clear();
|
|
249
|
+
this.zoneHullElements.clear();
|
|
250
|
+
this.zoneLabelElements.clear();
|
|
251
|
+
this.collapsedZones.clear();
|
|
252
|
+
this.zoneNodeIndices.clear();
|
|
253
|
+
this.resolvedLinks.length = 0;
|
|
254
|
+
this.linkElements.length = 0;
|
|
255
|
+
this.nodeGroups.length = 0;
|
|
256
|
+
this.nodes.length = 0;
|
|
257
|
+
this.labelRects.length = 0;
|
|
258
|
+
}
|
|
259
|
+
// ── Private: Constructor helpers ──────────────────────────────────────────
|
|
260
|
+
/** Create SVG <defs> with the arrowhead marker. */
|
|
261
|
+
createSvgDefs(svg) {
|
|
262
|
+
const ns = SVG_NS;
|
|
263
|
+
const defs = document.createElementNS(ns, "defs");
|
|
264
|
+
const marker = document.createElementNS(ns, "marker");
|
|
265
|
+
marker.setAttribute("id", "arrowhead");
|
|
266
|
+
marker.setAttribute("viewBox", "0 0 10 7");
|
|
267
|
+
marker.setAttribute("refX", "10");
|
|
268
|
+
marker.setAttribute("refY", "3.5");
|
|
269
|
+
marker.setAttribute("markerWidth", "6");
|
|
270
|
+
marker.setAttribute("markerHeight", "5");
|
|
271
|
+
marker.setAttribute("orient", "auto");
|
|
272
|
+
const polygon = document.createElementNS(ns, "polygon");
|
|
273
|
+
polygon.setAttribute("points", "0 0, 10 3.5, 0 7");
|
|
274
|
+
polygon.setAttribute("fill", "var(--border)");
|
|
275
|
+
marker.appendChild(polygon);
|
|
276
|
+
defs.appendChild(marker);
|
|
277
|
+
svg.appendChild(defs);
|
|
278
|
+
}
|
|
279
|
+
/** Build the zone → node-index map used for hull rendering and collapse. */
|
|
280
|
+
buildZoneNodeMap() {
|
|
281
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
282
|
+
const z = this.nodes[i].zone;
|
|
283
|
+
if (!z)
|
|
284
|
+
continue;
|
|
285
|
+
let indices = this.zoneNodeIndices.get(z);
|
|
286
|
+
if (!indices) {
|
|
287
|
+
indices = [];
|
|
288
|
+
this.zoneNodeIndices.set(z, indices);
|
|
289
|
+
}
|
|
290
|
+
indices.push(i);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** Create an SVG <g> layer, append it to the root group, and return it. */
|
|
294
|
+
createSvgLayer(className) {
|
|
295
|
+
const layer = document.createElementNS(SVG_NS, "g");
|
|
296
|
+
layer.setAttribute("class", className);
|
|
297
|
+
this.g.appendChild(layer);
|
|
298
|
+
return layer;
|
|
299
|
+
}
|
|
300
|
+
/** Resolve string-based link endpoints to GraphNode references. */
|
|
301
|
+
resolveGraphLinks(links, nodeMap) {
|
|
302
|
+
return links.map((l) => ({
|
|
303
|
+
...l,
|
|
304
|
+
source: nodeMap.get(typeof l.source === "string" ? l.source : l.source.id),
|
|
305
|
+
target: nodeMap.get(typeof l.target === "string" ? l.target : l.target.id),
|
|
306
|
+
})).filter((l) => l.source && l.target);
|
|
307
|
+
}
|
|
308
|
+
/** Build a node-id → edge-index adjacency map for hover/select highlighting. */
|
|
309
|
+
buildAdjacencyMap() {
|
|
310
|
+
const map = new Map();
|
|
311
|
+
for (let i = 0; i < this.resolvedLinks.length; i++) {
|
|
312
|
+
const l = this.resolvedLinks[i];
|
|
313
|
+
const sId = l.source.id;
|
|
314
|
+
const tId = l.target.id;
|
|
315
|
+
if (!map.has(sId))
|
|
316
|
+
map.set(sId, new Set());
|
|
317
|
+
if (!map.has(tId))
|
|
318
|
+
map.set(tId, new Set());
|
|
319
|
+
map.get(sId).add(i);
|
|
320
|
+
map.get(tId).add(i);
|
|
321
|
+
}
|
|
322
|
+
return map;
|
|
323
|
+
}
|
|
324
|
+
/** Create SVG line elements for all resolved links. */
|
|
325
|
+
createLinkElements() {
|
|
326
|
+
for (const l of this.resolvedLinks) {
|
|
327
|
+
const line = document.createElementNS(SVG_NS, "line");
|
|
328
|
+
line.setAttribute("class", `graph-link${l.crossZone ? " cross-zone" : ""}`);
|
|
329
|
+
line.setAttribute("marker-end", "url(#arrowhead)");
|
|
330
|
+
this.g.appendChild(line);
|
|
331
|
+
this.linkElements.push(line);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Create SVG groups for all nodes (hit target, circle, label). */
|
|
335
|
+
createNodeElements(nodeLayer) {
|
|
336
|
+
for (const n of this.nodes) {
|
|
337
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
338
|
+
group.setAttribute("class", "graph-node");
|
|
339
|
+
const radius = Math.min(3 + Math.sqrt(n.importCount) * 2, 16);
|
|
340
|
+
this.nodeRadii.push(radius);
|
|
341
|
+
// Invisible hit target — larger than the visible circle for easy clicking
|
|
342
|
+
const hitTarget = document.createElementNS(SVG_NS, "circle");
|
|
343
|
+
hitTarget.setAttribute("r", String(Math.max(radius + 4, 10)));
|
|
344
|
+
hitTarget.setAttribute("fill", "transparent");
|
|
345
|
+
hitTarget.setAttribute("class", "graph-node-hit");
|
|
346
|
+
group.appendChild(hitTarget);
|
|
347
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
348
|
+
circle.setAttribute("r", String(radius));
|
|
349
|
+
circle.setAttribute("fill", n.zoneColor || "#555");
|
|
350
|
+
group.appendChild(circle);
|
|
351
|
+
// Always create labels — LOD + overlap detection controls visibility
|
|
352
|
+
const fullName = basename(n.id);
|
|
353
|
+
const label = document.createElementNS(SVG_NS, "text");
|
|
354
|
+
label.setAttribute("class", "graph-label");
|
|
355
|
+
label.setAttribute("dy", String(-radius - 3));
|
|
356
|
+
label.setAttribute("text-anchor", "middle");
|
|
357
|
+
label.setAttribute("data-full", fullName);
|
|
358
|
+
label.textContent = truncateFilename(fullName);
|
|
359
|
+
group.appendChild(label);
|
|
360
|
+
nodeLayer.appendChild(group);
|
|
361
|
+
this.nodeGroups.push(group);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Pre-allocate label rect objects reused each frame to avoid GC pressure. */
|
|
365
|
+
allocateLabelRects() {
|
|
366
|
+
const rects = new Array(this.nodes.length);
|
|
367
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
368
|
+
rects[i] = { x: 0, y: 0, w: 0, h: 0 };
|
|
369
|
+
}
|
|
370
|
+
return rects;
|
|
371
|
+
}
|
|
372
|
+
/** Create the shared tooltip SVG group (hidden by default). */
|
|
373
|
+
createTooltipElement() {
|
|
374
|
+
const tooltip = document.createElementNS(SVG_NS, "g");
|
|
375
|
+
tooltip.setAttribute("class", "graph-tooltip");
|
|
376
|
+
tooltip.style.display = "none";
|
|
377
|
+
tooltip.style.pointerEvents = "none";
|
|
378
|
+
const tooltipBg = document.createElementNS(SVG_NS, "rect");
|
|
379
|
+
tooltipBg.setAttribute("class", "graph-tooltip-bg");
|
|
380
|
+
tooltipBg.setAttribute("rx", "3");
|
|
381
|
+
tooltipBg.setAttribute("ry", "3");
|
|
382
|
+
tooltip.appendChild(tooltipBg);
|
|
383
|
+
const tooltipText = document.createElementNS(SVG_NS, "text");
|
|
384
|
+
tooltipText.setAttribute("class", "graph-tooltip-text");
|
|
385
|
+
tooltipText.setAttribute("dy", "0.35em");
|
|
386
|
+
tooltip.appendChild(tooltipText);
|
|
387
|
+
return tooltip;
|
|
388
|
+
}
|
|
389
|
+
// ── Private: Selection highlighting ───────────────────────────────────────
|
|
390
|
+
applySelectionHighlight(nodeId) {
|
|
391
|
+
const connectedEdges = this.nodeEdgeMap.get(nodeId) ?? new Set();
|
|
392
|
+
const connectedNodes = new Set([nodeId]);
|
|
393
|
+
for (const ei of connectedEdges) {
|
|
394
|
+
const l = this.resolvedLinks[ei];
|
|
395
|
+
connectedNodes.add(l.source.id);
|
|
396
|
+
connectedNodes.add(l.target.id);
|
|
397
|
+
}
|
|
398
|
+
// Dim non-connected nodes
|
|
399
|
+
for (let j = 0; j < this.nodes.length; j++) {
|
|
400
|
+
const ng = this.nodeGroups[j];
|
|
401
|
+
if (connectedNodes.has(this.nodes[j].id)) {
|
|
402
|
+
ng.style.opacity = "1";
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
ng.style.opacity = "0.2";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Highlight connected edges
|
|
409
|
+
for (let j = 0; j < this.linkElements.length; j++) {
|
|
410
|
+
if (connectedEdges.has(j)) {
|
|
411
|
+
this.linkElements[j].style.strokeOpacity = "0.9";
|
|
412
|
+
this.linkElements[j].style.stroke = "var(--accent)";
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
this.linkElements[j].style.strokeOpacity = "0.05";
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// ── Private: Zone hull management ─────────────────────────────────────────
|
|
420
|
+
/** Create SVG elements for each zone hull (background + label). */
|
|
421
|
+
createZoneHulls() {
|
|
422
|
+
const ns = SVG_NS;
|
|
423
|
+
const signal = this.ac.signal;
|
|
424
|
+
for (const zi of this.zoneInfos) {
|
|
425
|
+
const indices = this.zoneNodeIndices.get(zi.id);
|
|
426
|
+
if (!indices || indices.length < 2)
|
|
427
|
+
continue;
|
|
428
|
+
// Zone hull group
|
|
429
|
+
const zoneG = document.createElementNS(ns, "g");
|
|
430
|
+
zoneG.setAttribute("class", "zone-hull-group");
|
|
431
|
+
zoneG.setAttribute("data-zone", zi.id);
|
|
432
|
+
// Hull path (filled background)
|
|
433
|
+
const path = document.createElementNS(ns, "path");
|
|
434
|
+
path.setAttribute("class", "zone-hull");
|
|
435
|
+
path.setAttribute("fill", zi.color);
|
|
436
|
+
path.setAttribute("stroke", zi.color);
|
|
437
|
+
zoneG.appendChild(path);
|
|
438
|
+
this.zoneHullElements.set(zi.id, path);
|
|
439
|
+
// Zone label group — rendered in a separate layer above nodes for readability
|
|
440
|
+
const labelGroup = document.createElementNS(ns, "g");
|
|
441
|
+
labelGroup.setAttribute("class", "zone-hull-label-group");
|
|
442
|
+
labelGroup.setAttribute("data-zone", zi.id);
|
|
443
|
+
const labelBg = document.createElementNS(ns, "rect");
|
|
444
|
+
labelBg.setAttribute("class", "zone-hull-label-bg");
|
|
445
|
+
labelBg.setAttribute("rx", "4");
|
|
446
|
+
labelBg.setAttribute("ry", "4");
|
|
447
|
+
labelGroup.appendChild(labelBg);
|
|
448
|
+
const labelText = document.createElementNS(ns, "text");
|
|
449
|
+
labelText.setAttribute("class", "zone-hull-label");
|
|
450
|
+
labelText.setAttribute("text-anchor", "middle");
|
|
451
|
+
labelText.setAttribute("dy", "0.35em");
|
|
452
|
+
labelText.textContent = zi.name;
|
|
453
|
+
labelGroup.appendChild(labelText);
|
|
454
|
+
this.zoneLabelLayer.appendChild(labelGroup);
|
|
455
|
+
this.zoneLabelElements.set(zi.id, labelGroup);
|
|
456
|
+
this.zoneHullGroup.appendChild(zoneG);
|
|
457
|
+
// Click on zone hull to select/toggle collapse
|
|
458
|
+
zoneG.addEventListener("click", (e) => {
|
|
459
|
+
// Don't trigger if clicking a node inside the hull
|
|
460
|
+
const target = e.target;
|
|
461
|
+
if (target.tagName === "circle")
|
|
462
|
+
return;
|
|
463
|
+
// Suppress zone select if this click completed a pan gesture
|
|
464
|
+
if (this.wasPanning) {
|
|
465
|
+
this.wasPanning = false;
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
e.stopPropagation();
|
|
469
|
+
if (this.onZoneSelect) {
|
|
470
|
+
this.onZoneSelect(zi.id);
|
|
471
|
+
}
|
|
472
|
+
}, { signal });
|
|
473
|
+
// Double-click to toggle collapse
|
|
474
|
+
zoneG.addEventListener("dblclick", (e) => {
|
|
475
|
+
const target = e.target;
|
|
476
|
+
if (target.tagName === "circle")
|
|
477
|
+
return;
|
|
478
|
+
e.preventDefault();
|
|
479
|
+
e.stopPropagation();
|
|
480
|
+
this.toggleZoneCollapse(zi.id);
|
|
481
|
+
}, { signal });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/** Update hull paths and labels to match current node positions. */
|
|
485
|
+
updateZoneHulls() {
|
|
486
|
+
if (!this.zonesVisible)
|
|
487
|
+
return;
|
|
488
|
+
// Zone label font size scales with zoom (stays readable at all levels)
|
|
489
|
+
const zoneFontSize = Math.max(10, Math.min(14, 12 / Math.sqrt(this.scale)));
|
|
490
|
+
for (const zi of this.zoneInfos) {
|
|
491
|
+
const indices = this.zoneNodeIndices.get(zi.id);
|
|
492
|
+
if (!indices || indices.length < 2)
|
|
493
|
+
continue;
|
|
494
|
+
const path = this.zoneHullElements.get(zi.id);
|
|
495
|
+
const labelGroup = this.zoneLabelElements.get(zi.id);
|
|
496
|
+
if (!path || !labelGroup)
|
|
497
|
+
continue;
|
|
498
|
+
// Skip collapsed zones — hull and label are hidden
|
|
499
|
+
if (this.collapsedZones.has(zi.id)) {
|
|
500
|
+
path.parentElement?.style.setProperty("display", "none");
|
|
501
|
+
labelGroup.style.display = "none";
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
path.parentElement?.style.removeProperty("display");
|
|
505
|
+
labelGroup.style.display = "";
|
|
506
|
+
// Collect visible node positions with padding
|
|
507
|
+
const points = [];
|
|
508
|
+
let cx = 0, cy = 0;
|
|
509
|
+
for (const idx of indices) {
|
|
510
|
+
const n = this.nodes[idx];
|
|
511
|
+
if (n.x != null && n.y != null) {
|
|
512
|
+
points.push([n.x, n.y]);
|
|
513
|
+
cx += n.x;
|
|
514
|
+
cy += n.y;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (points.length < 2)
|
|
518
|
+
continue;
|
|
519
|
+
cx /= points.length;
|
|
520
|
+
cy /= points.length;
|
|
521
|
+
// Compute convex hull with padding
|
|
522
|
+
const padding = 25;
|
|
523
|
+
const hull = convexHull(points);
|
|
524
|
+
const paddedHull = padHull(hull, padding);
|
|
525
|
+
const d = hullToSmoothPath(paddedHull);
|
|
526
|
+
path.setAttribute("d", d);
|
|
527
|
+
// Position zone label above the zone cluster
|
|
528
|
+
const labelY = cy - this.getZoneRadius(indices) - 15;
|
|
529
|
+
const labelText = labelGroup.querySelector("text");
|
|
530
|
+
const labelBg = labelGroup.querySelector("rect");
|
|
531
|
+
if (labelText && labelBg) {
|
|
532
|
+
labelText.style.fontSize = `${zoneFontSize}px`;
|
|
533
|
+
// Position the group at the label center
|
|
534
|
+
labelGroup.setAttribute("transform", `translate(${cx},${labelY})`);
|
|
535
|
+
// Size the background pill around the text
|
|
536
|
+
const textLen = (labelText.textContent || "").length;
|
|
537
|
+
const charWidth = zoneFontSize * 0.55;
|
|
538
|
+
const pillW = textLen * charWidth + 12; // 6px padding each side
|
|
539
|
+
const pillH = zoneFontSize + 8; // 4px padding top/bottom
|
|
540
|
+
labelBg.setAttribute("x", String(-pillW / 2));
|
|
541
|
+
labelBg.setAttribute("y", String(-pillH / 2));
|
|
542
|
+
labelBg.setAttribute("width", String(pillW));
|
|
543
|
+
labelBg.setAttribute("height", String(pillH));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** Get approximate radius of a zone cluster for label positioning. */
|
|
548
|
+
getZoneRadius(indices) {
|
|
549
|
+
if (indices.length === 0)
|
|
550
|
+
return 0;
|
|
551
|
+
let cx = 0, cy = 0;
|
|
552
|
+
for (const i of indices) {
|
|
553
|
+
cx += this.nodes[i].x ?? 0;
|
|
554
|
+
cy += this.nodes[i].y ?? 0;
|
|
555
|
+
}
|
|
556
|
+
cx /= indices.length;
|
|
557
|
+
cy /= indices.length;
|
|
558
|
+
let maxDist = 0;
|
|
559
|
+
for (const i of indices) {
|
|
560
|
+
const dx = (this.nodes[i].x ?? 0) - cx;
|
|
561
|
+
const dy = (this.nodes[i].y ?? 0) - cy;
|
|
562
|
+
maxDist = Math.max(maxDist, Math.sqrt(dx * dx + dy * dy));
|
|
563
|
+
}
|
|
564
|
+
return maxDist;
|
|
565
|
+
}
|
|
566
|
+
/** Hide member nodes/edges when a zone is collapsed, show summary. */
|
|
567
|
+
applyZoneCollapse(zoneId) {
|
|
568
|
+
const indices = this.zoneNodeIndices.get(zoneId);
|
|
569
|
+
if (!indices)
|
|
570
|
+
return;
|
|
571
|
+
// Compute centroid for summary position
|
|
572
|
+
let cx = 0, cy = 0;
|
|
573
|
+
for (const i of indices) {
|
|
574
|
+
cx += this.nodes[i].x ?? 0;
|
|
575
|
+
cy += this.nodes[i].y ?? 0;
|
|
576
|
+
}
|
|
577
|
+
cx /= indices.length;
|
|
578
|
+
cy /= indices.length;
|
|
579
|
+
// Hide individual nodes
|
|
580
|
+
for (const i of indices) {
|
|
581
|
+
this.nodeGroups[i].style.display = "none";
|
|
582
|
+
}
|
|
583
|
+
// Dim edges connected only to hidden nodes
|
|
584
|
+
const hiddenIds = new Set(indices.map(i => this.nodes[i].id));
|
|
585
|
+
for (let i = 0; i < this.resolvedLinks.length; i++) {
|
|
586
|
+
const l = this.resolvedLinks[i];
|
|
587
|
+
const srcHidden = hiddenIds.has(l.source.id);
|
|
588
|
+
const tgtHidden = hiddenIds.has(l.target.id);
|
|
589
|
+
if (srcHidden && tgtHidden) {
|
|
590
|
+
this.linkElements[i].style.display = "none";
|
|
591
|
+
}
|
|
592
|
+
else if (srcHidden || tgtHidden) {
|
|
593
|
+
this.linkElements[i].style.opacity = "0.3";
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Show collapsed summary hull
|
|
597
|
+
const hullGroup = this.zoneHullGroup.querySelector(`[data-zone="${zoneId}"]`);
|
|
598
|
+
if (hullGroup) {
|
|
599
|
+
hullGroup.classList.add("collapsed");
|
|
600
|
+
// Position a summary badge at the centroid
|
|
601
|
+
const ns = SVG_NS;
|
|
602
|
+
let badge = hullGroup.querySelector(".zone-collapse-badge");
|
|
603
|
+
if (!badge) {
|
|
604
|
+
badge = document.createElementNS(ns, "g");
|
|
605
|
+
badge.setAttribute("class", "zone-collapse-badge");
|
|
606
|
+
const circle = document.createElementNS(ns, "circle");
|
|
607
|
+
circle.setAttribute("r", "18");
|
|
608
|
+
circle.setAttribute("class", "zone-collapse-circle");
|
|
609
|
+
const info = this.zoneInfos.find(z => z.id === zoneId);
|
|
610
|
+
if (info)
|
|
611
|
+
circle.setAttribute("fill", info.color);
|
|
612
|
+
badge.appendChild(circle);
|
|
613
|
+
const text = document.createElementNS(ns, "text");
|
|
614
|
+
text.setAttribute("class", "zone-collapse-count");
|
|
615
|
+
text.setAttribute("text-anchor", "middle");
|
|
616
|
+
text.setAttribute("dy", "0.35em");
|
|
617
|
+
text.textContent = String(indices.length);
|
|
618
|
+
badge.appendChild(text);
|
|
619
|
+
hullGroup.appendChild(badge);
|
|
620
|
+
}
|
|
621
|
+
badge.setAttribute("transform", `translate(${cx},${cy})`);
|
|
622
|
+
badge.style.display = "";
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/** Show member nodes/edges when a zone is expanded. */
|
|
626
|
+
applyZoneExpand(zoneId) {
|
|
627
|
+
const indices = this.zoneNodeIndices.get(zoneId);
|
|
628
|
+
if (!indices)
|
|
629
|
+
return;
|
|
630
|
+
// Show individual nodes
|
|
631
|
+
for (const i of indices) {
|
|
632
|
+
this.nodeGroups[i].style.display = "";
|
|
633
|
+
}
|
|
634
|
+
// Restore edges
|
|
635
|
+
const hiddenIds = new Set(indices.map(i => this.nodes[i].id));
|
|
636
|
+
for (let i = 0; i < this.resolvedLinks.length; i++) {
|
|
637
|
+
const l = this.resolvedLinks[i];
|
|
638
|
+
const srcWas = hiddenIds.has(l.source.id);
|
|
639
|
+
const tgtWas = hiddenIds.has(l.target.id);
|
|
640
|
+
if (srcWas || tgtWas) {
|
|
641
|
+
this.linkElements[i].style.display = "";
|
|
642
|
+
this.linkElements[i].style.opacity = "";
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Hide collapsed summary
|
|
646
|
+
const hullGroup = this.zoneHullGroup.querySelector(`[data-zone="${zoneId}"]`);
|
|
647
|
+
if (hullGroup) {
|
|
648
|
+
hullGroup.classList.remove("collapsed");
|
|
649
|
+
const badge = hullGroup.querySelector(".zone-collapse-badge");
|
|
650
|
+
if (badge)
|
|
651
|
+
badge.style.display = "none";
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// ── Private: ViewBox ───────────────────────────────────────────────────────
|
|
655
|
+
updateViewBox() {
|
|
656
|
+
this.svg.setAttribute("viewBox", `${this.viewX} ${this.viewY} ${this.viewW} ${this.viewH}`);
|
|
657
|
+
this.scale = this.viewW / this.width;
|
|
658
|
+
}
|
|
659
|
+
fitToContent() {
|
|
660
|
+
if (this.nodes.length === 0)
|
|
661
|
+
return;
|
|
662
|
+
const xs = this.nodes.map((n) => n.x).sort((a, b) => a - b);
|
|
663
|
+
const ys = this.nodes.map((n) => n.y).sort((a, b) => a - b);
|
|
664
|
+
const lo = Math.floor(this.nodes.length * 0.02);
|
|
665
|
+
const hi = Math.min(Math.ceil(this.nodes.length * 0.98), this.nodes.length - 1);
|
|
666
|
+
const minX = xs[lo];
|
|
667
|
+
const maxX = xs[hi];
|
|
668
|
+
const minY = ys[lo];
|
|
669
|
+
const maxY = ys[hi];
|
|
670
|
+
const padding = 60;
|
|
671
|
+
let fitW = (maxX - minX) + padding * 2;
|
|
672
|
+
let fitH = (maxY - minY) + padding * 2;
|
|
673
|
+
let fitX = minX - padding;
|
|
674
|
+
let fitY = minY - padding;
|
|
675
|
+
// Maintain aspect ratio of container
|
|
676
|
+
const aspect = this.width / this.height;
|
|
677
|
+
const contentAspect = fitW / fitH;
|
|
678
|
+
if (contentAspect > aspect) {
|
|
679
|
+
const newH = fitW / aspect;
|
|
680
|
+
fitY -= (newH - fitH) / 2;
|
|
681
|
+
fitH = newH;
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
const newW = fitH * aspect;
|
|
685
|
+
fitX -= (newW - fitW) / 2;
|
|
686
|
+
fitW = newW;
|
|
687
|
+
}
|
|
688
|
+
this.viewX = fitX;
|
|
689
|
+
this.viewY = fitY;
|
|
690
|
+
this.viewW = fitW;
|
|
691
|
+
this.viewH = fitH;
|
|
692
|
+
this.scale = this.viewW / this.width;
|
|
693
|
+
this.updateViewBox();
|
|
694
|
+
}
|
|
695
|
+
// ── Private: LOD + Label overlap detection ─────────────────────────────────
|
|
696
|
+
/**
|
|
697
|
+
* Update level-of-detail: circle sizing, label visibility, and overlap hiding.
|
|
698
|
+
*
|
|
699
|
+
* Strategy:
|
|
700
|
+
* 1. Zoom-based LOD: hide labels when nodes are too small to read.
|
|
701
|
+
* 2. Density-aware priority: in crowded areas, only show labels for high-import
|
|
702
|
+
* nodes (they are the most useful landmarks).
|
|
703
|
+
* 3. Greedy overlap removal: iterate nodes by importance (import count desc),
|
|
704
|
+
* place labels greedily, hide labels that would overlap already-placed ones.
|
|
705
|
+
* 4. User toggle: labelsHidden overrides everything.
|
|
706
|
+
*/
|
|
707
|
+
updateLOD() {
|
|
708
|
+
const n = this.nodes.length;
|
|
709
|
+
// Phase 1: circle sizing + compute which labels *could* show (zoom-based)
|
|
710
|
+
const fontSize = Math.max(7, Math.min(11, 9 / Math.sqrt(this.scale)));
|
|
711
|
+
// Approximate label dimensions in SVG units
|
|
712
|
+
const charWidth = fontSize * 0.55;
|
|
713
|
+
const labelHeight = fontSize * 1.3;
|
|
714
|
+
// Track which labels pass zoom test (before overlap)
|
|
715
|
+
const zoomVisible = new Array(n);
|
|
716
|
+
for (let i = 0; i < n; i++) {
|
|
717
|
+
const visualRadius = this.nodeRadii[i] / this.scale;
|
|
718
|
+
// Select visible circle (skip the hit target which has class .graph-node-hit)
|
|
719
|
+
const circle = this.nodeGroups[i].querySelector("circle:not(.graph-node-hit)");
|
|
720
|
+
this.nodeGroups[i].style.display = "";
|
|
721
|
+
if (circle && visualRadius < 1) {
|
|
722
|
+
circle.setAttribute("r", String(this.scale));
|
|
723
|
+
}
|
|
724
|
+
else if (circle) {
|
|
725
|
+
circle.setAttribute("r", String(this.nodeRadii[i]));
|
|
726
|
+
}
|
|
727
|
+
// Also update hit target radius to stay proportional
|
|
728
|
+
const hitTarget = this.nodeGroups[i].querySelector(".graph-node-hit");
|
|
729
|
+
if (hitTarget) {
|
|
730
|
+
const hitRadius = Math.max(this.nodeRadii[i] + 4, 10);
|
|
731
|
+
hitTarget.setAttribute("r", String(visualRadius < 1 ? this.scale + 4 : hitRadius));
|
|
732
|
+
}
|
|
733
|
+
zoomVisible[i] = !this.labelsHidden && visualRadius >= 3;
|
|
734
|
+
}
|
|
735
|
+
// Phase 2: sort by importance for greedy placement (descending import count)
|
|
736
|
+
// Use a lightweight index array to avoid allocations on hot path
|
|
737
|
+
const order = this.getSortedLabelOrder();
|
|
738
|
+
// Phase 3: greedy overlap removal
|
|
739
|
+
// We track placed label bounding boxes and skip labels that overlap.
|
|
740
|
+
// Zone labels are pre-seeded as reserved regions so file labels never cover them.
|
|
741
|
+
const placed = [];
|
|
742
|
+
const showLabel = new Array(n).fill(false);
|
|
743
|
+
// Pre-seed zone label bounding boxes as reserved regions
|
|
744
|
+
if (this.zonesVisible) {
|
|
745
|
+
const zoneFontSize = Math.max(10, Math.min(14, 12 / Math.sqrt(this.scale)));
|
|
746
|
+
const zoneCharWidth = zoneFontSize * 0.55;
|
|
747
|
+
for (const zi of this.zoneInfos) {
|
|
748
|
+
const indices = this.zoneNodeIndices.get(zi.id);
|
|
749
|
+
if (!indices || indices.length < 2)
|
|
750
|
+
continue;
|
|
751
|
+
if (this.collapsedZones.has(zi.id))
|
|
752
|
+
continue;
|
|
753
|
+
const labelGroup = this.zoneLabelElements.get(zi.id);
|
|
754
|
+
if (!labelGroup || labelGroup.style.display === "none")
|
|
755
|
+
continue;
|
|
756
|
+
const labelText = labelGroup.querySelector("text");
|
|
757
|
+
if (!labelText)
|
|
758
|
+
continue;
|
|
759
|
+
const textLen = (labelText.textContent || "").length;
|
|
760
|
+
const pillW = textLen * zoneCharWidth + 12;
|
|
761
|
+
const pillH = zoneFontSize + 8;
|
|
762
|
+
// Extract position from transform attribute
|
|
763
|
+
const transform = labelGroup.getAttribute("transform");
|
|
764
|
+
const match = transform?.match(/translate\(([-\d.]+),([-\d.]+)\)/);
|
|
765
|
+
if (match) {
|
|
766
|
+
const zx = parseFloat(match[1]);
|
|
767
|
+
const zy = parseFloat(match[2]);
|
|
768
|
+
placed.push({ x: zx - pillW / 2, y: zy - pillH / 2, w: pillW, h: pillH });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (let oi = 0; oi < n; oi++) {
|
|
773
|
+
const i = order[oi];
|
|
774
|
+
if (!zoomVisible[i])
|
|
775
|
+
continue;
|
|
776
|
+
const node = this.nodes[i];
|
|
777
|
+
const label = this.nodeGroups[i].querySelector("text");
|
|
778
|
+
if (!label)
|
|
779
|
+
continue;
|
|
780
|
+
const textLen = (label.textContent || "").length;
|
|
781
|
+
const lw = textLen * charWidth;
|
|
782
|
+
const lh = labelHeight;
|
|
783
|
+
const lx = (node.x ?? 0) - lw / 2;
|
|
784
|
+
const ly = (node.y ?? 0) - this.nodeRadii[i] - 3 - lh;
|
|
785
|
+
// Check overlap against already-placed labels and reserved zone labels
|
|
786
|
+
let overlaps = false;
|
|
787
|
+
for (let j = 0; j < placed.length; j++) {
|
|
788
|
+
const p = placed[j];
|
|
789
|
+
if (lx < p.x + p.w && lx + lw > p.x && ly < p.y + p.h && ly + lh > p.y) {
|
|
790
|
+
overlaps = true;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (!overlaps) {
|
|
795
|
+
showLabel[i] = true;
|
|
796
|
+
// Reuse pre-allocated rect
|
|
797
|
+
const rect = this.labelRects[i];
|
|
798
|
+
rect.x = lx;
|
|
799
|
+
rect.y = ly;
|
|
800
|
+
rect.w = lw;
|
|
801
|
+
rect.h = lh;
|
|
802
|
+
placed.push(rect);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// Phase 4: apply visibility + font size
|
|
806
|
+
for (let i = 0; i < n; i++) {
|
|
807
|
+
const label = this.nodeGroups[i].querySelector("text");
|
|
808
|
+
if (!label)
|
|
809
|
+
continue;
|
|
810
|
+
if (showLabel[i]) {
|
|
811
|
+
label.style.display = "";
|
|
812
|
+
label.style.fontSize = `${fontSize}px`;
|
|
813
|
+
label.classList.remove("graph-label-hidden");
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
label.style.display = "none";
|
|
817
|
+
label.classList.add("graph-label-hidden");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/** Cached sort order — recomputed only when node count changes. */
|
|
822
|
+
_sortedOrder = null;
|
|
823
|
+
_sortedOrderLen = -1;
|
|
824
|
+
/** Get indices sorted by import count (descending). Cached per node-count. */
|
|
825
|
+
getSortedLabelOrder() {
|
|
826
|
+
const n = this.nodes.length;
|
|
827
|
+
if (this._sortedOrder && this._sortedOrderLen === n)
|
|
828
|
+
return this._sortedOrder;
|
|
829
|
+
const order = new Array(n);
|
|
830
|
+
for (let i = 0; i < n; i++)
|
|
831
|
+
order[i] = i;
|
|
832
|
+
const nodes = this.nodes;
|
|
833
|
+
order.sort((a, b) => nodes[b].importCount - nodes[a].importCount);
|
|
834
|
+
this._sortedOrder = order;
|
|
835
|
+
this._sortedOrderLen = n;
|
|
836
|
+
return order;
|
|
837
|
+
}
|
|
838
|
+
// ── Private: DOM update (called by physics tick) ───────────────────────────
|
|
839
|
+
updateDOM() {
|
|
840
|
+
// Update link positions
|
|
841
|
+
for (let i = 0; i < this.resolvedLinks.length; i++) {
|
|
842
|
+
const l = this.resolvedLinks[i];
|
|
843
|
+
this.linkElements[i].setAttribute("x1", String(l.source.x));
|
|
844
|
+
this.linkElements[i].setAttribute("y1", String(l.source.y));
|
|
845
|
+
this.linkElements[i].setAttribute("x2", String(l.target.x));
|
|
846
|
+
this.linkElements[i].setAttribute("y2", String(l.target.y));
|
|
847
|
+
}
|
|
848
|
+
// Update node positions and LOD
|
|
849
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
850
|
+
this.nodeGroups[i].setAttribute("transform", `translate(${this.nodes[i].x},${this.nodes[i].y})`);
|
|
851
|
+
}
|
|
852
|
+
this.updateLOD();
|
|
853
|
+
// Update zone hull boundaries
|
|
854
|
+
this.updateZoneHulls();
|
|
855
|
+
}
|
|
856
|
+
// ── Private: Simulation ────────────────────────────────────────────────────
|
|
857
|
+
tickCallbacks() {
|
|
858
|
+
return {
|
|
859
|
+
updateDOM: () => this.updateDOM(),
|
|
860
|
+
fitToContent: () => this.fitToContent(),
|
|
861
|
+
scheduleNextTick: (fn) => { if (!this.destroyed)
|
|
862
|
+
requestAnimationFrame(fn); },
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
startSimulation() {
|
|
866
|
+
if (this.destroyed)
|
|
867
|
+
return;
|
|
868
|
+
const runTick = () => { if (!this.destroyed)
|
|
869
|
+
tick(this.sim, this.tickCallbacks()); };
|
|
870
|
+
requestAnimationFrame(runTick);
|
|
871
|
+
}
|
|
872
|
+
/** Re-heat the simulation to adapt to moved nodes. */
|
|
873
|
+
reheat() {
|
|
874
|
+
if (this.destroyed)
|
|
875
|
+
return;
|
|
876
|
+
if (this.sim.alpha.value < 0.01) {
|
|
877
|
+
this.sim.alpha.value = 0.3;
|
|
878
|
+
requestAnimationFrame(() => { if (!this.destroyed)
|
|
879
|
+
tick(this.sim, this.tickCallbacks()); });
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
this.sim.alpha.value = Math.max(this.sim.alpha.value, 0.3);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// ── Private: Coordinate helpers ──────────────────────────────────────────
|
|
886
|
+
/** Convert client (screen) coordinates to SVG viewBox coordinates. */
|
|
887
|
+
clientToViewBox(clientX, clientY) {
|
|
888
|
+
const rect = this.svg.getBoundingClientRect();
|
|
889
|
+
return {
|
|
890
|
+
x: this.viewX + ((clientX - rect.left) / rect.width) * this.viewW,
|
|
891
|
+
y: this.viewY + ((clientY - rect.top) / rect.height) * this.viewH,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/** Find the node index under a given SVG element by walking up the DOM tree. */
|
|
895
|
+
findNodeIndex(target) {
|
|
896
|
+
let el = target;
|
|
897
|
+
// Walk up until we find a .graph-node group or leave the SVG
|
|
898
|
+
while (el && el !== this.svg) {
|
|
899
|
+
if (el.classList?.contains("graph-node")) {
|
|
900
|
+
return this.nodeGroups.indexOf(el);
|
|
901
|
+
}
|
|
902
|
+
// Direct circle/text child of a node group (fast path)
|
|
903
|
+
if ((el.tagName === "circle" || el.tagName === "text") && el.parentElement?.classList?.contains("graph-node")) {
|
|
904
|
+
return this.nodeGroups.indexOf(el.parentElement);
|
|
905
|
+
}
|
|
906
|
+
el = el.parentElement;
|
|
907
|
+
}
|
|
908
|
+
return -1;
|
|
909
|
+
}
|
|
910
|
+
// ── Private: Zoom ──────────────────────────────────────────────────────────
|
|
911
|
+
/** Apply a zoom factor centered on the current viewport center. */
|
|
912
|
+
applyZoomFromCenter(factor) {
|
|
913
|
+
const cx = this.viewX + this.viewW / 2;
|
|
914
|
+
const cy = this.viewY + this.viewH / 2;
|
|
915
|
+
const newW = this.viewW * factor;
|
|
916
|
+
const newH = this.viewH * factor;
|
|
917
|
+
this.viewX = cx - newW / 2;
|
|
918
|
+
this.viewY = cy - newH / 2;
|
|
919
|
+
this.viewW = newW;
|
|
920
|
+
this.viewH = newH;
|
|
921
|
+
this.scale = this.viewW / this.width;
|
|
922
|
+
this.updateViewBox();
|
|
923
|
+
this.updateLOD();
|
|
924
|
+
}
|
|
925
|
+
setupZoom() {
|
|
926
|
+
this.svg.addEventListener("wheel", (e) => {
|
|
927
|
+
e.preventDefault();
|
|
928
|
+
const mouseVB = this.clientToViewBox(e.clientX, e.clientY);
|
|
929
|
+
const zoomFactor = e.deltaY > 0 ? 1.1 : 0.9;
|
|
930
|
+
const newW = this.viewW * zoomFactor;
|
|
931
|
+
const newH = this.viewH * zoomFactor;
|
|
932
|
+
this.viewX = mouseVB.x - (mouseVB.x - this.viewX) * (newW / this.viewW);
|
|
933
|
+
this.viewY = mouseVB.y - (mouseVB.y - this.viewY) * (newH / this.viewH);
|
|
934
|
+
this.viewW = newW;
|
|
935
|
+
this.viewH = newH;
|
|
936
|
+
this.scale = this.viewW / this.width;
|
|
937
|
+
this.updateViewBox();
|
|
938
|
+
this.updateLOD();
|
|
939
|
+
}, { passive: false, signal: this.ac.signal });
|
|
940
|
+
}
|
|
941
|
+
// ── Private: Pan + Node drag (mouse) ───────────────────────────────────────
|
|
942
|
+
setupPanAndDrag(onNodeSelect, onNodeDblClick) {
|
|
943
|
+
let isPanning = false;
|
|
944
|
+
let panStartX = 0, panStartY = 0;
|
|
945
|
+
let panStartVX = 0, panStartVY = 0;
|
|
946
|
+
let dragNode = null;
|
|
947
|
+
let dragNodeIdx = -1;
|
|
948
|
+
let mouseDownPos = null;
|
|
949
|
+
let isDragging = false;
|
|
950
|
+
const DRAG_THRESHOLD = 3;
|
|
951
|
+
const signal = this.ac.signal;
|
|
952
|
+
// Double-click handler
|
|
953
|
+
this.svg.addEventListener("dblclick", (e) => {
|
|
954
|
+
const target = e.target;
|
|
955
|
+
const idx = this.findNodeIndex(target);
|
|
956
|
+
if (idx >= 0 && onNodeDblClick) {
|
|
957
|
+
e.preventDefault();
|
|
958
|
+
onNodeDblClick(this.nodes[idx].id);
|
|
959
|
+
}
|
|
960
|
+
}, { signal });
|
|
961
|
+
this.svg.addEventListener("mousedown", (e) => {
|
|
962
|
+
// Only handle primary (left) button
|
|
963
|
+
if (e.button !== 0)
|
|
964
|
+
return;
|
|
965
|
+
// Reset pan gesture flag at the start of each interaction
|
|
966
|
+
this.wasPanning = false;
|
|
967
|
+
const target = e.target;
|
|
968
|
+
const idx = this.findNodeIndex(target);
|
|
969
|
+
if (idx >= 0) {
|
|
970
|
+
// Prevent browser text selection / native drag during node drag
|
|
971
|
+
e.preventDefault();
|
|
972
|
+
dragNode = this.nodes[idx];
|
|
973
|
+
dragNodeIdx = idx;
|
|
974
|
+
mouseDownPos = { x: e.clientX, y: e.clientY };
|
|
975
|
+
isDragging = false;
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
// Background click clears selection
|
|
979
|
+
if (target === this.svg || target === this.g) {
|
|
980
|
+
this.clearSelection();
|
|
981
|
+
}
|
|
982
|
+
// Prevent browser text selection / native drag during pan
|
|
983
|
+
e.preventDefault();
|
|
984
|
+
// Background pan
|
|
985
|
+
isPanning = true;
|
|
986
|
+
panStartX = e.clientX;
|
|
987
|
+
panStartY = e.clientY;
|
|
988
|
+
panStartVX = this.viewX;
|
|
989
|
+
panStartVY = this.viewY;
|
|
990
|
+
this.svg.classList.add("grabbing");
|
|
991
|
+
}, { signal });
|
|
992
|
+
// mousemove and mouseup are attached to window so dragging/panning
|
|
993
|
+
// continues even when the pointer leaves the SVG boundary.
|
|
994
|
+
const onMouseMove = (e) => {
|
|
995
|
+
// Node drag
|
|
996
|
+
if (dragNode && mouseDownPos) {
|
|
997
|
+
const dx = e.clientX - mouseDownPos.x;
|
|
998
|
+
const dy = e.clientY - mouseDownPos.y;
|
|
999
|
+
if (!isDragging) {
|
|
1000
|
+
if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
|
|
1001
|
+
isDragging = true;
|
|
1002
|
+
dragNode.fx = dragNode.x;
|
|
1003
|
+
dragNode.fy = dragNode.y;
|
|
1004
|
+
// Visual feedback for dragging
|
|
1005
|
+
this.svg.classList.add("node-dragging");
|
|
1006
|
+
if (dragNodeIdx >= 0)
|
|
1007
|
+
this.nodeGroups[dragNodeIdx].classList.add("dragging");
|
|
1008
|
+
this.reheat();
|
|
1009
|
+
}
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
// Convert client coords to viewBox coords
|
|
1013
|
+
const vb = this.clientToViewBox(e.clientX, e.clientY);
|
|
1014
|
+
dragNode.fx = vb.x;
|
|
1015
|
+
dragNode.fy = vb.y;
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
// Background pan
|
|
1019
|
+
if (isPanning) {
|
|
1020
|
+
const dx = e.clientX - panStartX;
|
|
1021
|
+
const dy = e.clientY - panStartY;
|
|
1022
|
+
if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
|
|
1023
|
+
this.wasPanning = true;
|
|
1024
|
+
}
|
|
1025
|
+
const rect = this.svg.getBoundingClientRect();
|
|
1026
|
+
this.viewX = panStartVX - (dx / rect.width) * this.viewW;
|
|
1027
|
+
this.viewY = panStartVY - (dy / rect.height) * this.viewH;
|
|
1028
|
+
this.updateViewBox();
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
const onMouseUp = () => {
|
|
1032
|
+
if (dragNode) {
|
|
1033
|
+
if (!isDragging && dragNodeIdx >= 0) {
|
|
1034
|
+
// Click (no drag) — select node and show details
|
|
1035
|
+
const n = this.nodes[dragNodeIdx];
|
|
1036
|
+
const fileName = basename(n.id);
|
|
1037
|
+
this.selectNode(n.id);
|
|
1038
|
+
onNodeSelect({
|
|
1039
|
+
title: fileName,
|
|
1040
|
+
path: n.id,
|
|
1041
|
+
zone: n.zone || "unzoned",
|
|
1042
|
+
incomingImports: n.importCount,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
else if (isDragging) {
|
|
1046
|
+
// Drag ended — persist position for the session by updating x/y
|
|
1047
|
+
// and then release the fixed constraint
|
|
1048
|
+
dragNode.x = dragNode.fx;
|
|
1049
|
+
dragNode.y = dragNode.fy;
|
|
1050
|
+
dragNode.vx = 0;
|
|
1051
|
+
dragNode.vy = 0;
|
|
1052
|
+
}
|
|
1053
|
+
// Clear drag visual feedback
|
|
1054
|
+
this.svg.classList.remove("node-dragging");
|
|
1055
|
+
if (dragNodeIdx >= 0)
|
|
1056
|
+
this.nodeGroups[dragNodeIdx].classList.remove("dragging");
|
|
1057
|
+
dragNode.fx = null;
|
|
1058
|
+
dragNode.fy = null;
|
|
1059
|
+
dragNode = null;
|
|
1060
|
+
dragNodeIdx = -1;
|
|
1061
|
+
mouseDownPos = null;
|
|
1062
|
+
isDragging = false;
|
|
1063
|
+
}
|
|
1064
|
+
if (isPanning) {
|
|
1065
|
+
isPanning = false;
|
|
1066
|
+
this.svg.classList.remove("grabbing");
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
window.addEventListener("mousemove", onMouseMove, { signal });
|
|
1070
|
+
window.addEventListener("mouseup", onMouseUp, { signal });
|
|
1071
|
+
}
|
|
1072
|
+
// ── Private: Touch interaction ─────────────────────────────────────────────
|
|
1073
|
+
setupTouchInteraction(onNodeSelect, onNodeDblClick) {
|
|
1074
|
+
let touchDragNode = null;
|
|
1075
|
+
let touchDragIdx = -1;
|
|
1076
|
+
let touchStartPos = null;
|
|
1077
|
+
let isTouchDragging = false;
|
|
1078
|
+
const DRAG_THRESHOLD = 8; // higher threshold for touch
|
|
1079
|
+
// Touch pan state
|
|
1080
|
+
let isTouchPanning = false;
|
|
1081
|
+
let touchPanStartX = 0, touchPanStartY = 0;
|
|
1082
|
+
let touchPanStartVX = 0, touchPanStartVY = 0;
|
|
1083
|
+
// Double-tap detection
|
|
1084
|
+
let lastTapTime = 0;
|
|
1085
|
+
let lastTapNodeIdx = -1;
|
|
1086
|
+
const DOUBLE_TAP_DELAY = 300;
|
|
1087
|
+
// Pinch-zoom state
|
|
1088
|
+
let pinchStartDist = 0;
|
|
1089
|
+
let pinchStartViewW = 0;
|
|
1090
|
+
let pinchStartViewH = 0;
|
|
1091
|
+
let pinchMidX = 0;
|
|
1092
|
+
let pinchMidY = 0;
|
|
1093
|
+
const signal = this.ac.signal;
|
|
1094
|
+
this.svg.addEventListener("touchstart", (e) => {
|
|
1095
|
+
// Pinch-zoom: two-finger gesture
|
|
1096
|
+
if (e.touches.length === 2) {
|
|
1097
|
+
e.preventDefault();
|
|
1098
|
+
isTouchPanning = false;
|
|
1099
|
+
touchDragNode = null;
|
|
1100
|
+
const t0 = e.touches[0];
|
|
1101
|
+
const t1 = e.touches[1];
|
|
1102
|
+
pinchStartDist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
|
|
1103
|
+
pinchStartViewW = this.viewW;
|
|
1104
|
+
pinchStartViewH = this.viewH;
|
|
1105
|
+
const midX = (t0.clientX + t1.clientX) / 2;
|
|
1106
|
+
const midY = (t0.clientY + t1.clientY) / 2;
|
|
1107
|
+
const vb = this.clientToViewBox(midX, midY);
|
|
1108
|
+
pinchMidX = vb.x;
|
|
1109
|
+
pinchMidY = vb.y;
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (e.touches.length !== 1)
|
|
1113
|
+
return;
|
|
1114
|
+
const touch = e.touches[0];
|
|
1115
|
+
const target = touch.target;
|
|
1116
|
+
const idx = this.findNodeIndex(target);
|
|
1117
|
+
if (idx >= 0) {
|
|
1118
|
+
e.preventDefault();
|
|
1119
|
+
touchDragNode = this.nodes[idx];
|
|
1120
|
+
touchDragIdx = idx;
|
|
1121
|
+
touchStartPos = { x: touch.clientX, y: touch.clientY };
|
|
1122
|
+
isTouchDragging = false;
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
// Background touch pan
|
|
1126
|
+
isTouchPanning = true;
|
|
1127
|
+
touchPanStartX = touch.clientX;
|
|
1128
|
+
touchPanStartY = touch.clientY;
|
|
1129
|
+
touchPanStartVX = this.viewX;
|
|
1130
|
+
touchPanStartVY = this.viewY;
|
|
1131
|
+
}, { passive: false, signal });
|
|
1132
|
+
this.svg.addEventListener("touchmove", (e) => {
|
|
1133
|
+
// Pinch-zoom
|
|
1134
|
+
if (e.touches.length === 2) {
|
|
1135
|
+
e.preventDefault();
|
|
1136
|
+
const t0 = e.touches[0];
|
|
1137
|
+
const t1 = e.touches[1];
|
|
1138
|
+
const dist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
|
|
1139
|
+
const ratio = pinchStartDist / dist;
|
|
1140
|
+
const newW = pinchStartViewW * ratio;
|
|
1141
|
+
const newH = pinchStartViewH * ratio;
|
|
1142
|
+
this.viewX = pinchMidX - (pinchMidX - this.viewX) * (newW / this.viewW);
|
|
1143
|
+
this.viewY = pinchMidY - (pinchMidY - this.viewY) * (newH / this.viewH);
|
|
1144
|
+
this.viewW = newW;
|
|
1145
|
+
this.viewH = newH;
|
|
1146
|
+
this.scale = this.viewW / this.width;
|
|
1147
|
+
this.updateViewBox();
|
|
1148
|
+
this.updateLOD();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (e.touches.length !== 1)
|
|
1152
|
+
return;
|
|
1153
|
+
const touch = e.touches[0];
|
|
1154
|
+
// Node drag
|
|
1155
|
+
if (touchDragNode && touchStartPos) {
|
|
1156
|
+
const dx = touch.clientX - touchStartPos.x;
|
|
1157
|
+
const dy = touch.clientY - touchStartPos.y;
|
|
1158
|
+
if (!isTouchDragging) {
|
|
1159
|
+
if (Math.hypot(dx, dy) > DRAG_THRESHOLD) {
|
|
1160
|
+
isTouchDragging = true;
|
|
1161
|
+
touchDragNode.fx = touchDragNode.x;
|
|
1162
|
+
touchDragNode.fy = touchDragNode.y;
|
|
1163
|
+
// Visual feedback for touch dragging
|
|
1164
|
+
this.svg.classList.add("node-dragging");
|
|
1165
|
+
if (touchDragIdx >= 0)
|
|
1166
|
+
this.nodeGroups[touchDragIdx].classList.add("dragging");
|
|
1167
|
+
this.reheat();
|
|
1168
|
+
}
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
e.preventDefault();
|
|
1172
|
+
const vb = this.clientToViewBox(touch.clientX, touch.clientY);
|
|
1173
|
+
touchDragNode.fx = vb.x;
|
|
1174
|
+
touchDragNode.fy = vb.y;
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
// Background pan
|
|
1178
|
+
if (isTouchPanning) {
|
|
1179
|
+
const dx = touch.clientX - touchPanStartX;
|
|
1180
|
+
const dy = touch.clientY - touchPanStartY;
|
|
1181
|
+
const rect = this.svg.getBoundingClientRect();
|
|
1182
|
+
this.viewX = touchPanStartVX - (dx / rect.width) * this.viewW;
|
|
1183
|
+
this.viewY = touchPanStartVY - (dy / rect.height) * this.viewH;
|
|
1184
|
+
this.updateViewBox();
|
|
1185
|
+
}
|
|
1186
|
+
}, { passive: false, signal });
|
|
1187
|
+
this.svg.addEventListener("touchend", (e) => {
|
|
1188
|
+
if (touchDragNode) {
|
|
1189
|
+
if (!isTouchDragging && touchDragIdx >= 0) {
|
|
1190
|
+
const n = this.nodes[touchDragIdx];
|
|
1191
|
+
const now = Date.now();
|
|
1192
|
+
// Double-tap detection
|
|
1193
|
+
if (now - lastTapTime < DOUBLE_TAP_DELAY && lastTapNodeIdx === touchDragIdx) {
|
|
1194
|
+
if (onNodeDblClick)
|
|
1195
|
+
onNodeDblClick(n.id);
|
|
1196
|
+
lastTapTime = 0;
|
|
1197
|
+
lastTapNodeIdx = -1;
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
// Single tap — select node
|
|
1201
|
+
lastTapTime = now;
|
|
1202
|
+
lastTapNodeIdx = touchDragIdx;
|
|
1203
|
+
const fileName = basename(n.id);
|
|
1204
|
+
this.selectNode(n.id);
|
|
1205
|
+
onNodeSelect({
|
|
1206
|
+
title: fileName,
|
|
1207
|
+
path: n.id,
|
|
1208
|
+
zone: n.zone || "unzoned",
|
|
1209
|
+
incomingImports: n.importCount,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
else if (isTouchDragging && touchDragNode) {
|
|
1214
|
+
// Drag ended — persist position
|
|
1215
|
+
touchDragNode.x = touchDragNode.fx;
|
|
1216
|
+
touchDragNode.y = touchDragNode.fy;
|
|
1217
|
+
touchDragNode.vx = 0;
|
|
1218
|
+
touchDragNode.vy = 0;
|
|
1219
|
+
}
|
|
1220
|
+
// Clear drag visual feedback
|
|
1221
|
+
this.svg.classList.remove("node-dragging");
|
|
1222
|
+
if (touchDragIdx >= 0)
|
|
1223
|
+
this.nodeGroups[touchDragIdx].classList.remove("dragging");
|
|
1224
|
+
if (touchDragNode) {
|
|
1225
|
+
touchDragNode.fx = null;
|
|
1226
|
+
touchDragNode.fy = null;
|
|
1227
|
+
}
|
|
1228
|
+
touchDragNode = null;
|
|
1229
|
+
touchDragIdx = -1;
|
|
1230
|
+
touchStartPos = null;
|
|
1231
|
+
isTouchDragging = false;
|
|
1232
|
+
}
|
|
1233
|
+
// Clear touch pan on last finger lift
|
|
1234
|
+
if (e.touches.length === 0) {
|
|
1235
|
+
isTouchPanning = false;
|
|
1236
|
+
}
|
|
1237
|
+
}, { signal });
|
|
1238
|
+
this.svg.addEventListener("touchcancel", () => {
|
|
1239
|
+
this.svg.classList.remove("node-dragging");
|
|
1240
|
+
if (touchDragIdx >= 0)
|
|
1241
|
+
this.nodeGroups[touchDragIdx].classList.remove("dragging");
|
|
1242
|
+
if (touchDragNode) {
|
|
1243
|
+
touchDragNode.fx = null;
|
|
1244
|
+
touchDragNode.fy = null;
|
|
1245
|
+
touchDragNode = null;
|
|
1246
|
+
}
|
|
1247
|
+
touchDragIdx = -1;
|
|
1248
|
+
touchStartPos = null;
|
|
1249
|
+
isTouchDragging = false;
|
|
1250
|
+
isTouchPanning = false;
|
|
1251
|
+
}, { signal });
|
|
1252
|
+
}
|
|
1253
|
+
// ── Private: Label tooltips ──────────────────────────────────────────────
|
|
1254
|
+
/**
|
|
1255
|
+
* Show full filename tooltip on node hover. The tooltip is a single shared
|
|
1256
|
+
* SVG group that follows the hovered node. This avoids per-node <title>
|
|
1257
|
+
* elements (which have inconsistent browser rendering and delays).
|
|
1258
|
+
*/
|
|
1259
|
+
setupLabelTooltips() {
|
|
1260
|
+
const signal = this.ac.signal;
|
|
1261
|
+
const tooltipText = this.tooltip.querySelector("text");
|
|
1262
|
+
const tooltipBg = this.tooltip.querySelector("rect");
|
|
1263
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
1264
|
+
this.nodeGroups[i].addEventListener("mouseenter", () => {
|
|
1265
|
+
const node = this.nodes[i];
|
|
1266
|
+
const fullName = basename(node.id);
|
|
1267
|
+
const displayName = this.nodeGroups[i].querySelector("text")?.textContent || "";
|
|
1268
|
+
// Only show tooltip if the label is truncated or hidden
|
|
1269
|
+
const label = this.nodeGroups[i].querySelector("text");
|
|
1270
|
+
const labelHidden = label?.style.display === "none";
|
|
1271
|
+
if (fullName === displayName && !labelHidden) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
tooltipText.textContent = fullName;
|
|
1275
|
+
// Position tooltip above node
|
|
1276
|
+
const tx = node.x ?? 0;
|
|
1277
|
+
const ty = (node.y ?? 0) - this.nodeRadii[i] - 18;
|
|
1278
|
+
this.tooltip.setAttribute("transform", `translate(${tx},${ty})`);
|
|
1279
|
+
// Size the background rectangle around the text
|
|
1280
|
+
const textLen = fullName.length;
|
|
1281
|
+
const approxWidth = textLen * (9 * 0.55) + 8; // 9px font, 0.55 char width, 4px padding each side
|
|
1282
|
+
tooltipBg.setAttribute("x", String(-approxWidth / 2));
|
|
1283
|
+
tooltipBg.setAttribute("y", "-8");
|
|
1284
|
+
tooltipBg.setAttribute("width", String(approxWidth));
|
|
1285
|
+
tooltipBg.setAttribute("height", "16");
|
|
1286
|
+
tooltipText.setAttribute("text-anchor", "middle");
|
|
1287
|
+
this.tooltip.style.display = "";
|
|
1288
|
+
}, { signal });
|
|
1289
|
+
this.nodeGroups[i].addEventListener("mouseleave", () => {
|
|
1290
|
+
this.tooltip.style.display = "none";
|
|
1291
|
+
}, { signal });
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
// ── Private: Hover highlighting ────────────────────────────────────────────
|
|
1295
|
+
setupHoverHighlighting() {
|
|
1296
|
+
let hoveredNode = null;
|
|
1297
|
+
const signal = this.ac.signal;
|
|
1298
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
1299
|
+
const nodeId = this.nodes[i].id;
|
|
1300
|
+
this.nodeGroups[i].addEventListener("mouseenter", () => {
|
|
1301
|
+
// Don't override persistent selection with hover
|
|
1302
|
+
if (this.selectedNodeId)
|
|
1303
|
+
return;
|
|
1304
|
+
if (hoveredNode === nodeId)
|
|
1305
|
+
return;
|
|
1306
|
+
hoveredNode = nodeId;
|
|
1307
|
+
const connectedEdges = this.nodeEdgeMap.get(nodeId) ?? new Set();
|
|
1308
|
+
const connectedNodes = new Set([nodeId]);
|
|
1309
|
+
for (const ei of connectedEdges) {
|
|
1310
|
+
const l = this.resolvedLinks[ei];
|
|
1311
|
+
connectedNodes.add(l.source.id);
|
|
1312
|
+
connectedNodes.add(l.target.id);
|
|
1313
|
+
}
|
|
1314
|
+
// Dim non-connected
|
|
1315
|
+
for (let j = 0; j < this.nodes.length; j++) {
|
|
1316
|
+
const ng = this.nodeGroups[j];
|
|
1317
|
+
if (connectedNodes.has(this.nodes[j].id)) {
|
|
1318
|
+
ng.style.opacity = "1";
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
ng.style.opacity = "0.2";
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
for (let j = 0; j < this.linkElements.length; j++) {
|
|
1325
|
+
if (connectedEdges.has(j)) {
|
|
1326
|
+
this.linkElements[j].style.strokeOpacity = "0.9";
|
|
1327
|
+
this.linkElements[j].style.stroke = "var(--accent)";
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
this.linkElements[j].style.strokeOpacity = "0.05";
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}, { signal });
|
|
1334
|
+
this.nodeGroups[i].addEventListener("mouseleave", () => {
|
|
1335
|
+
// Don't clear if a node is persistently selected
|
|
1336
|
+
if (this.selectedNodeId)
|
|
1337
|
+
return;
|
|
1338
|
+
if (hoveredNode !== nodeId)
|
|
1339
|
+
return;
|
|
1340
|
+
hoveredNode = null;
|
|
1341
|
+
for (let j = 0; j < this.nodes.length; j++) {
|
|
1342
|
+
this.nodeGroups[j].style.opacity = "";
|
|
1343
|
+
}
|
|
1344
|
+
for (let j = 0; j < this.linkElements.length; j++) {
|
|
1345
|
+
this.linkElements[j].style.strokeOpacity = "";
|
|
1346
|
+
this.linkElements[j].style.stroke = "";
|
|
1347
|
+
}
|
|
1348
|
+
}, { signal });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
// ── Geometry helpers (module-level, no DOM) ──────────────────────────────────
|
|
1353
|
+
/**
|
|
1354
|
+
* Compute convex hull of a set of 2D points using Andrew's monotone chain.
|
|
1355
|
+
* Returns points in counter-clockwise order. O(n log n).
|
|
1356
|
+
*/
|
|
1357
|
+
function convexHull(points) {
|
|
1358
|
+
if (points.length <= 2)
|
|
1359
|
+
return [...points];
|
|
1360
|
+
const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
1361
|
+
const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
1362
|
+
const lower = [];
|
|
1363
|
+
for (const p of sorted) {
|
|
1364
|
+
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
|
|
1365
|
+
lower.pop();
|
|
1366
|
+
lower.push(p);
|
|
1367
|
+
}
|
|
1368
|
+
const upper = [];
|
|
1369
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
1370
|
+
const p = sorted[i];
|
|
1371
|
+
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
|
|
1372
|
+
upper.pop();
|
|
1373
|
+
upper.push(p);
|
|
1374
|
+
}
|
|
1375
|
+
// Remove last point of each half (duplicate of first point of other half)
|
|
1376
|
+
lower.pop();
|
|
1377
|
+
upper.pop();
|
|
1378
|
+
return lower.concat(upper);
|
|
1379
|
+
}
|
|
1380
|
+
/** Expand a convex hull outward by a padding distance. */
|
|
1381
|
+
function padHull(hull, padding) {
|
|
1382
|
+
if (hull.length < 3) {
|
|
1383
|
+
// For degenerate cases, expand bounding box
|
|
1384
|
+
const xs = hull.map(p => p[0]);
|
|
1385
|
+
const ys = hull.map(p => p[1]);
|
|
1386
|
+
const minX = Math.min(...xs) - padding;
|
|
1387
|
+
const maxX = Math.max(...xs) + padding;
|
|
1388
|
+
const minY = Math.min(...ys) - padding;
|
|
1389
|
+
const maxY = Math.max(...ys) + padding;
|
|
1390
|
+
return [[minX, minY], [maxX, minY], [maxX, maxY], [minX, maxY]];
|
|
1391
|
+
}
|
|
1392
|
+
const result = [];
|
|
1393
|
+
const n = hull.length;
|
|
1394
|
+
// Compute centroid
|
|
1395
|
+
let cx = 0, cy = 0;
|
|
1396
|
+
for (const [x, y] of hull) {
|
|
1397
|
+
cx += x;
|
|
1398
|
+
cy += y;
|
|
1399
|
+
}
|
|
1400
|
+
cx /= n;
|
|
1401
|
+
cy /= n;
|
|
1402
|
+
// Push each vertex outward from centroid
|
|
1403
|
+
for (const [x, y] of hull) {
|
|
1404
|
+
const dx = x - cx;
|
|
1405
|
+
const dy = y - cy;
|
|
1406
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
1407
|
+
result.push([x + (dx / dist) * padding, y + (dy / dist) * padding]);
|
|
1408
|
+
}
|
|
1409
|
+
return result;
|
|
1410
|
+
}
|
|
1411
|
+
/** Convert a hull (polygon) to a smooth SVG path with rounded corners. */
|
|
1412
|
+
function hullToSmoothPath(hull) {
|
|
1413
|
+
if (hull.length < 3)
|
|
1414
|
+
return "";
|
|
1415
|
+
const n = hull.length;
|
|
1416
|
+
// Use cardinal spline approach: cubic Bézier through hull vertices
|
|
1417
|
+
const tension = 0.3;
|
|
1418
|
+
const parts = [];
|
|
1419
|
+
// Start at midpoint between last and first vertex
|
|
1420
|
+
const mx = (hull[n - 1][0] + hull[0][0]) / 2;
|
|
1421
|
+
const my = (hull[n - 1][1] + hull[0][1]) / 2;
|
|
1422
|
+
parts.push(`M ${mx} ${my}`);
|
|
1423
|
+
for (let i = 0; i < n; i++) {
|
|
1424
|
+
const p0 = hull[(i - 1 + n) % n];
|
|
1425
|
+
const p1 = hull[i];
|
|
1426
|
+
const p2 = hull[(i + 1) % n];
|
|
1427
|
+
const p3 = hull[(i + 2) % n];
|
|
1428
|
+
// Control points using Catmull-Rom to Bézier conversion
|
|
1429
|
+
const cp1x = p1[0] + (p2[0] - p0[0]) * tension;
|
|
1430
|
+
const cp1y = p1[1] + (p2[1] - p0[1]) * tension;
|
|
1431
|
+
const cp2x = p2[0] - (p3[0] - p1[0]) * tension;
|
|
1432
|
+
const cp2y = p2[1] - (p3[1] - p1[1]) * tension;
|
|
1433
|
+
parts.push(`C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2[0]} ${p2[1]}`);
|
|
1434
|
+
}
|
|
1435
|
+
parts.push("Z");
|
|
1436
|
+
return parts.join(" ");
|
|
1437
|
+
}
|
|
1438
|
+
//# sourceMappingURL=renderer.js.map
|