spytial-core 1.4.20 → 1.4.22

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.
@@ -91338,9 +91338,6 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91338
91338
  const bbox2 = hasgetBBox(element2) ? element2.getBBox() : { x: 0, y: 0, width: 0, height: 0 };
91339
91339
  return !(bbox2.x > bbox1.x + bbox1.width || bbox2.x + bbox2.width < bbox1.x || bbox2.y > bbox1.y + bbox1.height || bbox2.y + bbox2.height < bbox1.y);
91340
91340
  }
91341
- function hasInnerBounds(target) {
91342
- return target && typeof target === "object" && "innerBounds" in target;
91343
- }
91344
91341
  var d32, cola, DEFAULT_SCALE_FACTOR, _WebColaCnDGraph, WebColaCnDGraph;
91345
91342
  var init_webcola_cnd_graph = __esm({
91346
91343
  "src/translators/webcola/webcola-cnd-graph.ts"() {
@@ -91350,7 +91347,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91350
91347
  cola = window.cola;
91351
91348
  DEFAULT_SCALE_FACTOR = 5;
91352
91349
  _WebColaCnDGraph = class _WebColaCnDGraph extends HTMLElement {
91353
- constructor() {
91350
+ constructor(isInputAllowed = false) {
91354
91351
  super();
91355
91352
  // Reduced from 5 for performance, but kept at 1 for alignment
91356
91353
  /**
@@ -91364,6 +91361,16 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91364
91361
  edgesBetweenNodes: /* @__PURE__ */ new Map(),
91365
91362
  alignmentEdges: /* @__PURE__ */ new Set()
91366
91363
  };
91364
+ /**
91365
+ * Tracks whether the user has manually interacted with zoom/pan.
91366
+ * When true, we don't auto-fit the viewport to preserve user's view.
91367
+ */
91368
+ this.userHasManuallyZoomed = false;
91369
+ /**
91370
+ * Tracks whether this is the initial render (first layout).
91371
+ * We always fit viewport on initial render.
91372
+ */
91373
+ this.isInitialRender = true;
91367
91374
  /**
91368
91375
  * Stores the starting coordinates when a node begins dragging so
91369
91376
  * drag end events can report both the previous and new positions.
@@ -91373,6 +91380,23 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91373
91380
  * Input mode state management for edge creation and modification
91374
91381
  */
91375
91382
  this.isInputModeActive = false;
91383
+ this.inputModeEnabled = true;
91384
+ this.inputModeListenersAttached = false;
91385
+ this.handleInputModeKeydown = (event) => {
91386
+ if ((event.metaKey || event.ctrlKey) && !this.isInputModeActive) {
91387
+ this.activateInputMode();
91388
+ }
91389
+ };
91390
+ this.handleInputModeKeyup = (event) => {
91391
+ if (!event.metaKey && !event.ctrlKey && this.isInputModeActive) {
91392
+ this.deactivateInputMode();
91393
+ }
91394
+ };
91395
+ this.handleInputModeBlur = () => {
91396
+ if (this.isInputModeActive) {
91397
+ this.deactivateInputMode();
91398
+ }
91399
+ };
91376
91400
  this.edgeCreationState = {
91377
91401
  isCreating: false,
91378
91402
  sourceNode: null,
@@ -91395,6 +91419,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91395
91419
  this.initializeDOM();
91396
91420
  this.initializeD3();
91397
91421
  this.lineFunction = d32.line().x((d) => d.x).y((d) => d.y).curve(d32.curveBasis);
91422
+ this.inputModeEnabled = isInputAllowed;
91398
91423
  this.initializeInputModeHandlers();
91399
91424
  }
91400
91425
  /**
@@ -91689,6 +91714,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91689
91714
  <div id="zoom-controls">
91690
91715
  <button id="zoom-in" title="Zoom In" aria-label="Zoom in">+</button>
91691
91716
  <button id="zoom-out" title="Zoom Out" aria-label="Zoom out">\u2212</button>
91717
+ <button id="zoom-fit" title="Fit to View" aria-label="Fit graph to view">\u2922</button>
91692
91718
  </div>
91693
91719
  </div>
91694
91720
  <div id="svg-container">
@@ -91724,7 +91750,11 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91724
91750
  this.svg = d32.select(this.shadowRoot.querySelector("#svg"));
91725
91751
  this.container = this.svg.select(".zoomable");
91726
91752
  if (d32.zoom) {
91727
- this.zoomBehavior = d32.zoom().scaleExtent([0.01, 20]).on("zoom", () => {
91753
+ this.zoomBehavior = d32.zoom().scaleExtent([0.01, 20]).on("start", () => {
91754
+ if (d32.event.sourceEvent) {
91755
+ this.userHasManuallyZoomed = true;
91756
+ }
91757
+ }).on("zoom", () => {
91728
91758
  this.container.attr("transform", d32.event.transform);
91729
91759
  this.updateZoomControlStates();
91730
91760
  this.updateSmallNodeClasses();
@@ -91741,37 +91771,51 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91741
91771
  initializeZoomControls() {
91742
91772
  const zoomInButton = this.shadowRoot.querySelector("#zoom-in");
91743
91773
  const zoomOutButton = this.shadowRoot.querySelector("#zoom-out");
91774
+ const zoomFitButton = this.shadowRoot.querySelector("#zoom-fit");
91744
91775
  if (zoomInButton) {
91745
91776
  zoomInButton.addEventListener("click", () => {
91777
+ this.userHasManuallyZoomed = true;
91746
91778
  this.zoomIn();
91747
91779
  });
91748
91780
  }
91749
91781
  if (zoomOutButton) {
91750
91782
  zoomOutButton.addEventListener("click", () => {
91783
+ this.userHasManuallyZoomed = true;
91751
91784
  this.zoomOut();
91752
91785
  });
91753
91786
  }
91787
+ if (zoomFitButton) {
91788
+ zoomFitButton.addEventListener("click", () => {
91789
+ this.resetViewToFitContent();
91790
+ });
91791
+ }
91754
91792
  this.updateZoomControlStates();
91755
91793
  }
91756
91794
  /**
91757
91795
  * Initialize keyboard event handlers for input mode activation
91758
91796
  */
91759
91797
  initializeInputModeHandlers() {
91760
- document.addEventListener("keydown", (event) => {
91761
- if ((event.metaKey || event.ctrlKey) && !this.isInputModeActive) {
91762
- this.activateInputMode();
91763
- }
91764
- });
91765
- document.addEventListener("keyup", (event) => {
91766
- if (!event.metaKey && !event.ctrlKey && this.isInputModeActive) {
91767
- this.deactivateInputMode();
91768
- }
91769
- });
91770
- window.addEventListener("blur", () => {
91771
- if (this.isInputModeActive) {
91772
- this.deactivateInputMode();
91773
- }
91774
- });
91798
+ if (this.inputModeEnabled) {
91799
+ this.attachInputModeListeners();
91800
+ }
91801
+ }
91802
+ attachInputModeListeners() {
91803
+ if (this.inputModeListenersAttached) {
91804
+ return;
91805
+ }
91806
+ document.addEventListener("keydown", this.handleInputModeKeydown);
91807
+ document.addEventListener("keyup", this.handleInputModeKeyup);
91808
+ window.addEventListener("blur", this.handleInputModeBlur);
91809
+ this.inputModeListenersAttached = true;
91810
+ }
91811
+ detachInputModeListeners() {
91812
+ if (!this.inputModeListenersAttached) {
91813
+ return;
91814
+ }
91815
+ document.removeEventListener("keydown", this.handleInputModeKeydown);
91816
+ document.removeEventListener("keyup", this.handleInputModeKeyup);
91817
+ window.removeEventListener("blur", this.handleInputModeBlur);
91818
+ this.inputModeListenersAttached = false;
91775
91819
  }
91776
91820
  /**
91777
91821
  * Activate input mode for edge creation and modification
@@ -91899,6 +91943,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91899
91943
  */
91900
91944
  setupNodeDragHandlers(nodeDrag) {
91901
91945
  nodeDrag.on("start.cnd", (d) => {
91946
+ this.userHasManuallyZoomed = true;
91902
91947
  const start = { x: d.x, y: d.y };
91903
91948
  this.dragStartPositions.set(d.id, start);
91904
91949
  this.dispatchEvent(
@@ -92130,6 +92175,10 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
92130
92175
  if (!isInstanceLayout(instanceLayout)) {
92131
92176
  throw new Error("Invalid instance layout provided. Expected an InstanceLayout instance.");
92132
92177
  }
92178
+ if (!options?.priorPositions) {
92179
+ this.isInitialRender = true;
92180
+ this.userHasManuallyZoomed = false;
92181
+ }
92133
92182
  if (this.svg && this.zoomBehavior && d32) {
92134
92183
  try {
92135
92184
  const identity = d32.zoomIdentity;
@@ -92968,35 +93017,17 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
92968
93017
  const targetGroup = potentialGroups.find((group) => group.keyNode === this.getNodeIndex(source));
92969
93018
  if (targetGroup) {
92970
93019
  target = targetGroup;
92971
- if (hasInnerBounds(target)) {
92972
- target.innerBounds = targetGroup.bounds?.inflate(-1 * (targetGroup.padding || 10));
92973
- }
92974
- } else {
92975
- console.log("Target group not found", potentialGroups, this.getNodeIndex(target));
92976
93020
  }
92977
93021
  } else if (addSourceToGroup) {
92978
93022
  const potentialGroups = this.getContainingGroups(this.currentLayout?.groups || [], source);
92979
93023
  const sourceGroup = potentialGroups.find((group) => group.keyNode === this.getNodeIndex(target));
92980
93024
  if (sourceGroup) {
92981
93025
  source = sourceGroup;
92982
- if (hasInnerBounds(source)) {
92983
- source.innerBounds = sourceGroup.bounds?.inflate(-1 * (sourceGroup.padding || 10));
92984
- }
92985
- } else {
92986
- console.log("Source group not found", potentialGroups, this.getNodeIndex(source));
92987
93026
  }
92988
- } else {
92989
- console.log("This is a group edge (on tick), but neither source nor target is a group.", d);
92990
93027
  }
92991
93028
  }
92992
- if (typeof cola.makeEdgeBetween === "function" && hasInnerBounds(source) && hasInnerBounds(target) && source.innerBounds && target.innerBounds) {
92993
- const route = cola.makeEdgeBetween(source.innerBounds, target.innerBounds, 5);
92994
- return this.lineFunction([route.sourceIntersection, route.arrowStart]);
92995
- }
92996
- return this.lineFunction([
92997
- { x: source.x || 0, y: source.y || 0 },
92998
- { x: target.x || 0, y: target.y || 0 }
92999
- ]);
93029
+ const route = this.getStableEdgePath(source, target);
93030
+ return this.lineFunction(route);
93000
93031
  }).attr("marker-end", (d) => {
93001
93032
  if (this.isAlignmentEdge(d)) return "none";
93002
93033
  return "url(#end-arrow)";
@@ -93632,6 +93663,83 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93632
93663
  const closestY = Math.max(y, Math.min(point.y, Y));
93633
93664
  return { x: closestX, y: closestY };
93634
93665
  }
93666
+ /**
93667
+ * Calculates a stable anchor point on a rectangle's perimeter for edge drawing.
93668
+ * This method produces consistent, jitter-free anchor points by using the
93669
+ * center of the rectangle edge that faces the target point.
93670
+ *
93671
+ * Unlike intersection-based approaches that can jump erratically as rectangles
93672
+ * move, this method selects one of four edge centers (top, bottom, left, right)
93673
+ * based on the dominant direction to the target, producing smooth transitions.
93674
+ *
93675
+ * @param bounds - Rectangle bounds with x, y, X, Y properties (or cx(), cy(), width(), height() methods)
93676
+ * @param targetPoint - The point the edge is connecting to
93677
+ * @returns Stable anchor point on the rectangle's perimeter
93678
+ */
93679
+ getStableEdgeAnchor(bounds, targetPoint) {
93680
+ if (!bounds) return targetPoint;
93681
+ let cx, cy, halfWidth, halfHeight;
93682
+ if (typeof bounds.cx === "function") {
93683
+ cx = bounds.cx();
93684
+ cy = bounds.cy();
93685
+ halfWidth = bounds.width() / 2;
93686
+ halfHeight = bounds.height() / 2;
93687
+ } else if (bounds.x !== void 0 && bounds.X !== void 0) {
93688
+ cx = (bounds.x + bounds.X) / 2;
93689
+ cy = (bounds.y + bounds.Y) / 2;
93690
+ halfWidth = (bounds.X - bounds.x) / 2;
93691
+ halfHeight = (bounds.Y - bounds.y) / 2;
93692
+ } else {
93693
+ return targetPoint;
93694
+ }
93695
+ const dx = targetPoint.x - cx;
93696
+ const dy = targetPoint.y - cy;
93697
+ const normalizedDx = Math.abs(dx) / halfWidth;
93698
+ const normalizedDy = Math.abs(dy) / halfHeight;
93699
+ if (normalizedDx > normalizedDy) {
93700
+ if (dx > 0) {
93701
+ return { x: cx + halfWidth, y: cy };
93702
+ } else {
93703
+ return { x: cx - halfWidth, y: cy };
93704
+ }
93705
+ } else {
93706
+ if (dy > 0) {
93707
+ return { x: cx, y: cy + halfHeight };
93708
+ } else {
93709
+ return { x: cx, y: cy - halfHeight };
93710
+ }
93711
+ }
93712
+ }
93713
+ /**
93714
+ * Calculates stable edge path points for drawing during tick/drag operations.
93715
+ * This method avoids jitter by using stable anchor points instead of
93716
+ * dynamic intersection calculations.
93717
+ *
93718
+ * @param source - Source node or group with bounds
93719
+ * @param target - Target node or group with bounds
93720
+ * @returns Array of two points for a simple line path
93721
+ */
93722
+ getStableEdgePath(source, target) {
93723
+ let targetCenter;
93724
+ if (target.bounds && typeof target.bounds.cx === "function") {
93725
+ targetCenter = { x: target.bounds.cx(), y: target.bounds.cy() };
93726
+ } else if (target.bounds) {
93727
+ targetCenter = { x: (target.bounds.x + target.bounds.X) / 2, y: (target.bounds.y + target.bounds.Y) / 2 };
93728
+ } else {
93729
+ targetCenter = { x: target.x || 0, y: target.y || 0 };
93730
+ }
93731
+ let sourceCenter;
93732
+ if (source.bounds && typeof source.bounds.cx === "function") {
93733
+ sourceCenter = { x: source.bounds.cx(), y: source.bounds.cy() };
93734
+ } else if (source.bounds) {
93735
+ sourceCenter = { x: (source.bounds.x + source.bounds.X) / 2, y: (source.bounds.y + source.bounds.Y) / 2 };
93736
+ } else {
93737
+ sourceCenter = { x: source.x || 0, y: source.y || 0 };
93738
+ }
93739
+ const sourceAnchor = source.bounds || source.innerBounds ? this.getStableEdgeAnchor(source.bounds || source.innerBounds, targetCenter) : sourceCenter;
93740
+ const targetAnchor = target.bounds || target.innerBounds ? this.getStableEdgeAnchor(target.bounds || target.innerBounds, sourceCenter) : targetCenter;
93741
+ return [sourceAnchor, targetAnchor];
93742
+ }
93635
93743
  /**
93636
93744
  * Adjusts a point to lie on the perimeter of a rectangle.
93637
93745
  *
@@ -93688,36 +93796,51 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93688
93796
  minimizeOverlap(currentLabel, overlappingLabels) {
93689
93797
  }
93690
93798
  /**
93691
- * Fits the viewport to show all content with padding.
93692
- * Uses manual calculation of bounds from all nodes and edges to ensure complete coverage.
93799
+ * Fits the viewport to show all content with appropriate zoom and pan.
93800
+ * Uses D3 zoom transform for smooth, consistent behavior.
93801
+ * Only performs fit if:
93802
+ * - This is the initial render, OR
93803
+ * - User has not manually zoomed/panned, OR
93804
+ * - Force parameter is true (e.g., from reset button)
93805
+ *
93806
+ * @param force - If true, fit regardless of user interaction state
93807
+ */
93808
+ fitViewportToContent(force = false) {
93809
+ const svgElement = this.svg?.node();
93810
+ if (!svgElement || !this.zoomBehavior) return;
93811
+ if (this.userHasManuallyZoomed && !this.isInitialRender && !force) {
93812
+ return;
93813
+ }
93814
+ const bounds = this.calculateContentBounds();
93815
+ if (!bounds) return;
93816
+ const containerWidth = svgElement.clientWidth || svgElement.parentElement?.clientWidth || 800;
93817
+ const containerHeight = svgElement.clientHeight || svgElement.parentElement?.clientHeight || 600;
93818
+ const padding = _WebColaCnDGraph.VIEWBOX_PADDING * 4;
93819
+ const scaleX = (containerWidth - padding * 2) / bounds.width;
93820
+ const scaleY = (containerHeight - padding * 2) / bounds.height;
93821
+ const scale = Math.min(scaleX, scaleY, 1);
93822
+ const [minScale, maxScale] = this.zoomBehavior.scaleExtent();
93823
+ const clampedScale = Math.max(minScale, Math.min(maxScale, scale));
93824
+ const contentCenterX = bounds.x + bounds.width / 2;
93825
+ const contentCenterY = bounds.y + bounds.height / 2;
93826
+ const translateX = containerWidth / 2 - contentCenterX * clampedScale;
93827
+ const translateY = containerHeight / 2 - contentCenterY * clampedScale;
93828
+ const transform = d32.zoomIdentity.translate(translateX, translateY).scale(clampedScale);
93829
+ if (this.isInitialRender) {
93830
+ this.svg.call(this.zoomBehavior.transform, transform);
93831
+ this.isInitialRender = false;
93832
+ } else {
93833
+ this.svg.transition().duration(300).ease(d32.easeCubicOut).call(this.zoomBehavior.transform, transform);
93834
+ }
93835
+ this.updateZoomControlStates();
93836
+ }
93837
+ /**
93838
+ * Resets the view to fit all content, clearing user zoom state.
93839
+ * Called when user clicks the reset/fit button.
93693
93840
  */
93694
- fitViewportToContent() {
93695
- const svgElement = this.svg.node();
93696
- if (!svgElement) return;
93697
- const manualBounds = this.calculateContentBounds();
93698
- const bbox = manualBounds || svgElement.getBBox();
93699
- const padding = _WebColaCnDGraph.VIEWBOX_PADDING;
93700
- let extraBottomPadding = 50;
93701
- if (this.currentLayout?.nodes) {
93702
- const bottomNode = this.currentLayout.nodes.reduce((bottom, node) => {
93703
- if (typeof node.x === "number" && typeof node.y === "number") {
93704
- const nodeBottom = node.y + (node.height || 0);
93705
- const currentBottom = bottom ? bottom.y + (bottom.height || 0) : -Infinity;
93706
- return nodeBottom > currentBottom ? node : bottom;
93707
- }
93708
- return bottom;
93709
- }, null);
93710
- if (bottomNode && bottomNode.height) {
93711
- extraBottomPadding = Math.min(50, bottomNode.height / 1.5);
93712
- }
93713
- }
93714
- const viewBox = [
93715
- bbox.x - padding,
93716
- bbox.y - padding,
93717
- bbox.width + 2 * padding,
93718
- bbox.height + 2 * padding + extraBottomPadding
93719
- ].join(" ");
93720
- this.svg.attr("viewBox", viewBox);
93841
+ resetViewToFitContent() {
93842
+ this.userHasManuallyZoomed = false;
93843
+ this.fitViewportToContent(true);
93721
93844
  }
93722
93845
  /**
93723
93846
  * Manually calculates the bounding box of all content to ensure accurate viewport fitting.
@@ -94273,7 +94396,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
94273
94396
  #zoom-controls {
94274
94397
  display: flex;
94275
94398
  flex-direction: row;
94276
- gap: 4px;
94399
+ gap: 8px;
94277
94400
  align-items: center;
94278
94401
  }
94279
94402
 
@@ -94677,6 +94800,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
94677
94800
  * - Temporary UI elements (modals, overlays)
94678
94801
  */
94679
94802
  dispose() {
94803
+ this.detachInputModeListeners();
94680
94804
  this.deactivateInputMode();
94681
94805
  if (this.svg) {
94682
94806
  this.svg.on(".zoom", null);
@@ -95491,7 +95615,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
95491
95615
  init_layoutspec();
95492
95616
  exports.StructuredInputGraph = class extends WebColaCnDGraph {
95493
95617
  constructor(dataInstance) {
95494
- super();
95618
+ super(true);
95495
95619
  this.evaluator = null;
95496
95620
  this.layoutInstance = null;
95497
95621
  this.cndSpecString = "";