@ankhorage/zora 22.1.2 → 22.1.4

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.d.ts +3 -3
  4. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js +3 -3
  5. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js.map +1 -1
  6. package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.d.ts +6 -5
  7. package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.d.ts.map +1 -1
  8. package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.js +107 -171
  9. package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.js.map +1 -1
  10. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts +4 -0
  11. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts.map +1 -1
  12. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js +8 -0
  13. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js.map +1 -1
  14. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js +5 -1
  15. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js.map +1 -1
  16. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.d.ts +9 -0
  17. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.d.ts.map +1 -0
  18. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.js +234 -0
  19. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.js.map +1 -0
  20. package/dist/types/graph-view-spacing.d.ts +15 -0
  21. package/dist/types/graph-view-spacing.d.ts.map +1 -0
  22. package/dist/types/graph-view-spacing.js +2 -0
  23. package/dist/types/graph-view-spacing.js.map +1 -0
  24. package/dist/types/graph-view.d.ts +4 -1
  25. package/dist/types/graph-view.d.ts.map +1 -1
  26. package/dist/types/graph-view.js.map +1 -1
  27. package/package.json +5 -5
  28. package/src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx +3 -3
  29. package/src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.test.ts +195 -69
  30. package/src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts +162 -241
  31. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.test.ts +2 -0
  32. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.ts +8 -0
  33. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.ts +5 -1
  34. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.ts +338 -0
  35. package/src/types/graph-view-spacing.ts +16 -0
  36. package/src/types/graph-view.ts +4 -1
  37. package/web-dist/components/graph-view/GraphView.d.ts +3 -3
  38. package/web-dist/components/graph-view/GraphView.js +237 -126
  39. package/web-dist/components/graph-view/GraphView.types-0.d.ts +4 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 22.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 24915fd: Update dependencies: `@ankhorage/contracts`, `@ankhorage/paradox`, `@ankhorage/utility`.
8
+
9
+ ## 22.1.3
10
+
11
+ ### Patch Changes
12
+
13
+ - a362a56: Make GraphView optimized fit stop at the effective fit-size target, keep labels and compound boundaries collision-safe, and use only a bounded rendered background-overlap fallback for genuinely dense graphs.
14
+
3
15
  ## 22.1.2
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/zora
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v22.1.2](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v22.1.4](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
7
7
 
8
8
  Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.
9
9
 
@@ -9,9 +9,9 @@ import type { GraphViewProps } from '../../../../../types/graph-view';
9
9
  * minReadableLabelSize Expand the manual zoom ceiling until plain labels reach this CSS-pixel size.
10
10
  * maxFitLabelSize Cap automatic fitting at this plain-label CSS-pixel size, leaving whitespace for tiny graphs.
11
11
  * onSpacingFactorChange Receive the spacing accepted by explicit fit({ optimizeSpacing: true }).
12
- * Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering
13
- * and uses bounded collision checks including labels and compound siblings; it can recover uniformly
14
- * cramped layouts but cannot repair non-uniform overlaps.
12
+ * Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering,
13
+ * stops once effective fit reaches maxFitLabelSize, keeps labels and unrelated compound boundaries
14
+ * collision-safe, and permits only a small rendered background-overlap fallback for dense graphs.
15
15
  */
16
16
  export declare function GraphView(props: GraphViewProps): React.JSX.Element;
17
17
  //# sourceMappingURL=GraphView.d.ts.map
@@ -10,9 +10,9 @@ import { GraphNodeOverlay } from './GraphNodeOverlay';
10
10
  * minReadableLabelSize Expand the manual zoom ceiling until plain labels reach this CSS-pixel size.
11
11
  * maxFitLabelSize Cap automatic fitting at this plain-label CSS-pixel size, leaving whitespace for tiny graphs.
12
12
  * onSpacingFactorChange Receive the spacing accepted by explicit fit({ optimizeSpacing: true }).
13
- * Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering
14
- * and uses bounded collision checks including labels and compound siblings; it can recover uniformly
15
- * cramped layouts but cannot repair non-uniform overlaps.
13
+ * Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering,
14
+ * stops once effective fit reaches maxFitLabelSize, keeps labels and unrelated compound boundaries
15
+ * collision-safe, and permits only a small rendered background-overlap fallback for dense graphs.
16
16
  */
17
17
  export function GraphView(props) {
18
18
  const { containerRef, renderedNodes, runtimeRef } = useGraphViewRuntime(props);
@@ -1 +1 @@
1
- {"version":3,"file":"GraphView.js","sourceRoot":"","sources":["../../../../../../src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { CytoscapeOptions } from 'cytoscape';\nimport React from 'react';\n\nimport type {\n GraphViewCallbacks,\n GraphViewNode,\n GraphViewProps,\n GraphViewRenderedNode,\n} from '../../../../../types/graph-view';\nimport { createGraphRuntime, type GraphRuntime } from './createGraphRuntime';\nimport { GraphNodeOverlay } from './GraphNodeOverlay';\n\ntype GraphContainer = NonNullable<CytoscapeOptions['container']>;\n\n/***\n * Render a browser graph while one runtime owns Cytoscape layout, viewport, and disposal.\n * zoomMode Use `fit-relative` to make controller zoom, events and min/max limits relative\n * to the visible-node fit (1 = fit). The default `absolute` keeps engine-space zoom units.\n * sizeNodesToLabels Opt in to renderer-measured plain labels, including bold and Unicode\n * text. Keep padding in styleRules. Compound and rich-node dimensions retain their existing owners.\n * minReadableLabelSize Expand the manual zoom ceiling until plain labels reach this CSS-pixel size.\n * maxFitLabelSize Cap automatic fitting at this plain-label CSS-pixel size, leaving whitespace for tiny graphs.\n * onSpacingFactorChange Receive the spacing accepted by explicit fit({ optimizeSpacing: true }).\n * Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering\n * and uses bounded collision checks including labels and compound siblings; it can recover uniformly\n * cramped layouts but cannot repair non-uniform overlaps.\n */\nexport function GraphView(props: GraphViewProps) {\n const { containerRef, renderedNodes, runtimeRef } = useGraphViewRuntime(props);\n\n return (\n <div\n aria-label={props.ariaLabel ?? 'Graph'}\n className={props.className}\n role=\"application\"\n style={{\n height: '100%',\n minHeight: 0,\n minWidth: 0,\n position: 'relative',\n width: '100%',\n ...props.style,\n }}\n >\n <div ref={containerRef} style={{ height: '100%', width: '100%' }} />\n {props.renderNode ? (\n <GraphNodeOverlay\n nodes={props.nodes}\n renderedNodes={renderedNodes}\n renderNode={props.renderNode}\n onNodeEvent={(id, type) => runtimeRef.current?.handleOverlayNodeEvent(id, type)}\n onNodeSize={(id, size) => runtimeRef.current?.setNodeSize(id, size)}\n />\n ) : null}\n </div>\n );\n}\n\n/*** Compose React synchronization around the single imperative graph runtime. */\nfunction useGraphViewRuntime(props: GraphViewProps) {\n const containerRef = React.useRef<GraphContainer | null>(null);\n const runtimeRef = React.useRef<GraphRuntime | null>(null);\n const callbacksRef = useGraphCallbacksRef(props);\n const [renderedNodes, setRenderedNodes] = React.useState<readonly GraphViewRenderedNode[]>([]);\n\n useRuntimeLifecycle(containerRef, runtimeRef, callbacksRef);\n useRuntimeGraphUpdate(runtimeRef, props);\n useRuntimeSelection(runtimeRef, props.selectedNodeIds);\n useRenderedNodeSubscription(runtimeRef, props.renderNode, setRenderedNodes);\n useKnownNodeSizes(runtimeRef, props.nodes, props.getNodeSize);\n\n return { containerRef, renderedNodes, runtimeRef };\n}\n\n/*** Keep the callback ref current without mutating refs during render. */\nfunction useGraphCallbacksRef(props: GraphViewProps) {\n const callbacksRef = React.useRef<GraphViewCallbacks>(readCallbacks(props));\n const {\n onEdgeEvent,\n onLayoutComplete,\n onNodeEvent,\n onReady,\n onViewportChange,\n onSpacingFactorChange,\n } = props;\n\n React.useEffect(() => {\n callbacksRef.current = {\n onEdgeEvent,\n onLayoutComplete,\n onNodeEvent,\n onReady,\n onViewportChange,\n onSpacingFactorChange,\n };\n }, [\n onEdgeEvent,\n onLayoutComplete,\n onNodeEvent,\n onReady,\n onViewportChange,\n onSpacingFactorChange,\n ]);\n\n return callbacksRef;\n}\n\n/*** Create and dispose the runtime only with the underlying browser container lifetime. */\nfunction useRuntimeLifecycle(\n containerRef: { current: GraphContainer | null },\n runtimeRef: { current: GraphRuntime | null },\n callbacksRef: { current: GraphViewCallbacks },\n) {\n React.useEffect(() => {\n const container = containerRef.current;\n if (container === null) return;\n const runtime = createGraphRuntime(container, callbacksRef);\n runtimeRef.current = runtime;\n return () => {\n runtime.destroy();\n runtimeRef.current = null;\n };\n }, [callbacksRef, containerRef, runtimeRef]);\n}\n\n/*** Synchronize graph data and layout policy without exposing Cytoscape to React. */\nfunction useRuntimeGraphUpdate(\n runtimeRef: { current: GraphRuntime | null },\n props: GraphViewProps,\n) {\n const {\n edges,\n fitPadding,\n layout,\n layoutOptions,\n maxZoom,\n minReadableLabelSize,\n maxFitLabelSize,\n minZoom,\n zoomMode,\n sizeNodesToLabels,\n nodes,\n renderNode,\n spacingFactor,\n styleRules,\n } = props;\n\n React.useEffect(() => {\n runtimeRef.current?.update({\n edges,\n fitPadding,\n layout,\n layoutOptions,\n maxZoom,\n minReadableLabelSize,\n maxFitLabelSize,\n minZoom,\n zoomMode,\n sizeNodesToLabels,\n nodes,\n richNodeRendering: renderNode !== undefined,\n spacingFactor,\n styleRules,\n });\n }, [\n edges,\n fitPadding,\n layout,\n layoutOptions,\n maxZoom,\n minReadableLabelSize,\n maxFitLabelSize,\n minZoom,\n zoomMode,\n sizeNodesToLabels,\n nodes,\n renderNode,\n runtimeRef,\n spacingFactor,\n styleRules,\n ]);\n}\n\n/*** Synchronize optional controlled selection independently from graph layout. */\nfunction useRuntimeSelection(\n runtimeRef: { current: GraphRuntime | null },\n selectedNodeIds: readonly string[] | undefined,\n) {\n React.useEffect(() => {\n runtimeRef.current?.setSelectedNodeIds(selectedNodeIds);\n }, [runtimeRef, selectedNodeIds]);\n}\n\n/*** Subscribe the optional rich-node overlay to rendered runtime positions. */\nfunction useRenderedNodeSubscription(\n runtimeRef: { current: GraphRuntime | null },\n renderNode: GraphViewProps['renderNode'],\n setRenderedNodes: (nodes: readonly GraphViewRenderedNode[]) => void,\n) {\n React.useEffect(() => {\n const runtime = runtimeRef.current;\n if (runtime === null || renderNode === undefined) {\n setRenderedNodes([]);\n return;\n }\n return runtime.subscribeRenderedNodes(setRenderedNodes);\n }, [renderNode, runtimeRef, setRenderedNodes]);\n}\n\n/*** Seed known rich-node dimensions without requiring DOM measurement. */\nfunction useKnownNodeSizes(\n runtimeRef: { current: GraphRuntime | null },\n nodes: readonly GraphViewNode[],\n getNodeSize: GraphViewProps['getNodeSize'],\n) {\n React.useEffect(() => {\n const runtime = runtimeRef.current;\n if (runtime === null || getNodeSize === undefined) return;\n for (const node of nodes) {\n const size = getNodeSize(node);\n if (size) runtime.setNodeSize(node.id, size);\n }\n }, [getNodeSize, nodes, runtimeRef]);\n}\n\n/*** Read callback props for the initial runtime callback-ref value. */\nfunction readCallbacks(props: GraphViewProps): GraphViewCallbacks {\n return {\n onEdgeEvent: props.onEdgeEvent,\n onLayoutComplete: props.onLayoutComplete,\n onNodeEvent: props.onNodeEvent,\n onReady: props.onReady,\n onViewportChange: props.onViewportChange,\n onSpacingFactorChange: props.onSpacingFactorChange,\n };\n}\n"]}
1
+ {"version":3,"file":"GraphView.js","sourceRoot":"","sources":["../../../../../../src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { CytoscapeOptions } from 'cytoscape';\nimport React from 'react';\n\nimport type {\n GraphViewCallbacks,\n GraphViewNode,\n GraphViewProps,\n GraphViewRenderedNode,\n} from '../../../../../types/graph-view';\nimport { createGraphRuntime, type GraphRuntime } from './createGraphRuntime';\nimport { GraphNodeOverlay } from './GraphNodeOverlay';\n\ntype GraphContainer = NonNullable<CytoscapeOptions['container']>;\n\n/***\n * Render a browser graph while one runtime owns Cytoscape layout, viewport, and disposal.\n * zoomMode Use `fit-relative` to make controller zoom, events and min/max limits relative\n * to the visible-node fit (1 = fit). The default `absolute` keeps engine-space zoom units.\n * sizeNodesToLabels Opt in to renderer-measured plain labels, including bold and Unicode\n * text. Keep padding in styleRules. Compound and rich-node dimensions retain their existing owners.\n * minReadableLabelSize Expand the manual zoom ceiling until plain labels reach this CSS-pixel size.\n * maxFitLabelSize Cap automatic fitting at this plain-label CSS-pixel size, leaving whitespace for tiny graphs.\n * onSpacingFactorChange Receive the spacing accepted by explicit fit({ optimizeSpacing: true }).\n * Ordinary zoom never adjusts spacing or relayouts. Explicit optimization preserves layout ordering,\n * stops once effective fit reaches maxFitLabelSize, keeps labels and unrelated compound boundaries\n * collision-safe, and permits only a small rendered background-overlap fallback for dense graphs.\n */\nexport function GraphView(props: GraphViewProps) {\n const { containerRef, renderedNodes, runtimeRef } = useGraphViewRuntime(props);\n\n return (\n <div\n aria-label={props.ariaLabel ?? 'Graph'}\n className={props.className}\n role=\"application\"\n style={{\n height: '100%',\n minHeight: 0,\n minWidth: 0,\n position: 'relative',\n width: '100%',\n ...props.style,\n }}\n >\n <div ref={containerRef} style={{ height: '100%', width: '100%' }} />\n {props.renderNode ? (\n <GraphNodeOverlay\n nodes={props.nodes}\n renderedNodes={renderedNodes}\n renderNode={props.renderNode}\n onNodeEvent={(id, type) => runtimeRef.current?.handleOverlayNodeEvent(id, type)}\n onNodeSize={(id, size) => runtimeRef.current?.setNodeSize(id, size)}\n />\n ) : null}\n </div>\n );\n}\n\n/*** Compose React synchronization around the single imperative graph runtime. */\nfunction useGraphViewRuntime(props: GraphViewProps) {\n const containerRef = React.useRef<GraphContainer | null>(null);\n const runtimeRef = React.useRef<GraphRuntime | null>(null);\n const callbacksRef = useGraphCallbacksRef(props);\n const [renderedNodes, setRenderedNodes] = React.useState<readonly GraphViewRenderedNode[]>([]);\n\n useRuntimeLifecycle(containerRef, runtimeRef, callbacksRef);\n useRuntimeGraphUpdate(runtimeRef, props);\n useRuntimeSelection(runtimeRef, props.selectedNodeIds);\n useRenderedNodeSubscription(runtimeRef, props.renderNode, setRenderedNodes);\n useKnownNodeSizes(runtimeRef, props.nodes, props.getNodeSize);\n\n return { containerRef, renderedNodes, runtimeRef };\n}\n\n/*** Keep the callback ref current without mutating refs during render. */\nfunction useGraphCallbacksRef(props: GraphViewProps) {\n const callbacksRef = React.useRef<GraphViewCallbacks>(readCallbacks(props));\n const {\n onEdgeEvent,\n onLayoutComplete,\n onNodeEvent,\n onReady,\n onViewportChange,\n onSpacingFactorChange,\n } = props;\n\n React.useEffect(() => {\n callbacksRef.current = {\n onEdgeEvent,\n onLayoutComplete,\n onNodeEvent,\n onReady,\n onViewportChange,\n onSpacingFactorChange,\n };\n }, [\n onEdgeEvent,\n onLayoutComplete,\n onNodeEvent,\n onReady,\n onViewportChange,\n onSpacingFactorChange,\n ]);\n\n return callbacksRef;\n}\n\n/*** Create and dispose the runtime only with the underlying browser container lifetime. */\nfunction useRuntimeLifecycle(\n containerRef: { current: GraphContainer | null },\n runtimeRef: { current: GraphRuntime | null },\n callbacksRef: { current: GraphViewCallbacks },\n) {\n React.useEffect(() => {\n const container = containerRef.current;\n if (container === null) return;\n const runtime = createGraphRuntime(container, callbacksRef);\n runtimeRef.current = runtime;\n return () => {\n runtime.destroy();\n runtimeRef.current = null;\n };\n }, [callbacksRef, containerRef, runtimeRef]);\n}\n\n/*** Synchronize graph data and layout policy without exposing Cytoscape to React. */\nfunction useRuntimeGraphUpdate(\n runtimeRef: { current: GraphRuntime | null },\n props: GraphViewProps,\n) {\n const {\n edges,\n fitPadding,\n layout,\n layoutOptions,\n maxZoom,\n minReadableLabelSize,\n maxFitLabelSize,\n minZoom,\n zoomMode,\n sizeNodesToLabels,\n nodes,\n renderNode,\n spacingFactor,\n styleRules,\n } = props;\n\n React.useEffect(() => {\n runtimeRef.current?.update({\n edges,\n fitPadding,\n layout,\n layoutOptions,\n maxZoom,\n minReadableLabelSize,\n maxFitLabelSize,\n minZoom,\n zoomMode,\n sizeNodesToLabels,\n nodes,\n richNodeRendering: renderNode !== undefined,\n spacingFactor,\n styleRules,\n });\n }, [\n edges,\n fitPadding,\n layout,\n layoutOptions,\n maxZoom,\n minReadableLabelSize,\n maxFitLabelSize,\n minZoom,\n zoomMode,\n sizeNodesToLabels,\n nodes,\n renderNode,\n runtimeRef,\n spacingFactor,\n styleRules,\n ]);\n}\n\n/*** Synchronize optional controlled selection independently from graph layout. */\nfunction useRuntimeSelection(\n runtimeRef: { current: GraphRuntime | null },\n selectedNodeIds: readonly string[] | undefined,\n) {\n React.useEffect(() => {\n runtimeRef.current?.setSelectedNodeIds(selectedNodeIds);\n }, [runtimeRef, selectedNodeIds]);\n}\n\n/*** Subscribe the optional rich-node overlay to rendered runtime positions. */\nfunction useRenderedNodeSubscription(\n runtimeRef: { current: GraphRuntime | null },\n renderNode: GraphViewProps['renderNode'],\n setRenderedNodes: (nodes: readonly GraphViewRenderedNode[]) => void,\n) {\n React.useEffect(() => {\n const runtime = runtimeRef.current;\n if (runtime === null || renderNode === undefined) {\n setRenderedNodes([]);\n return;\n }\n return runtime.subscribeRenderedNodes(setRenderedNodes);\n }, [renderNode, runtimeRef, setRenderedNodes]);\n}\n\n/*** Seed known rich-node dimensions without requiring DOM measurement. */\nfunction useKnownNodeSizes(\n runtimeRef: { current: GraphRuntime | null },\n nodes: readonly GraphViewNode[],\n getNodeSize: GraphViewProps['getNodeSize'],\n) {\n React.useEffect(() => {\n const runtime = runtimeRef.current;\n if (runtime === null || getNodeSize === undefined) return;\n for (const node of nodes) {\n const size = getNodeSize(node);\n if (size) runtime.setNodeSize(node.id, size);\n }\n }, [getNodeSize, nodes, runtimeRef]);\n}\n\n/*** Read callback props for the initial runtime callback-ref value. */\nfunction readCallbacks(props: GraphViewProps): GraphViewCallbacks {\n return {\n onEdgeEvent: props.onEdgeEvent,\n onLayoutComplete: props.onLayoutComplete,\n onNodeEvent: props.onNodeEvent,\n onReady: props.onReady,\n onViewportChange: props.onViewportChange,\n onSpacingFactorChange: props.onSpacingFactorChange,\n };\n}\n"]}
@@ -1,9 +1,10 @@
1
1
  import type { Core } from 'cytoscape';
2
+ import type { GraphSpacingFitOptions } from '../../../../../types/graph-view-spacing';
2
3
  /***
3
- * Optimize settled layout spacing around readable label bounds instead of full node clearance.
4
- * Compound containment stays intentional while sibling groups retain non-overlapping boundaries.
5
- * Cytoscape geometry is measured once, then bounded pure numeric trials reuse the
6
- * immutable snapshot. No layout reruns, zoom work, or intermediate frames are painted.
4
+ * Optimize settled spacing for useful rendered scale instead of minimum model-space distance.
5
+ * A graph already at its fit-size target remains unchanged. When compaction is useful, semantic
6
+ * label/compound collisions stay forbidden; only dense graphs may spend up to four rendered pixels
7
+ * of decorative leaf-background overlap. Locked/large views retain their settled geometry.
7
8
  */
8
- export declare function compactGraphSpacing(cy: Core, spacingFactor: number): number;
9
+ export declare function compactGraphSpacing(cy: Core, spacingFactor: number, options?: GraphSpacingFitOptions): number;
9
10
  //# sourceMappingURL=compactGraphSpacing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compactGraphSpacing.d.ts","sourceRoot":"","sources":["../../../../../../src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAgB,MAAM,WAAW,CAAC;AAQpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAiC3E"}
1
+ {"version":3,"file":"compactGraphSpacing.d.ts","sourceRoot":"","sources":["../../../../../../src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,KAAK,EAEV,sBAAsB,EACvB,MAAM,yCAAyC,CAAC;AASjD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,IAAI,EACR,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CA2BR"}
@@ -1,195 +1,131 @@
1
- import { findMinimumAcceptedNumber } from '@ankhorage/utility/algorithms';
2
- const LABEL_OVERLAP_RATIO = 0.12;
1
+ import { createGraphSpacingEvaluator } from './createGraphSpacingEvaluator';
3
2
  const MAX_EXPANSION_ATTEMPTS = 8;
4
3
  const MAX_NODE_COUNT = 2000;
5
- const MAX_PAIR_CHECKS = 200000;
4
+ const MAX_RENDERED_BACKGROUND_OVERLAP = 4;
6
5
  const SEARCH_ITERATIONS = 8;
6
+ const ZERO_TOLERANCE = 0.001;
7
7
  /***
8
- * Optimize settled layout spacing around readable label bounds instead of full node clearance.
9
- * Compound containment stays intentional while sibling groups retain non-overlapping boundaries.
10
- * Cytoscape geometry is measured once, then bounded pure numeric trials reuse the
11
- * immutable snapshot. No layout reruns, zoom work, or intermediate frames are painted.
8
+ * Optimize settled spacing for useful rendered scale instead of minimum model-space distance.
9
+ * A graph already at its fit-size target remains unchanged. When compaction is useful, semantic
10
+ * label/compound collisions stay forbidden; only dense graphs may spend up to four rendered pixels
11
+ * of decorative leaf-background overlap. Locked/large views retain their settled geometry.
12
12
  */
13
- export function compactGraphSpacing(cy, spacingFactor) {
14
- const leaves = cy.nodes(':childless');
15
- if (leaves.length < 2 || cy.nodes().length > MAX_NODE_COUNT || cy.nodes(':locked').length > 0)
13
+ export function compactGraphSpacing(cy, spacingFactor, options = {}) {
14
+ if (spacingFactor <= 0 ||
15
+ cy.nodes(':childless').length < 2 ||
16
+ cy.nodes().length > MAX_NODE_COUNT ||
17
+ cy.nodes(':locked').length > 0)
16
18
  return spacingFactor;
17
- const bounds = leaves.boundingBox();
18
- const center = { x: (bounds.x1 + bounds.x2) / 2, y: (bounds.y1 + bounds.y2) / 2 };
19
- const leafGeometry = leaves.map(readLeafGeometry);
20
- const groups = cy.nodes(':parent').map((node) => readGroupGeometry(node.id(), node.ancestors().map((parent) => parent.id()), node.boundingBox(), leafGeometry));
21
- const accepts = (factor) => !hasUnsafeCollisions(resolveCollisionBoxes(leafGeometry, groups, center, factor));
22
- const currentAccepted = accepts(1);
23
- const minimum = currentAccepted ? Math.min(1, 0.1 / spacingFactor) : 1;
24
- const maximum = currentAccepted ? 1 : findExpansionMaximum(accepts);
25
- if (maximum === undefined)
19
+ const evaluator = createGraphSpacingEvaluator(cy, options);
20
+ if (evaluator === null)
26
21
  return spacingFactor;
27
- const factor = findMinimumAcceptedNumber({
28
- minimum,
29
- maximum,
30
- iterations: SEARCH_ITERATIONS,
31
- accepts,
32
- });
33
- if (factor === undefined)
22
+ const current = evaluator.evaluate(1);
23
+ const maxFitZoom = options.maxFitZoom ?? Infinity;
24
+ const minReadableZoom = options.minReadableZoom ?? 0;
25
+ const factor = needsExpansion(current)
26
+ ? chooseExpansionFactor(evaluator.evaluate, minReadableZoom)
27
+ : chooseCompactionFactor(evaluator.evaluate, spacingFactor, maxFitZoom, minReadableZoom, current);
28
+ if (factor === 1)
34
29
  return spacingFactor;
35
- applyScale(cy, leafGeometry, center, factor);
30
+ evaluator.apply(factor);
36
31
  return spacingFactor * factor;
37
32
  }
38
- /*** Read one leaf's immutable position, hierarchy and renderer-measured label geometry. */
39
- function readLeafGeometry(node) {
40
- const nodeBox = node.boundingBox({
41
- includeLabels: true,
42
- includeOverlays: false,
43
- includeUnderlays: false,
44
- });
45
- const labelBox = node.boundingBox({
46
- includeNodes: false,
47
- includeEdges: false,
48
- includeLabels: true,
49
- includeOverlays: false,
50
- includeUnderlays: false,
51
- });
52
- return {
53
- id: node.id(),
54
- ancestors: new Set(node.ancestors().map((parent) => parent.id())),
55
- position: { ...node.position() },
56
- labelBox: isUsableBox(labelBox) ? labelBox : nodeBox,
57
- nodeBox,
58
- };
33
+ /*** Choose a smaller factor only while it improves rendered fit or reaches the configured cap. */
34
+ function chooseCompactionFactor(evaluate, spacingFactor, maxFitZoom, minReadableZoom, current) {
35
+ if (reachesFitTarget(current, maxFitZoom))
36
+ return 1;
37
+ const minimum = Math.min(1, 0.1 / spacingFactor);
38
+ if (current.renderedBackgroundOverlap > ZERO_TOLERANCE) {
39
+ if (reachesReadableTarget(current, minReadableZoom))
40
+ return 1;
41
+ return chooseSoftCompactionFactor(evaluate, minimum, 1, minReadableZoom);
42
+ }
43
+ const strictMinimum = findMinimumAcceptedFactor(minimum, 1, (factor) => isStrictCandidate(evaluate(factor)));
44
+ if (strictMinimum === undefined)
45
+ return 1;
46
+ const strict = evaluate(strictMinimum);
47
+ if (reachesFitTarget(strict, maxFitZoom)) {
48
+ return (findMaximumAcceptedFactor(strictMinimum, 1, (factor) => {
49
+ const candidate = evaluate(factor);
50
+ return isStrictCandidate(candidate) && reachesFitTarget(candidate, maxFitZoom);
51
+ }) ?? strictMinimum);
52
+ }
53
+ if (reachesReadableTarget(strict, minReadableZoom))
54
+ return strictMinimum;
55
+ return chooseSoftCompactionFactor(evaluate, minimum, strictMinimum, minReadableZoom);
59
56
  }
60
- /*** Capture one compound group's descendant geometry and constant renderer-owned outer insets. */
61
- function readGroupGeometry(id, ancestorIds, parentBox, leaves) {
62
- const descendants = leaves.filter((leaf) => leaf.ancestors.has(id));
63
- const descendantUnion = unionBounds(descendants.map((leaf) => leaf.nodeBox)) ?? parentBox;
64
- return {
65
- id,
66
- ancestors: new Set(ancestorIds),
67
- descendants,
68
- insets: {
69
- left: Math.max(0, descendantUnion.x1 - parentBox.x1),
70
- right: Math.max(0, parentBox.x2 - descendantUnion.x2),
71
- top: Math.max(0, descendantUnion.y1 - parentBox.y1),
72
- bottom: Math.max(0, parentBox.y2 - descendantUnion.y2),
73
- },
74
- };
57
+ /*** Spend background-overlap budget only to recover the configured readable-label threshold. */
58
+ function chooseSoftCompactionFactor(evaluate, minimum, maximum, minReadableZoom) {
59
+ const softMinimum = findMinimumAcceptedFactor(minimum, maximum, (factor) => isSoftCandidate(evaluate(factor)));
60
+ if (softMinimum === undefined)
61
+ return maximum;
62
+ const soft = evaluate(softMinimum);
63
+ if (!reachesReadableTarget(soft, minReadableZoom))
64
+ return softMinimum;
65
+ return (findMaximumAcceptedFactor(softMinimum, maximum, (factor) => {
66
+ const candidate = evaluate(factor);
67
+ return isSoftCandidate(candidate) && reachesReadableTarget(candidate, minReadableZoom);
68
+ }) ?? softMinimum);
75
69
  }
76
- /*** Resolve immutable label and compound boxes for one candidate uniform spacing factor. */
77
- function resolveCollisionBoxes(leaves, groups, center, factor) {
78
- const labels = leaves.map((leaf) => ({
79
- id: leaf.id,
80
- ancestors: leaf.ancestors,
81
- kind: 'label',
82
- box: shiftBounds(leaf.labelBox, leaf.position, center, factor),
83
- }));
84
- const compoundBoxes = groups.flatMap((group) => {
85
- const descendants = group.descendants.map((leaf) => shiftBounds(leaf.nodeBox, leaf.position, center, factor));
86
- const union = unionBounds(descendants);
87
- if (!union)
88
- return [];
89
- return [
90
- {
91
- id: group.id,
92
- ancestors: group.ancestors,
93
- kind: 'group',
94
- box: expandBounds(union, group.insets),
95
- },
96
- ];
97
- });
98
- return [...labels, ...compoundBoxes];
70
+ /*** Expand only enough to restore strict geometry unless that would sacrifice readable labels. */
71
+ function chooseExpansionFactor(evaluate, minReadableZoom) {
72
+ const strict = findExpansionFactor((factor) => isStrictCandidate(evaluate(factor)));
73
+ if (strict !== undefined && reachesReadableTarget(evaluate(strict), minReadableZoom))
74
+ return strict;
75
+ return findExpansionFactor((factor) => isSoftCandidate(evaluate(factor))) ?? strict ?? 1;
99
76
  }
100
- /*** Find the first bounded expansion factor that makes the pure geometry snapshot acceptable. */
101
- function findExpansionMaximum(accepts) {
102
- return Array.from({ length: MAX_EXPANSION_ATTEMPTS }, (_, index) => 2 ** (index + 1)).find(accepts);
77
+ /*** Return whether a candidate is collision-free, including decorative leaf backgrounds. */
78
+ function isStrictCandidate(candidate) {
79
+ return !candidate.hardCollision && candidate.renderedBackgroundOverlap <= ZERO_TOLERANCE;
103
80
  }
104
- /***
105
- * Sweep resolved boxes by x extent and stop conservatively if the pair-check budget is exhausted.
106
- * Local loop counters are intentionally mutable on this performance-critical boundary.
107
- */
108
- function hasUnsafeCollisions(boxes) {
109
- const sorted = [...boxes].sort((first, second) => first.box.x1 - second.box.x1);
110
- const budget = { remaining: MAX_PAIR_CHECKS };
111
- for (const [index, first] of sorted.entries()) {
112
- for (let next = index + 1; next < sorted.length; next += 1) {
113
- const second = sorted.at(next);
114
- if (!second)
115
- break;
116
- if (second.box.x1 >= first.box.x2)
117
- break;
118
- budget.remaining -= 1;
119
- if (budget.remaining < 0)
120
- return true;
121
- if (isIntentionalContainment(first, second))
122
- continue;
123
- if (isUnsafeOverlap(first, second))
124
- return true;
125
- }
126
- }
127
- return false;
81
+ /*** Allow only bounded rendered-pixel background overlap as a dense-graph fallback. */
82
+ function isSoftCandidate(candidate) {
83
+ return (!candidate.hardCollision &&
84
+ candidate.renderedBackgroundOverlap <= MAX_RENDERED_BACKGROUND_OVERLAP + ZERO_TOLERANCE);
128
85
  }
129
- /*** Ignore intentional compound containment while retaining sibling and unrelated separation. */
130
- function isIntentionalContainment(first, second) {
131
- return first.ancestors.has(second.id) || second.ancestors.has(first.id);
86
+ /*** Detect whether current geometry must expand before compaction can be considered. */
87
+ function needsExpansion(candidate) {
88
+ return candidate.hardCollision || !isSoftCandidate(candidate);
132
89
  }
133
- /*** Permit only a small bounded label-label overlap; every other intersection remains unsafe. */
134
- function isUnsafeOverlap(first, second) {
135
- const overlapWidth = Math.min(first.box.x2, second.box.x2) - Math.max(first.box.x1, second.box.x1);
136
- const overlapHeight = Math.min(first.box.y2, second.box.y2) - Math.max(first.box.y1, second.box.y1);
137
- if (overlapWidth <= 0 || overlapHeight <= 0)
90
+ /*** Treat a finite fit-size ceiling as the point after which tighter spacing cannot improve output. */
91
+ function reachesFitTarget(candidate, maxFitZoom) {
92
+ if (!Number.isFinite(maxFitZoom))
138
93
  return false;
139
- if (first.kind !== 'label' || second.kind !== 'label')
140
- return true;
141
- const widthRatio = overlapWidth / Math.min(first.box.w, second.box.w);
142
- const heightRatio = overlapHeight / Math.min(first.box.h, second.box.h);
143
- return widthRatio > LABEL_OVERLAP_RATIO && heightRatio > LABEL_OVERLAP_RATIO;
144
- }
145
- /*** Shift one fixed renderer box with its leaf center under uniform radial scaling. */
146
- function shiftBounds(box, position, center, factor) {
147
- const dx = (position.x - center.x) * (factor - 1);
148
- const dy = (position.y - center.y) * (factor - 1);
149
- return {
150
- x1: box.x1 + dx,
151
- y1: box.y1 + dy,
152
- x2: box.x2 + dx,
153
- y2: box.y2 + dy,
154
- w: box.w,
155
- h: box.h,
156
- };
94
+ return candidate.effectiveFitZoom >= maxFitZoom - ZERO_TOLERANCE;
157
95
  }
158
- /*** Expand a descendant union by the compound renderer's measured outer insets. */
159
- function expandBounds(box, insets) {
160
- const x1 = box.x1 - insets.left;
161
- const y1 = box.y1 - insets.top;
162
- const x2 = box.x2 + insets.right;
163
- const y2 = box.y2 + insets.bottom;
164
- return { x1, y1, x2, y2, w: x2 - x1, h: y2 - y1 };
96
+ /*** Return whether full-graph fit keeps semantic labels at the configured readable threshold. */
97
+ function reachesReadableTarget(candidate, minReadableZoom) {
98
+ return minReadableZoom <= 0 || candidate.effectiveFitZoom >= minReadableZoom - ZERO_TOLERANCE;
165
99
  }
166
- /*** Return the immutable union of measured boxes, or undefined for an empty collection. */
167
- function unionBounds(boxes) {
168
- if (boxes.length === 0)
100
+ /*** Binary-search the smallest factor accepted by a monotonic spacing constraint. */
101
+ function findMinimumAcceptedFactor(minimum, maximum, accepts) {
102
+ if (!accepts(maximum))
169
103
  return undefined;
170
- const x1 = Math.min(...boxes.map((box) => box.x1));
171
- const y1 = Math.min(...boxes.map((box) => box.y1));
172
- const x2 = Math.max(...boxes.map((box) => box.x2));
173
- const y2 = Math.max(...boxes.map((box) => box.y2));
174
- return { x1, y1, x2, y2, w: x2 - x1, h: y2 - y1 };
104
+ if (accepts(minimum))
105
+ return minimum;
106
+ return Array.from({ length: SEARCH_ITERATIONS }).reduce((range) => {
107
+ const factor = (range.low + range.high) / 2;
108
+ return accepts(factor) ? { low: range.low, high: factor } : { low: factor, high: range.high };
109
+ }, { low: minimum, high: maximum }).high;
175
110
  }
176
- /*** Return whether renderer measurement produced a non-empty label box. */
177
- function isUsableBox(box) {
178
- return box.w > 0 && box.h > 0;
111
+ /*** Binary-search the largest factor that still reaches a target under its collision policy. */
112
+ function findMaximumAcceptedFactor(minimum, maximum, accepts) {
113
+ if (!accepts(minimum))
114
+ return undefined;
115
+ if (accepts(maximum))
116
+ return maximum;
117
+ return Array.from({ length: SEARCH_ITERATIONS }).reduce((range) => {
118
+ const factor = (range.low + range.high) / 2;
119
+ return accepts(factor) ? { low: factor, high: range.high } : { low: range.low, high: factor };
120
+ }, { low: minimum, high: maximum }).low;
179
121
  }
180
- /*** Apply the accepted factor once, preserving node dimensions, hierarchy, ordering and edge data. */
181
- function applyScale(cy, leaves, center, factor) {
182
- const positions = new Map(leaves.map((leaf) => [leaf.id, leaf.position]));
183
- cy.batch(() => {
184
- cy.nodes()
185
- .not(':parent')
186
- .positions((node) => {
187
- const original = positions.get(node.id()) ?? node.position();
188
- return {
189
- x: center.x + (original.x - center.x) * factor,
190
- y: center.y + (original.y - center.y) * factor,
191
- };
192
- });
193
- });
122
+ /*** Find the first bounded expansion threshold and refine it without unbounded layout work. */
123
+ function findExpansionFactor(accepts) {
124
+ if (accepts(1))
125
+ return 1;
126
+ const maximum = Array.from({ length: MAX_EXPANSION_ATTEMPTS }, (_, index) => 2 ** (index + 1)).find(accepts);
127
+ if (maximum === undefined)
128
+ return undefined;
129
+ return findMinimumAcceptedFactor(maximum / 2, maximum, accepts);
194
130
  }
195
131
  //# sourceMappingURL=compactGraphSpacing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"compactGraphSpacing.js","sourceRoot":"","sources":["../../../../../../src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts"],"names":[],"mappings":"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","sourcesContent":["import { findMinimumAcceptedNumber } from '@ankhorage/utility/algorithms';\nimport type { Core, NodeSingular } from 'cytoscape';\n\nconst LABEL_OVERLAP_RATIO = 0.12;\nconst MAX_EXPANSION_ATTEMPTS = 8;\nconst MAX_NODE_COUNT = 2000;\nconst MAX_PAIR_CHECKS = 200000;\nconst SEARCH_ITERATIONS = 8;\n\n/***\n * Optimize settled layout spacing around readable label bounds instead of full node clearance.\n * Compound containment stays intentional while sibling groups retain non-overlapping boundaries.\n * Cytoscape geometry is measured once, then bounded pure numeric trials reuse the\n * immutable snapshot. No layout reruns, zoom work, or intermediate frames are painted.\n */\nexport function compactGraphSpacing(cy: Core, spacingFactor: number): number {\n const leaves = cy.nodes(':childless');\n if (leaves.length < 2 || cy.nodes().length > MAX_NODE_COUNT || cy.nodes(':locked').length > 0)\n return spacingFactor;\n\n const bounds = leaves.boundingBox();\n const center = { x: (bounds.x1 + bounds.x2) / 2, y: (bounds.y1 + bounds.y2) / 2 };\n const leafGeometry = leaves.map(readLeafGeometry);\n const groups = cy.nodes(':parent').map((node) =>\n readGroupGeometry(\n node.id(),\n node.ancestors().map((parent) => parent.id()),\n node.boundingBox(),\n leafGeometry,\n ),\n );\n const accepts = (factor: number) =>\n !hasUnsafeCollisions(resolveCollisionBoxes(leafGeometry, groups, center, factor));\n const currentAccepted = accepts(1);\n const minimum = currentAccepted ? Math.min(1, 0.1 / spacingFactor) : 1;\n const maximum = currentAccepted ? 1 : findExpansionMaximum(accepts);\n if (maximum === undefined) return spacingFactor;\n\n const factor = findMinimumAcceptedNumber({\n minimum,\n maximum,\n iterations: SEARCH_ITERATIONS,\n accepts,\n });\n if (factor === undefined) return spacingFactor;\n\n applyScale(cy, leafGeometry, center, factor);\n return spacingFactor * factor;\n}\n\ninterface Point {\n readonly x: number;\n readonly y: number;\n}\n\ninterface Bounds {\n readonly x1: number;\n readonly y1: number;\n readonly x2: number;\n readonly y2: number;\n readonly w: number;\n readonly h: number;\n}\n\ninterface Insets {\n readonly left: number;\n readonly right: number;\n readonly top: number;\n readonly bottom: number;\n}\n\ninterface LeafGeometry {\n readonly id: string;\n readonly ancestors: ReadonlySet<string>;\n readonly position: Point;\n readonly labelBox: Bounds;\n readonly nodeBox: Bounds;\n}\n\ninterface GroupGeometry {\n readonly id: string;\n readonly ancestors: ReadonlySet<string>;\n readonly descendants: readonly LeafGeometry[];\n readonly insets: Insets;\n}\n\ninterface CollisionBox {\n readonly id: string;\n readonly ancestors: ReadonlySet<string>;\n readonly kind: 'label' | 'group';\n readonly box: Bounds;\n}\n\n/*** Read one leaf's immutable position, hierarchy and renderer-measured label geometry. */\nfunction readLeafGeometry(node: NodeSingular): LeafGeometry {\n const nodeBox = node.boundingBox({\n includeLabels: true,\n includeOverlays: false,\n includeUnderlays: false,\n });\n const labelBox = node.boundingBox({\n includeNodes: false,\n includeEdges: false,\n includeLabels: true,\n includeOverlays: false,\n includeUnderlays: false,\n });\n return {\n id: node.id(),\n ancestors: new Set(node.ancestors().map((parent) => parent.id())),\n position: { ...node.position() },\n labelBox: isUsableBox(labelBox) ? labelBox : nodeBox,\n nodeBox,\n };\n}\n\n/*** Capture one compound group's descendant geometry and constant renderer-owned outer insets. */\nfunction readGroupGeometry(\n id: string,\n ancestorIds: readonly string[],\n parentBox: Bounds,\n leaves: readonly LeafGeometry[],\n): GroupGeometry {\n const descendants = leaves.filter((leaf) => leaf.ancestors.has(id));\n const descendantUnion = unionBounds(descendants.map((leaf) => leaf.nodeBox)) ?? parentBox;\n return {\n id,\n ancestors: new Set(ancestorIds),\n descendants,\n insets: {\n left: Math.max(0, descendantUnion.x1 - parentBox.x1),\n right: Math.max(0, parentBox.x2 - descendantUnion.x2),\n top: Math.max(0, descendantUnion.y1 - parentBox.y1),\n bottom: Math.max(0, parentBox.y2 - descendantUnion.y2),\n },\n };\n}\n\n/*** Resolve immutable label and compound boxes for one candidate uniform spacing factor. */\nfunction resolveCollisionBoxes(\n leaves: readonly LeafGeometry[],\n groups: readonly GroupGeometry[],\n center: Point,\n factor: number,\n): readonly CollisionBox[] {\n const labels = leaves.map((leaf) => ({\n id: leaf.id,\n ancestors: leaf.ancestors,\n kind: 'label' as const,\n box: shiftBounds(leaf.labelBox, leaf.position, center, factor),\n }));\n const compoundBoxes = groups.flatMap((group) => {\n const descendants = group.descendants.map((leaf) =>\n shiftBounds(leaf.nodeBox, leaf.position, center, factor),\n );\n const union = unionBounds(descendants);\n if (!union) return [];\n return [\n {\n id: group.id,\n ancestors: group.ancestors,\n kind: 'group' as const,\n box: expandBounds(union, group.insets),\n },\n ];\n });\n return [...labels, ...compoundBoxes];\n}\n\n/*** Find the first bounded expansion factor that makes the pure geometry snapshot acceptable. */\nfunction findExpansionMaximum(accepts: (factor: number) => boolean): number | undefined {\n return Array.from({ length: MAX_EXPANSION_ATTEMPTS }, (_, index) => 2 ** (index + 1)).find(\n accepts,\n );\n}\n\n/***\n * Sweep resolved boxes by x extent and stop conservatively if the pair-check budget is exhausted.\n * Local loop counters are intentionally mutable on this performance-critical boundary.\n */\nfunction hasUnsafeCollisions(boxes: readonly CollisionBox[]): boolean {\n const sorted = [...boxes].sort((first, second) => first.box.x1 - second.box.x1);\n const budget = { remaining: MAX_PAIR_CHECKS };\n for (const [index, first] of sorted.entries()) {\n for (let next = index + 1; next < sorted.length; next += 1) {\n const second = sorted.at(next);\n if (!second) break;\n if (second.box.x1 >= first.box.x2) break;\n budget.remaining -= 1;\n if (budget.remaining < 0) return true;\n if (isIntentionalContainment(first, second)) continue;\n if (isUnsafeOverlap(first, second)) return true;\n }\n }\n return false;\n}\n\n/*** Ignore intentional compound containment while retaining sibling and unrelated separation. */\nfunction isIntentionalContainment(first: CollisionBox, second: CollisionBox): boolean {\n return first.ancestors.has(second.id) || second.ancestors.has(first.id);\n}\n\n/*** Permit only a small bounded label-label overlap; every other intersection remains unsafe. */\nfunction isUnsafeOverlap(first: CollisionBox, second: CollisionBox): boolean {\n const overlapWidth =\n Math.min(first.box.x2, second.box.x2) - Math.max(first.box.x1, second.box.x1);\n const overlapHeight =\n Math.min(first.box.y2, second.box.y2) - Math.max(first.box.y1, second.box.y1);\n if (overlapWidth <= 0 || overlapHeight <= 0) return false;\n if (first.kind !== 'label' || second.kind !== 'label') return true;\n const widthRatio = overlapWidth / Math.min(first.box.w, second.box.w);\n const heightRatio = overlapHeight / Math.min(first.box.h, second.box.h);\n return widthRatio > LABEL_OVERLAP_RATIO && heightRatio > LABEL_OVERLAP_RATIO;\n}\n\n/*** Shift one fixed renderer box with its leaf center under uniform radial scaling. */\nfunction shiftBounds(box: Bounds, position: Point, center: Point, factor: number): Bounds {\n const dx = (position.x - center.x) * (factor - 1);\n const dy = (position.y - center.y) * (factor - 1);\n return {\n x1: box.x1 + dx,\n y1: box.y1 + dy,\n x2: box.x2 + dx,\n y2: box.y2 + dy,\n w: box.w,\n h: box.h,\n };\n}\n\n/*** Expand a descendant union by the compound renderer's measured outer insets. */\nfunction expandBounds(box: Bounds, insets: Insets): Bounds {\n const x1 = box.x1 - insets.left;\n const y1 = box.y1 - insets.top;\n const x2 = box.x2 + insets.right;\n const y2 = box.y2 + insets.bottom;\n return { x1, y1, x2, y2, w: x2 - x1, h: y2 - y1 };\n}\n\n/*** Return the immutable union of measured boxes, or undefined for an empty collection. */\nfunction unionBounds(boxes: readonly Bounds[]): Bounds | undefined {\n if (boxes.length === 0) return undefined;\n const x1 = Math.min(...boxes.map((box) => box.x1));\n const y1 = Math.min(...boxes.map((box) => box.y1));\n const x2 = Math.max(...boxes.map((box) => box.x2));\n const y2 = Math.max(...boxes.map((box) => box.y2));\n return { x1, y1, x2, y2, w: x2 - x1, h: y2 - y1 };\n}\n\n/*** Return whether renderer measurement produced a non-empty label box. */\nfunction isUsableBox(box: Bounds): boolean {\n return box.w > 0 && box.h > 0;\n}\n\n/*** Apply the accepted factor once, preserving node dimensions, hierarchy, ordering and edge data. */\nfunction applyScale(\n cy: Core,\n leaves: readonly LeafGeometry[],\n center: Point,\n factor: number,\n): void {\n const positions = new Map(leaves.map((leaf) => [leaf.id, leaf.position]));\n cy.batch(() => {\n cy.nodes()\n .not(':parent')\n .positions((node) => {\n const original = positions.get(node.id()) ?? node.position();\n return {\n x: center.x + (original.x - center.x) * factor,\n y: center.y + (original.y - center.y) * factor,\n };\n });\n });\n}\n"]}
1
+ {"version":3,"file":"compactGraphSpacing.js","sourceRoot":"","sources":["../../../../../../src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Core } from 'cytoscape';\n\nimport type {\n GraphSpacingCandidate,\n GraphSpacingFitOptions,\n} from '../../../../../types/graph-view-spacing';\nimport { createGraphSpacingEvaluator } from './createGraphSpacingEvaluator';\n\nconst MAX_EXPANSION_ATTEMPTS = 8;\nconst MAX_NODE_COUNT = 2000;\nconst MAX_RENDERED_BACKGROUND_OVERLAP = 4;\nconst SEARCH_ITERATIONS = 8;\nconst ZERO_TOLERANCE = 0.001;\n\n/***\n * Optimize settled spacing for useful rendered scale instead of minimum model-space distance.\n * A graph already at its fit-size target remains unchanged. When compaction is useful, semantic\n * label/compound collisions stay forbidden; only dense graphs may spend up to four rendered pixels\n * of decorative leaf-background overlap. Locked/large views retain their settled geometry.\n */\nexport function compactGraphSpacing(\n cy: Core,\n spacingFactor: number,\n options: GraphSpacingFitOptions = {},\n): number {\n if (\n spacingFactor <= 0 ||\n cy.nodes(':childless').length < 2 ||\n cy.nodes().length > MAX_NODE_COUNT ||\n cy.nodes(':locked').length > 0\n )\n return spacingFactor;\n\n const evaluator = createGraphSpacingEvaluator(cy, options);\n if (evaluator === null) return spacingFactor;\n const current = evaluator.evaluate(1);\n const maxFitZoom = options.maxFitZoom ?? Infinity;\n const minReadableZoom = options.minReadableZoom ?? 0;\n const factor = needsExpansion(current)\n ? chooseExpansionFactor(evaluator.evaluate, minReadableZoom)\n : chooseCompactionFactor(\n evaluator.evaluate,\n spacingFactor,\n maxFitZoom,\n minReadableZoom,\n current,\n );\n if (factor === 1) return spacingFactor;\n\n evaluator.apply(factor);\n return spacingFactor * factor;\n}\n\n/*** Choose a smaller factor only while it improves rendered fit or reaches the configured cap. */\nfunction chooseCompactionFactor(\n evaluate: (factor: number) => GraphSpacingCandidate,\n spacingFactor: number,\n maxFitZoom: number,\n minReadableZoom: number,\n current: GraphSpacingCandidate,\n): number {\n if (reachesFitTarget(current, maxFitZoom)) return 1;\n\n const minimum = Math.min(1, 0.1 / spacingFactor);\n if (current.renderedBackgroundOverlap > ZERO_TOLERANCE) {\n if (reachesReadableTarget(current, minReadableZoom)) return 1;\n return chooseSoftCompactionFactor(evaluate, minimum, 1, minReadableZoom);\n }\n\n const strictMinimum = findMinimumAcceptedFactor(minimum, 1, (factor) =>\n isStrictCandidate(evaluate(factor)),\n );\n if (strictMinimum === undefined) return 1;\n\n const strict = evaluate(strictMinimum);\n if (reachesFitTarget(strict, maxFitZoom)) {\n return (\n findMaximumAcceptedFactor(strictMinimum, 1, (factor) => {\n const candidate = evaluate(factor);\n return isStrictCandidate(candidate) && reachesFitTarget(candidate, maxFitZoom);\n }) ?? strictMinimum\n );\n }\n if (reachesReadableTarget(strict, minReadableZoom)) return strictMinimum;\n\n return chooseSoftCompactionFactor(evaluate, minimum, strictMinimum, minReadableZoom);\n}\n\n/*** Spend background-overlap budget only to recover the configured readable-label threshold. */\nfunction chooseSoftCompactionFactor(\n evaluate: (factor: number) => GraphSpacingCandidate,\n minimum: number,\n maximum: number,\n minReadableZoom: number,\n): number {\n const softMinimum = findMinimumAcceptedFactor(minimum, maximum, (factor) =>\n isSoftCandidate(evaluate(factor)),\n );\n if (softMinimum === undefined) return maximum;\n\n const soft = evaluate(softMinimum);\n if (!reachesReadableTarget(soft, minReadableZoom)) return softMinimum;\n return (\n findMaximumAcceptedFactor(softMinimum, maximum, (factor) => {\n const candidate = evaluate(factor);\n return isSoftCandidate(candidate) && reachesReadableTarget(candidate, minReadableZoom);\n }) ?? softMinimum\n );\n}\n\n/*** Expand only enough to restore strict geometry unless that would sacrifice readable labels. */\nfunction chooseExpansionFactor(\n evaluate: (factor: number) => GraphSpacingCandidate,\n minReadableZoom: number,\n): number {\n const strict = findExpansionFactor((factor) => isStrictCandidate(evaluate(factor)));\n if (strict !== undefined && reachesReadableTarget(evaluate(strict), minReadableZoom))\n return strict;\n return findExpansionFactor((factor) => isSoftCandidate(evaluate(factor))) ?? strict ?? 1;\n}\n\n/*** Return whether a candidate is collision-free, including decorative leaf backgrounds. */\nfunction isStrictCandidate(candidate: GraphSpacingCandidate): boolean {\n return !candidate.hardCollision && candidate.renderedBackgroundOverlap <= ZERO_TOLERANCE;\n}\n\n/*** Allow only bounded rendered-pixel background overlap as a dense-graph fallback. */\nfunction isSoftCandidate(candidate: GraphSpacingCandidate): boolean {\n return (\n !candidate.hardCollision &&\n candidate.renderedBackgroundOverlap <= MAX_RENDERED_BACKGROUND_OVERLAP + ZERO_TOLERANCE\n );\n}\n\n/*** Detect whether current geometry must expand before compaction can be considered. */\nfunction needsExpansion(candidate: GraphSpacingCandidate): boolean {\n return candidate.hardCollision || !isSoftCandidate(candidate);\n}\n\n/*** Treat a finite fit-size ceiling as the point after which tighter spacing cannot improve output. */\nfunction reachesFitTarget(candidate: GraphSpacingCandidate, maxFitZoom: number): boolean {\n if (!Number.isFinite(maxFitZoom)) return false;\n return candidate.effectiveFitZoom >= maxFitZoom - ZERO_TOLERANCE;\n}\n\n/*** Return whether full-graph fit keeps semantic labels at the configured readable threshold. */\nfunction reachesReadableTarget(candidate: GraphSpacingCandidate, minReadableZoom: number): boolean {\n return minReadableZoom <= 0 || candidate.effectiveFitZoom >= minReadableZoom - ZERO_TOLERANCE;\n}\n\n/*** Binary-search the smallest factor accepted by a monotonic spacing constraint. */\nfunction findMinimumAcceptedFactor(\n minimum: number,\n maximum: number,\n accepts: (factor: number) => boolean,\n): number | undefined {\n if (!accepts(maximum)) return undefined;\n if (accepts(minimum)) return minimum;\n return Array.from({ length: SEARCH_ITERATIONS }).reduce<{ low: number; high: number }>(\n (range) => {\n const factor = (range.low + range.high) / 2;\n return accepts(factor) ? { low: range.low, high: factor } : { low: factor, high: range.high };\n },\n { low: minimum, high: maximum },\n ).high;\n}\n\n/*** Binary-search the largest factor that still reaches a target under its collision policy. */\nfunction findMaximumAcceptedFactor(\n minimum: number,\n maximum: number,\n accepts: (factor: number) => boolean,\n): number | undefined {\n if (!accepts(minimum)) return undefined;\n if (accepts(maximum)) return maximum;\n return Array.from({ length: SEARCH_ITERATIONS }).reduce<{ low: number; high: number }>(\n (range) => {\n const factor = (range.low + range.high) / 2;\n return accepts(factor) ? { low: factor, high: range.high } : { low: range.low, high: factor };\n },\n { low: minimum, high: maximum },\n ).low;\n}\n\n/*** Find the first bounded expansion threshold and refine it without unbounded layout work. */\nfunction findExpansionFactor(accepts: (factor: number) => boolean): number | undefined {\n if (accepts(1)) return 1;\n const maximum = Array.from(\n { length: MAX_EXPANSION_ATTEMPTS },\n (_, index) => 2 ** (index + 1),\n ).find(accepts);\n if (maximum === undefined) return undefined;\n return findMinimumAcceptedFactor(maximum / 2, maximum, accepts);\n}\n"]}
@@ -7,6 +7,10 @@ export declare function createGraphController(cy: Core, fitPaddingRef: {
7
7
  }, optimizeFit?: (options: GraphViewFitOptions) => void): {
8
8
  controller: GraphViewController;
9
9
  configure(input: GraphRuntimeUpdate): void;
10
+ /*** Return the native automatic-fit ceiling used by spacing optimization. */
11
+ getFitZoomLimit(): number;
12
+ /*** Return the native label-readability threshold used before allowing background overlap. */
13
+ getReadableZoomTarget(): number;
10
14
  settle(fit: boolean): void;
11
15
  };
12
16
  //# sourceMappingURL=createGraphController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createGraphController.d.ts","sourceRoot":"","sources":["../../../../../../src/features/graph-view/adapters/inbound/web-artifact/createGraphController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EAEpB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAGhF,kGAAkG;AAClG,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,IAAI,EACR,aAAa,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,EAClC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI;;qBAiDjC,kBAAkB;gBASvB,OAAO;EAwBtB"}
1
+ {"version":3,"file":"createGraphController.d.ts","sourceRoot":"","sources":["../../../../../../src/features/graph-view/adapters/inbound/web-artifact/createGraphController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EAEpB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAGhF,kGAAkG;AAClG,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,IAAI,EACR,aAAa,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,EAClC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI;;qBAiDjC,kBAAkB;IASnC,6EAA6E;;IAI7E,8FAA8F;;gBAIlF,OAAO;EAwBtB"}
@@ -53,6 +53,14 @@ export function createGraphController(cy, fitPaddingRef, optimizeFit) {
53
53
  state.scale = 1;
54
54
  applyZoomLimits(cy, state);
55
55
  },
56
+ /*** Return the native automatic-fit ceiling used by spacing optimization. */
57
+ getFitZoomLimit() {
58
+ return state.relative ? state.fitZoom : Math.min(state.fitZoom, state.max);
59
+ },
60
+ /*** Return the native label-readability threshold used before allowing background overlap. */
61
+ getReadableZoomTarget() {
62
+ return state.readableZoom;
63
+ },
56
64
  settle(fit) {
57
65
  const logicalZoom = cy.zoom() / state.scale;
58
66
  const fonts = cy