@canvas-harness/core 0.1.4 → 0.1.6

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/dist/index.d.cts CHANGED
@@ -1454,9 +1454,33 @@ type InteractionState = {
1454
1454
  resizeLockAspect: boolean;
1455
1455
  /** Whether the user is holding Alt during a resize (resize from center). */
1456
1456
  resizeFromCenter: boolean;
1457
+ /**
1458
+ * Live in-progress geometry of the resized node — written every
1459
+ * pointermove, committed to the store once on pointer-up. While
1460
+ * present, `store.getNode(id)` still returns the original geometry;
1461
+ * the renderer overlays this draft via `mapDragPositions` for the
1462
+ * interactive layer paint. Mirrors how `dragDelta` works for drag.
1463
+ */
1464
+ resizeDraft: {
1465
+ x: number;
1466
+ y: number;
1467
+ w: number;
1468
+ h: number;
1469
+ angle: number;
1470
+ } | null;
1457
1471
  marqueeRect: WorldRect | null;
1458
1472
  /** Whether the marquee should add to (true, shift held) or replace selection. */
1459
1473
  marqueeAdditive: boolean;
1474
+ /**
1475
+ * Live in-progress cubic controls of an edge being mid-point-dragged.
1476
+ * Written every pointermove, committed to the store once on
1477
+ * pointer-up. Same draft+commit model as `resizeDraft` and
1478
+ * `dragDelta` — keeps mid-gesture mutations off the 'change' bus.
1479
+ */
1480
+ midpointDraft: {
1481
+ edgeId: EdgeId;
1482
+ control: [Vec2, Vec2];
1483
+ } | null;
1460
1484
  draftEdge: {
1461
1485
  source: EdgeEnd;
1462
1486
  target: EdgeEnd;
package/dist/index.d.ts CHANGED
@@ -1454,9 +1454,33 @@ type InteractionState = {
1454
1454
  resizeLockAspect: boolean;
1455
1455
  /** Whether the user is holding Alt during a resize (resize from center). */
1456
1456
  resizeFromCenter: boolean;
1457
+ /**
1458
+ * Live in-progress geometry of the resized node — written every
1459
+ * pointermove, committed to the store once on pointer-up. While
1460
+ * present, `store.getNode(id)` still returns the original geometry;
1461
+ * the renderer overlays this draft via `mapDragPositions` for the
1462
+ * interactive layer paint. Mirrors how `dragDelta` works for drag.
1463
+ */
1464
+ resizeDraft: {
1465
+ x: number;
1466
+ y: number;
1467
+ w: number;
1468
+ h: number;
1469
+ angle: number;
1470
+ } | null;
1457
1471
  marqueeRect: WorldRect | null;
1458
1472
  /** Whether the marquee should add to (true, shift held) or replace selection. */
1459
1473
  marqueeAdditive: boolean;
1474
+ /**
1475
+ * Live in-progress cubic controls of an edge being mid-point-dragged.
1476
+ * Written every pointermove, committed to the store once on
1477
+ * pointer-up. Same draft+commit model as `resizeDraft` and
1478
+ * `dragDelta` — keeps mid-gesture mutations off the 'change' bus.
1479
+ */
1480
+ midpointDraft: {
1481
+ edgeId: EdgeId;
1482
+ control: [Vec2, Vec2];
1483
+ } | null;
1460
1484
  draftEdge: {
1461
1485
  source: EdgeEnd;
1462
1486
  target: EdgeEnd;
package/dist/index.js CHANGED
@@ -1048,7 +1048,6 @@ var COMPOSITE = /* @__PURE__ */ new Set([
1048
1048
  var isCompositePrimitive = (type) => COMPOSITE.has(type);
1049
1049
  var isDrawablePrimitive = (type) => ATOMIC.has(type) || COMPOSITE.has(type);
1050
1050
  var PLAIN_RECT_CORNER_THRESHOLD_PX = 1.5;
1051
- var STROKE_VISIBILITY_THRESHOLD_PX = 0.5;
1052
1051
  var LAYERED_OFFSET = 12;
1053
1052
  var drawShape = (ctx, node, scale, theme, opts) => {
1054
1053
  if (!isDrawablePrimitive(node.type)) return;
@@ -1067,7 +1066,7 @@ var drawAtomic = (ctx, type, w, h, style, scale, theme, opts) => {
1067
1066
  const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
1068
1067
  const stroke = resolveColor(style, "strokeColor", DEFAULT_STYLE.strokeColor, theme);
1069
1068
  const fillVisible = !isFullyTransparent(fill);
1070
- const strokeVisible = strokeWidth > 0 && strokeWidth * scale >= STROKE_VISIBILITY_THRESHOLD_PX && !isFullyTransparent(stroke);
1069
+ const strokeVisible = strokeWidth > 0 && !isFullyTransparent(stroke);
1071
1070
  if (!fillVisible && !strokeVisible) return;
1072
1071
  const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
1073
1072
  switch (type) {
@@ -1104,7 +1103,7 @@ var drawAtomic = (ctx, type, w, h, style, scale, theme, opts) => {
1104
1103
  }
1105
1104
  if (strokeVisible && !opts?.skipStroke) {
1106
1105
  ctx.strokeStyle = stroke;
1107
- ctx.lineWidth = strokeWidth;
1106
+ ctx.lineWidth = Math.max(strokeWidth, 1 / scale);
1108
1107
  ctx.setLineDash(dashPatternFor(style?.strokeStyle, strokeWidth));
1109
1108
  ctx.stroke();
1110
1109
  }
@@ -1480,13 +1479,16 @@ var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
1480
1479
  const isDark = theme?.("mode") === "dark";
1481
1480
  const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
1482
1481
  const strokeColor = deriveRoughStrokeColor(rawStroke, fill, isDark);
1483
- const strokeWidth = resolveStrokeWidth(style, theme);
1484
- if (strokeWidth <= 0) return;
1482
+ const rawStrokeWidth = resolveStrokeWidth(style, theme);
1483
+ if (rawStrokeWidth <= 0) return;
1485
1484
  const roughness = style?.roughness ?? 0;
1486
1485
  if (roughness <= 0) return;
1486
+ const isNoBorderIntent = isFullyTransparent(rawStroke);
1487
+ const effectiveStrokeStyle = isNoBorderIntent ? "solid" : style?.strokeStyle ?? "solid";
1488
+ const strokeWidth = isNoBorderIntent ? DEFAULT_STYLE.strokeWidth : rawStrokeWidth;
1487
1489
  const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
1488
1490
  const radius = Math.max(0, Math.min(cornerRadius, w / 2, h / 2));
1489
- const dash = dashPatternFor(style?.strokeStyle, strokeWidth);
1491
+ const dash = dashPatternFor(effectiveStrokeStyle, strokeWidth);
1490
1492
  const detail = apparentDetail(Math.max(w, h), scale);
1491
1493
  const cacheKey = [
1492
1494
  type,
@@ -1495,7 +1497,7 @@ var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
1495
1497
  radius.toFixed(1),
1496
1498
  strokeColor,
1497
1499
  strokeWidth.toFixed(2),
1498
- style?.strokeStyle ?? "solid",
1500
+ effectiveStrokeStyle,
1499
1501
  roughness.toFixed(2),
1500
1502
  seed,
1501
1503
  detail.curveStepCount,
@@ -2646,7 +2648,7 @@ var DEFAULT_EDGE_STYLE = {
2646
2648
  sourceArrowhead: "none",
2647
2649
  targetArrowhead: "arrow-filled"
2648
2650
  };
2649
- var STROKE_VISIBILITY_THRESHOLD_PX2 = 0.5;
2651
+ var STROKE_VISIBILITY_THRESHOLD_PX = 0.5;
2650
2652
  var ARROWHEAD_VISIBILITY_THRESHOLD_PX = 2;
2651
2653
  var samplePaintStride = (scale) => {
2652
2654
  if (scale < 0.15) return 8;
@@ -2660,7 +2662,7 @@ var drawEdge = (ctx, edge, geom, sourceNode, targetNode, scale, theme, opts) =>
2660
2662
  if (samples.length < 2) return;
2661
2663
  const style = edge.style;
2662
2664
  const strokeWidth = typeof style?.strokeWidth === "number" ? style.strokeWidth : theme?.("strokeWidth") ?? DEFAULT_EDGE_STYLE.strokeWidth;
2663
- if (strokeWidth * scale < STROKE_VISIBILITY_THRESHOLD_PX2) return;
2665
+ if (strokeWidth * scale < STROKE_VISIBILITY_THRESHOLD_PX) return;
2664
2666
  const strokeColor = typeof style?.strokeColor === "string" ? style.strokeColor : theme?.("edge.strokeColor") ?? DEFAULT_EDGE_STYLE.strokeColor;
2665
2667
  const sourceArrowhead = style?.sourceArrowhead ?? DEFAULT_EDGE_STYLE.sourceArrowhead;
2666
2668
  const targetArrowhead = style?.targetArrowhead ?? DEFAULT_EDGE_STYLE.targetArrowhead;
@@ -3376,6 +3378,8 @@ var idleInteractionState = () => ({
3376
3378
  resizeHandle: null,
3377
3379
  resizeLockAspect: false,
3378
3380
  resizeFromCenter: false,
3381
+ resizeDraft: null,
3382
+ midpointDraft: null,
3379
3383
  marqueeRect: null,
3380
3384
  marqueeAdditive: false,
3381
3385
  draftEdge: null,
@@ -4884,7 +4888,7 @@ var drawWithNodeTransform = (ctx, node, fn) => {
4884
4888
  };
4885
4889
 
4886
4890
  // src/render/renderer.ts
4887
- var VIEWPORT_OVERSCAN_PX = 64;
4891
+ var SCENE_CACHE_MARGIN_PX = 256;
4888
4892
  var MIN_ON_SCREEN_SIZE_PX = 1.5;
4889
4893
  var MIN_READABLE_FONT_PX = 3;
4890
4894
  var createRenderer = (opts) => {
@@ -4901,6 +4905,30 @@ var createRenderer = (opts) => {
4901
4905
  let interactiveDirty = false;
4902
4906
  let overlaySet = /* @__PURE__ */ new Set();
4903
4907
  let lastDrawn = 0;
4908
+ let cacheSurface = null;
4909
+ let cacheCamX = 0;
4910
+ let cacheCamY = 0;
4911
+ let cacheCamZ = 1;
4912
+ let cacheStale = true;
4913
+ const ensureCacheSurface = () => {
4914
+ const dpr = staticSurface.dpr;
4915
+ const cssW = staticSurface.cssWidth + 2 * SCENE_CACHE_MARGIN_PX;
4916
+ const cssH = staticSurface.cssHeight + 2 * SCENE_CACHE_MARGIN_PX;
4917
+ if (!cacheSurface) {
4918
+ const canvas = document.createElement("canvas");
4919
+ const ctx = canvas.getContext("2d");
4920
+ if (!ctx) throw new Error("Canvas 2d context unavailable");
4921
+ cacheSurface = { canvas, ctx, cssWidth: 0, cssHeight: 0, dpr: 1 };
4922
+ }
4923
+ if (cacheSurface.cssWidth !== cssW || cacheSurface.cssHeight !== cssH || cacheSurface.dpr !== dpr) {
4924
+ cacheSurface.cssWidth = cssW;
4925
+ cacheSurface.cssHeight = cssH;
4926
+ cacheSurface.dpr = dpr;
4927
+ cacheSurface.canvas.width = Math.max(1, Math.round(cssW * dpr));
4928
+ cacheSurface.canvas.height = Math.max(1, Math.round(cssH * dpr));
4929
+ }
4930
+ return cacheSurface;
4931
+ };
4904
4932
  let sortedNodeIdsCache = null;
4905
4933
  let sortedEdgeIdsCache = null;
4906
4934
  const invalidateSortedCaches = () => {
@@ -4909,6 +4937,7 @@ var createRenderer = (opts) => {
4909
4937
  };
4910
4938
  const requestRepaint = () => {
4911
4939
  staticDirty = true;
4940
+ cacheStale = true;
4912
4941
  loop.requestFrame();
4913
4942
  };
4914
4943
  const assetCache = createAssetCache({ onReady: requestRepaint });
@@ -4923,16 +4952,14 @@ var createRenderer = (opts) => {
4923
4952
  interactiveDirty = false;
4924
4953
  }
4925
4954
  };
4926
- const paintStatic = () => {
4927
- const camera = store.getCamera();
4928
- clearSurface(staticSurface);
4929
- applyCameraTransform(staticSurface, camera);
4930
- const scale = camera.z * staticSurface.dpr;
4955
+ const paintSceneBody = (surface, camera, viewport, fullRender = true) => {
4956
+ const scale = camera.z * surface.dpr;
4931
4957
  const interaction = store.getInteractionState();
4932
4958
  const excludedNodes = interaction.mode === "dragging" || interaction.mode === "resizing" ? new Set(interaction.draggedIds) : null;
4933
- const excludedEdges = excludedNodes ? incidentEdgeIds(excludedNodes) : null;
4934
- const viewport = inflateRect(worldViewport(staticSurface, camera), VIEWPORT_OVERSCAN_PX);
4935
- paintBackground(staticSurface.ctx, { viewport, zoom: camera.z, background });
4959
+ const baseExcludedEdges = excludedNodes ? incidentEdgeIds(excludedNodes) : null;
4960
+ const midpointEdgeId = interaction.midpointDraft?.edgeId ?? null;
4961
+ const excludedEdges = midpointEdgeId !== null ? /* @__PURE__ */ new Set([...baseExcludedEdges ?? [], midpointEdgeId]) : baseExcludedEdges;
4962
+ paintBackground(surface.ctx, { viewport, zoom: camera.z, background });
4936
4963
  const visible = visibleNodes(camera, viewport);
4937
4964
  const isMoving2 = interaction.mode === "panning" || interaction.mode === "zooming" || interaction.mode === "dragging" || interaction.mode === "resizing" || interaction.mode === "rotating";
4938
4965
  const minOnScreen = MIN_ON_SCREEN_SIZE_PX;
@@ -4954,8 +4981,8 @@ var createRenderer = (opts) => {
4954
4981
  for (const node of visible) {
4955
4982
  if (node.type !== "frame") continue;
4956
4983
  if (excludedNodes?.has(node.id)) continue;
4957
- drawWithNodeTransform(staticSurface.ctx, node, () => {
4958
- paintFrameNode(staticSurface.ctx, node, scale, theme);
4984
+ drawWithNodeTransform(surface.ctx, node, () => {
4985
+ paintFrameNode(surface.ctx, node, scale, theme);
4959
4986
  });
4960
4987
  drawn++;
4961
4988
  }
@@ -4968,52 +4995,53 @@ var createRenderer = (opts) => {
4968
4995
  const useRough = roughEnabled && (node.style?.roughness ?? 0) > 0;
4969
4996
  const roughReady = useRough ? getRoughCanvasCtor() !== null : false;
4970
4997
  const composite = isCompositePrimitive(node.type);
4971
- drawWithNodeTransform(staticSurface.ctx, node, () => {
4998
+ drawWithNodeTransform(surface.ctx, node, () => {
4972
4999
  if (useRough && roughReady) {
4973
5000
  if (composite) {
4974
- drawCompositeRough(staticSurface.ctx, node, camera.z, theme);
5001
+ drawCompositeRough(surface.ctx, node, camera.z, theme);
4975
5002
  } else {
4976
- staticSurface.ctx.translate(ROUGH_FILL_MISREGISTER_X, ROUGH_FILL_MISREGISTER_Y);
4977
- drawShape(staticSurface.ctx, node, scale, theme, { skipStroke: true });
4978
- staticSurface.ctx.translate(-ROUGH_FILL_MISREGISTER_X, -ROUGH_FILL_MISREGISTER_Y);
4979
- drawRoughShape(staticSurface.ctx, node, camera.z, theme);
5003
+ surface.ctx.translate(ROUGH_FILL_MISREGISTER_X, ROUGH_FILL_MISREGISTER_Y);
5004
+ drawShape(surface.ctx, node, scale, theme, { skipStroke: true });
5005
+ surface.ctx.translate(-ROUGH_FILL_MISREGISTER_X, -ROUGH_FILL_MISREGISTER_Y);
5006
+ drawRoughShape(surface.ctx, node, camera.z, theme);
4980
5007
  }
4981
5008
  } else {
4982
- drawShape(staticSurface.ctx, node, scale, theme);
5009
+ drawShape(surface.ctx, node, scale, theme);
4983
5010
  if (useRough && !roughReady) {
4984
5011
  onRoughReady(() => {
4985
5012
  staticDirty = true;
5013
+ cacheStale = true;
4986
5014
  loop.requestFrame();
4987
5015
  });
4988
5016
  }
4989
5017
  }
4990
- if (!isEditingThis) paintNodeContent(staticSurface.ctx, node, renderEnv);
5018
+ if (!isEditingThis) paintNodeContent(surface.ctx, node, renderEnv);
4991
5019
  });
4992
5020
  drawn++;
4993
5021
  continue;
4994
5022
  }
4995
5023
  if (node.type === "image") {
4996
- drawWithNodeTransform(staticSurface.ctx, node, () => {
4997
- paintImageNode(staticSurface.ctx, node, assetCache, theme);
5024
+ drawWithNodeTransform(surface.ctx, node, () => {
5025
+ paintImageNode(surface.ctx, node, assetCache, theme);
4998
5026
  });
4999
5027
  drawn++;
5000
5028
  continue;
5001
5029
  }
5002
5030
  if (node.type === "icon") {
5003
- drawWithNodeTransform(staticSurface.ctx, node, () => {
5004
- paintIconNode(staticSurface.ctx, node, assetCache, scale, theme);
5031
+ drawWithNodeTransform(surface.ctx, node, () => {
5032
+ paintIconNode(surface.ctx, node, assetCache, scale, theme);
5005
5033
  });
5006
5034
  drawn++;
5007
5035
  continue;
5008
5036
  }
5009
5037
  if (node.type === "text") {
5010
- drawWithNodeTransform(staticSurface.ctx, node, () => {
5038
+ drawWithNodeTransform(surface.ctx, node, () => {
5011
5039
  if (isEditingThis) return;
5012
5040
  const hasContent = node.content && node.content.trim().length > 0;
5013
5041
  if (hasContent) {
5014
- paintNodeContent(staticSurface.ctx, node, renderEnv);
5042
+ paintNodeContent(surface.ctx, node, renderEnv);
5015
5043
  } else {
5016
- paintEmptyTextPlaceholder(staticSurface.ctx, node, camera.z);
5044
+ paintEmptyTextPlaceholder(surface.ctx, node, camera.z);
5017
5045
  }
5018
5046
  });
5019
5047
  drawn++;
@@ -5025,7 +5053,7 @@ var createRenderer = (opts) => {
5025
5053
  if (camera.z < def.lod.minZoomForPlaceholder) continue;
5026
5054
  const preferCanvas = camera.z < def.lod.minZoomForReact || isMoving2;
5027
5055
  if (preferCanvas) {
5028
- if (paintCustomCanvasFallback(staticSurface.ctx, node, def, scale, renderEnv)) {
5056
+ if (paintCustomCanvasFallback(surface.ctx, node, def, scale, renderEnv)) {
5029
5057
  drawn++;
5030
5058
  }
5031
5059
  continue;
@@ -5035,10 +5063,10 @@ var createRenderer = (opts) => {
5035
5063
  continue;
5036
5064
  }
5037
5065
  if (def.renderCanvas) {
5038
- drawWithNodeTransform(staticSurface.ctx, node, () => {
5039
- staticSurface.ctx.save();
5040
- def.renderCanvas(staticSurface.ctx, node, renderEnv);
5041
- staticSurface.ctx.restore();
5066
+ drawWithNodeTransform(surface.ctx, node, () => {
5067
+ surface.ctx.save();
5068
+ def.renderCanvas(surface.ctx, node, renderEnv);
5069
+ surface.ctx.restore();
5042
5070
  });
5043
5071
  drawn++;
5044
5072
  }
@@ -5047,15 +5075,120 @@ var createRenderer = (opts) => {
5047
5075
  const edgeRoughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visEdges.length <= ROUGH_MAX_NODES;
5048
5076
  for (const edge of visEdges) {
5049
5077
  if (excludedEdges?.has(edge.id)) continue;
5050
- paintOneEdge(staticSurface.ctx, edge, scale, edgeRoughEnabled, camera.z, isMoving2);
5078
+ paintOneEdge(surface.ctx, edge, scale, edgeRoughEnabled, camera.z, isMoving2);
5051
5079
  drawn++;
5052
5080
  }
5081
+ if (!fullRender) return;
5053
5082
  lastDrawn = drawn;
5054
5083
  if (!setsEqual(nextOverlaySet, overlaySet)) {
5055
5084
  overlaySet = nextOverlaySet;
5056
5085
  onOverlayChange?.([...overlaySet]);
5057
5086
  }
5058
5087
  };
5088
+ const applyCacheTransform = (cache5, centerX, centerY, z) => {
5089
+ const s = z * cache5.dpr;
5090
+ const m = SCENE_CACHE_MARGIN_PX * cache5.dpr;
5091
+ cache5.ctx.setTransform(s, 0, 0, s, -centerX * s + m, -centerY * s + m);
5092
+ };
5093
+ const renderFullCache = (camera) => {
5094
+ const cache5 = ensureCacheSurface();
5095
+ clearSurface(cache5);
5096
+ applyCacheTransform(cache5, camera.x, camera.y, camera.z);
5097
+ const marginWorld = SCENE_CACHE_MARGIN_PX / camera.z;
5098
+ const viewport = inflateRect(worldViewport(staticSurface, camera), marginWorld);
5099
+ paintSceneBody(cache5, camera, viewport);
5100
+ cacheCamX = camera.x;
5101
+ cacheCamY = camera.y;
5102
+ cacheCamZ = camera.z;
5103
+ cacheStale = false;
5104
+ };
5105
+ const canExtend = (camera) => {
5106
+ if (!cacheSurface) return false;
5107
+ const s = camera.z * staticSurface.dpr;
5108
+ const dx = Math.abs((cacheCamX - camera.x) * s);
5109
+ const dy = Math.abs((cacheCamY - camera.y) * s);
5110
+ return dx < cacheSurface.canvas.width && dy < cacheSurface.canvas.height;
5111
+ };
5112
+ const renderCacheStrip = (cache5, centerX, centerY, z, px, py, pw, ph) => {
5113
+ const ctx = cache5.ctx;
5114
+ const s = z * cache5.dpr;
5115
+ const m = SCENE_CACHE_MARGIN_PX * cache5.dpr;
5116
+ ctx.save();
5117
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
5118
+ ctx.beginPath();
5119
+ ctx.rect(px, py, pw, ph);
5120
+ ctx.clip();
5121
+ ctx.clearRect(px, py, pw, ph);
5122
+ applyCacheTransform(cache5, centerX, centerY, z);
5123
+ const viewport = {
5124
+ x: (px - m) / s + centerX,
5125
+ y: (py - m) / s + centerY,
5126
+ w: pw / s,
5127
+ h: ph / s
5128
+ };
5129
+ paintSceneBody(cache5, { z }, viewport, false);
5130
+ ctx.restore();
5131
+ };
5132
+ const extendCache = (camera) => {
5133
+ const cache5 = ensureCacheSurface();
5134
+ const s = camera.z * cache5.dpr;
5135
+ const cacheW = cache5.canvas.width;
5136
+ const cacheH = cache5.canvas.height;
5137
+ const dx = Math.round((cacheCamX - camera.x) * s);
5138
+ const dy = Math.round((cacheCamY - camera.y) * s);
5139
+ const newCamX = cacheCamX - dx / s;
5140
+ const newCamY = cacheCamY - dy / s;
5141
+ cache5.ctx.setTransform(1, 0, 0, 1, 0, 0);
5142
+ cache5.ctx.drawImage(cache5.canvas, 0, 0, cacheW, cacheH, dx, dy, cacheW, cacheH);
5143
+ cacheCamX = newCamX;
5144
+ cacheCamY = newCamY;
5145
+ cacheCamZ = camera.z;
5146
+ const hw = Math.abs(dx);
5147
+ const vh = Math.abs(dy);
5148
+ const hx = dx > 0 ? 0 : cacheW - hw;
5149
+ const vy = dy > 0 ? 0 : cacheH - vh;
5150
+ const vx = dx > 0 ? hw : 0;
5151
+ const vw = cacheW - hw;
5152
+ if (hw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, hx, 0, hw, cacheH);
5153
+ if (vh > 0 && vw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, vx, vy, vw, vh);
5154
+ };
5155
+ const cacheSourceOffset = (camera) => {
5156
+ const dpr = staticSurface.dpr;
5157
+ return {
5158
+ x: Math.round(((camera.x - cacheCamX) * cacheCamZ + SCENE_CACHE_MARGIN_PX) * dpr),
5159
+ y: Math.round(((camera.y - cacheCamY) * cacheCamZ + SCENE_CACHE_MARGIN_PX) * dpr)
5160
+ };
5161
+ };
5162
+ const viewportFitsInCache = (camera) => {
5163
+ if (!cacheSurface) return false;
5164
+ const { x, y } = cacheSourceOffset(camera);
5165
+ return x >= 0 && y >= 0 && x + staticSurface.canvas.width <= cacheSurface.canvas.width && y + staticSurface.canvas.height <= cacheSurface.canvas.height;
5166
+ };
5167
+ const presentStatic = (camera) => {
5168
+ const cache5 = ensureCacheSurface();
5169
+ const w = staticSurface.canvas.width;
5170
+ const h = staticSurface.canvas.height;
5171
+ const { x: srcX, y: srcY } = cacheSourceOffset(camera);
5172
+ staticSurface.ctx.setTransform(1, 0, 0, 1, 0, 0);
5173
+ staticSurface.ctx.clearRect(0, 0, w, h);
5174
+ staticSurface.ctx.drawImage(cache5.canvas, srcX, srcY, w, h, 0, 0, w, h);
5175
+ };
5176
+ const paintStatic = () => {
5177
+ const camera = store.getCamera();
5178
+ if (!cacheStale && camera.z === cacheCamZ) {
5179
+ if (viewportFitsInCache(camera)) {
5180
+ presentStatic(camera);
5181
+ return;
5182
+ }
5183
+ if (canExtend(camera)) {
5184
+ extendCache(camera);
5185
+ presentStatic(camera);
5186
+ return;
5187
+ }
5188
+ }
5189
+ renderFullCache(camera);
5190
+ presentStatic(camera);
5191
+ };
5059
5192
  const paintCustomCanvasFallback = (ctx, node, def, drawScale, env) => {
5060
5193
  if (def.getSnapshot) {
5061
5194
  const snap = def.getSnapshot(node, {
@@ -5237,6 +5370,23 @@ var createRenderer = (opts) => {
5237
5370
  }
5238
5371
  }
5239
5372
  }
5373
+ if (interaction.midpointDraft) {
5374
+ const { edgeId, control } = interaction.midpointDraft;
5375
+ const edge = store.getEdge(edgeId);
5376
+ if (edge) {
5377
+ const draftEdge = { ...edge, control };
5378
+ const geom = computeEdgeGeometry(draftEdge, (id) => store.getNode(id));
5379
+ if (geom) {
5380
+ const sourceNode = geom.sourceNodeId ? store.getNode(geom.sourceNodeId) ?? null : null;
5381
+ const targetNode = geom.targetNodeId ? store.getNode(geom.targetNodeId) ?? null : null;
5382
+ drawEdge(ctx, draftEdge, geom, sourceNode, targetNode, scale, theme, {
5383
+ zoom: camera.z,
5384
+ dpr: interactiveSurface.dpr,
5385
+ isMoving: true
5386
+ });
5387
+ }
5388
+ }
5389
+ }
5240
5390
  const selection = store.getSelection();
5241
5391
  const selectedNodeIds = [];
5242
5392
  const selectedEdgeIds = [];
@@ -5309,7 +5459,12 @@ var createRenderer = (opts) => {
5309
5459
  y: orig.y + interaction.dragDelta.y
5310
5460
  });
5311
5461
  } else {
5312
- m.set(orig.id, live);
5462
+ const d = interaction.resizeDraft;
5463
+ if (d) {
5464
+ m.set(orig.id, { ...live, x: d.x, y: d.y, w: d.w, h: d.h, angle: d.angle });
5465
+ } else {
5466
+ m.set(orig.id, live);
5467
+ }
5313
5468
  }
5314
5469
  }
5315
5470
  return m;
@@ -5340,6 +5495,7 @@ var createRenderer = (opts) => {
5340
5495
  const onStoreChange = () => {
5341
5496
  invalidateSortedCaches();
5342
5497
  staticDirty = true;
5498
+ cacheStale = true;
5343
5499
  interactiveDirty = true;
5344
5500
  loop.requestFrame();
5345
5501
  };
@@ -5356,6 +5512,7 @@ var createRenderer = (opts) => {
5356
5512
  interactiveDirty = true;
5357
5513
  if (state.mode === "dragging" || state.mode === "resizing" || state.mode === "rotating" || state.mode === "panning" || state.mode === "zooming" || state.mode === "idle") {
5358
5514
  staticDirty = true;
5515
+ cacheStale = true;
5359
5516
  }
5360
5517
  loop.requestFrame();
5361
5518
  };
@@ -5365,16 +5522,19 @@ var createRenderer = (opts) => {
5365
5522
  const unsubInteraction = store.subscribe("interaction", onInteractionChange);
5366
5523
  const unsubFontEpoch = subscribeFontEpoch(() => {
5367
5524
  staticDirty = true;
5525
+ cacheStale = true;
5368
5526
  loop.requestFrame();
5369
5527
  });
5370
5528
  const unsubMathEpoch = subscribeMathEpoch(() => {
5371
5529
  staticDirty = true;
5530
+ cacheStale = true;
5372
5531
  loop.requestFrame();
5373
5532
  });
5374
5533
  return {
5375
5534
  start() {
5376
5535
  loop.start();
5377
5536
  staticDirty = true;
5537
+ cacheStale = true;
5378
5538
  interactiveDirty = isInteractive(store.getInteractionState());
5379
5539
  loop.requestFrame();
5380
5540
  },
@@ -5383,6 +5543,7 @@ var createRenderer = (opts) => {
5383
5543
  },
5384
5544
  invalidate() {
5385
5545
  staticDirty = true;
5546
+ cacheStale = true;
5386
5547
  interactiveDirty = true;
5387
5548
  loop.requestFrame();
5388
5549
  },
@@ -5391,6 +5552,7 @@ var createRenderer = (opts) => {
5391
5552
  const b = sizeSurface(interactiveSurface, cssW, cssH, maxDpr);
5392
5553
  if (a || b) {
5393
5554
  staticDirty = true;
5555
+ cacheStale = true;
5394
5556
  interactiveDirty = true;
5395
5557
  loop.requestFrame();
5396
5558
  }
@@ -5398,6 +5560,7 @@ var createRenderer = (opts) => {
5398
5560
  setBackground(bg) {
5399
5561
  background = bg;
5400
5562
  staticDirty = true;
5563
+ cacheStale = true;
5401
5564
  loop.requestFrame();
5402
5565
  },
5403
5566
  setSelectionColor(color) {
@@ -5408,6 +5571,7 @@ var createRenderer = (opts) => {
5408
5571
  setHideFrames(hidden) {
5409
5572
  hideFrames = hidden;
5410
5573
  staticDirty = true;
5574
+ cacheStale = true;
5411
5575
  loop.requestFrame();
5412
5576
  },
5413
5577
  stats: () => loop.stats(),
@@ -5422,6 +5586,11 @@ var createRenderer = (opts) => {
5422
5586
  unsubFontEpoch();
5423
5587
  unsubMathEpoch();
5424
5588
  assetCache.dispose();
5589
+ if (cacheSurface) {
5590
+ cacheSurface.canvas.width = 0;
5591
+ cacheSurface.canvas.height = 0;
5592
+ cacheSurface = null;
5593
+ }
5425
5594
  }
5426
5595
  };
5427
5596
  };