spytial-core 1.4.20 → 1.4.21

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.
@@ -91364,6 +91364,16 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91364
91364
  edgesBetweenNodes: /* @__PURE__ */ new Map(),
91365
91365
  alignmentEdges: /* @__PURE__ */ new Set()
91366
91366
  };
91367
+ /**
91368
+ * Tracks whether the user has manually interacted with zoom/pan.
91369
+ * When true, we don't auto-fit the viewport to preserve user's view.
91370
+ */
91371
+ this.userHasManuallyZoomed = false;
91372
+ /**
91373
+ * Tracks whether this is the initial render (first layout).
91374
+ * We always fit viewport on initial render.
91375
+ */
91376
+ this.isInitialRender = true;
91367
91377
  /**
91368
91378
  * Stores the starting coordinates when a node begins dragging so
91369
91379
  * drag end events can report both the previous and new positions.
@@ -91689,6 +91699,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91689
91699
  <div id="zoom-controls">
91690
91700
  <button id="zoom-in" title="Zoom In" aria-label="Zoom in">+</button>
91691
91701
  <button id="zoom-out" title="Zoom Out" aria-label="Zoom out">\u2212</button>
91702
+ <button id="zoom-fit" title="Fit to View" aria-label="Fit graph to view">\u2922</button>
91692
91703
  </div>
91693
91704
  </div>
91694
91705
  <div id="svg-container">
@@ -91724,7 +91735,11 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91724
91735
  this.svg = d32.select(this.shadowRoot.querySelector("#svg"));
91725
91736
  this.container = this.svg.select(".zoomable");
91726
91737
  if (d32.zoom) {
91727
- this.zoomBehavior = d32.zoom().scaleExtent([0.01, 20]).on("zoom", () => {
91738
+ this.zoomBehavior = d32.zoom().scaleExtent([0.01, 20]).on("start", () => {
91739
+ if (d32.event.sourceEvent) {
91740
+ this.userHasManuallyZoomed = true;
91741
+ }
91742
+ }).on("zoom", () => {
91728
91743
  this.container.attr("transform", d32.event.transform);
91729
91744
  this.updateZoomControlStates();
91730
91745
  this.updateSmallNodeClasses();
@@ -91741,16 +91756,24 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91741
91756
  initializeZoomControls() {
91742
91757
  const zoomInButton = this.shadowRoot.querySelector("#zoom-in");
91743
91758
  const zoomOutButton = this.shadowRoot.querySelector("#zoom-out");
91759
+ const zoomFitButton = this.shadowRoot.querySelector("#zoom-fit");
91744
91760
  if (zoomInButton) {
91745
91761
  zoomInButton.addEventListener("click", () => {
91762
+ this.userHasManuallyZoomed = true;
91746
91763
  this.zoomIn();
91747
91764
  });
91748
91765
  }
91749
91766
  if (zoomOutButton) {
91750
91767
  zoomOutButton.addEventListener("click", () => {
91768
+ this.userHasManuallyZoomed = true;
91751
91769
  this.zoomOut();
91752
91770
  });
91753
91771
  }
91772
+ if (zoomFitButton) {
91773
+ zoomFitButton.addEventListener("click", () => {
91774
+ this.resetViewToFitContent();
91775
+ });
91776
+ }
91754
91777
  this.updateZoomControlStates();
91755
91778
  }
91756
91779
  /**
@@ -91899,6 +91922,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
91899
91922
  */
91900
91923
  setupNodeDragHandlers(nodeDrag) {
91901
91924
  nodeDrag.on("start.cnd", (d) => {
91925
+ this.userHasManuallyZoomed = true;
91902
91926
  const start = { x: d.x, y: d.y };
91903
91927
  this.dragStartPositions.set(d.id, start);
91904
91928
  this.dispatchEvent(
@@ -92130,6 +92154,10 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
92130
92154
  if (!isInstanceLayout(instanceLayout)) {
92131
92155
  throw new Error("Invalid instance layout provided. Expected an InstanceLayout instance.");
92132
92156
  }
92157
+ if (!options?.priorPositions) {
92158
+ this.isInitialRender = true;
92159
+ this.userHasManuallyZoomed = false;
92160
+ }
92133
92161
  if (this.svg && this.zoomBehavior && d32) {
92134
92162
  try {
92135
92163
  const identity = d32.zoomIdentity;
@@ -93688,36 +93716,51 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
93688
93716
  minimizeOverlap(currentLabel, overlappingLabels) {
93689
93717
  }
93690
93718
  /**
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.
93719
+ * Fits the viewport to show all content with appropriate zoom and pan.
93720
+ * Uses D3 zoom transform for smooth, consistent behavior.
93721
+ * Only performs fit if:
93722
+ * - This is the initial render, OR
93723
+ * - User has not manually zoomed/panned, OR
93724
+ * - Force parameter is true (e.g., from reset button)
93725
+ *
93726
+ * @param force - If true, fit regardless of user interaction state
93727
+ */
93728
+ fitViewportToContent(force = false) {
93729
+ const svgElement = this.svg?.node();
93730
+ if (!svgElement || !this.zoomBehavior) return;
93731
+ if (this.userHasManuallyZoomed && !this.isInitialRender && !force) {
93732
+ return;
93733
+ }
93734
+ const bounds = this.calculateContentBounds();
93735
+ if (!bounds) return;
93736
+ const containerWidth = svgElement.clientWidth || svgElement.parentElement?.clientWidth || 800;
93737
+ const containerHeight = svgElement.clientHeight || svgElement.parentElement?.clientHeight || 600;
93738
+ const padding = _WebColaCnDGraph.VIEWBOX_PADDING * 4;
93739
+ const scaleX = (containerWidth - padding * 2) / bounds.width;
93740
+ const scaleY = (containerHeight - padding * 2) / bounds.height;
93741
+ const scale = Math.min(scaleX, scaleY, 1);
93742
+ const [minScale, maxScale] = this.zoomBehavior.scaleExtent();
93743
+ const clampedScale = Math.max(minScale, Math.min(maxScale, scale));
93744
+ const contentCenterX = bounds.x + bounds.width / 2;
93745
+ const contentCenterY = bounds.y + bounds.height / 2;
93746
+ const translateX = containerWidth / 2 - contentCenterX * clampedScale;
93747
+ const translateY = containerHeight / 2 - contentCenterY * clampedScale;
93748
+ const transform = d32.zoomIdentity.translate(translateX, translateY).scale(clampedScale);
93749
+ if (this.isInitialRender) {
93750
+ this.svg.call(this.zoomBehavior.transform, transform);
93751
+ this.isInitialRender = false;
93752
+ } else {
93753
+ this.svg.transition().duration(300).ease(d32.easeCubicOut).call(this.zoomBehavior.transform, transform);
93754
+ }
93755
+ this.updateZoomControlStates();
93756
+ }
93757
+ /**
93758
+ * Resets the view to fit all content, clearing user zoom state.
93759
+ * Called when user clicks the reset/fit button.
93693
93760
  */
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);
93761
+ resetViewToFitContent() {
93762
+ this.userHasManuallyZoomed = false;
93763
+ this.fitViewportToContent(true);
93721
93764
  }
93722
93765
  /**
93723
93766
  * Manually calculates the bounding box of all content to ensure accurate viewport fitting.
@@ -94273,7 +94316,7 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
94273
94316
  #zoom-controls {
94274
94317
  display: flex;
94275
94318
  flex-direction: row;
94276
- gap: 4px;
94319
+ gap: 8px;
94277
94320
  align-items: center;
94278
94321
  }
94279
94322