@canvas-harness/core 0.1.5 → 0.1.7

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
@@ -945,10 +945,11 @@ declare const defineNode: (opts: NodeTypeDefOptions) => NodeTypeDef;
945
945
  */
946
946
  type InlineType = 'text' | 'bold' | 'italic' | 'underline' | 'strike' | 'highlight' | 'code' | 'link'
947
947
  /**
948
- * LaTeX math expression, content is the source between `$...$` (no
949
- * leading/trailing whitespace, no line breaks). Rendered via MathJax
950
- * SVG output and rasterized to an inline bitmap at paint time. See
951
- * `text/math/`.
948
+ * LaTeX math expression, content is the source between `$$...$$` (no
949
+ * line breaks). Rendered via MathJax SVG output and rasterized to an
950
+ * inline bitmap at paint time. See `text/math/`. Double-dollar
951
+ * delimiters avoid the false-positives of single `$` in prose that
952
+ * mentions currency (e.g. `$5 to $10`).
952
953
  */
953
954
  | 'math';
954
955
  type Token = {
@@ -1075,7 +1076,7 @@ declare const getFontEpoch: () => number;
1075
1076
  * Lazy loader for MathJax — same pattern as `render/rough/loader.ts`.
1076
1077
  *
1077
1078
  * MathJax is ~600KB and only useful for scenes with LaTeX math. We
1078
- * defer loading until the first `$...$` token requests a compile,
1079
+ * defer loading until the first `$$...$$` token requests a compile,
1079
1080
  * then convert LaTeX → SVG strings off the main rAF path.
1080
1081
  *
1081
1082
  * Loaded from jsDelivr CDN rather than bundled because the v4
@@ -1454,9 +1455,33 @@ type InteractionState = {
1454
1455
  resizeLockAspect: boolean;
1455
1456
  /** Whether the user is holding Alt during a resize (resize from center). */
1456
1457
  resizeFromCenter: boolean;
1458
+ /**
1459
+ * Live in-progress geometry of the resized node — written every
1460
+ * pointermove, committed to the store once on pointer-up. While
1461
+ * present, `store.getNode(id)` still returns the original geometry;
1462
+ * the renderer overlays this draft via `mapDragPositions` for the
1463
+ * interactive layer paint. Mirrors how `dragDelta` works for drag.
1464
+ */
1465
+ resizeDraft: {
1466
+ x: number;
1467
+ y: number;
1468
+ w: number;
1469
+ h: number;
1470
+ angle: number;
1471
+ } | null;
1457
1472
  marqueeRect: WorldRect | null;
1458
1473
  /** Whether the marquee should add to (true, shift held) or replace selection. */
1459
1474
  marqueeAdditive: boolean;
1475
+ /**
1476
+ * Live in-progress cubic controls of an edge being mid-point-dragged.
1477
+ * Written every pointermove, committed to the store once on
1478
+ * pointer-up. Same draft+commit model as `resizeDraft` and
1479
+ * `dragDelta` — keeps mid-gesture mutations off the 'change' bus.
1480
+ */
1481
+ midpointDraft: {
1482
+ edgeId: EdgeId;
1483
+ control: [Vec2, Vec2];
1484
+ } | null;
1460
1485
  draftEdge: {
1461
1486
  source: EdgeEnd;
1462
1487
  target: EdgeEnd;
package/dist/index.d.ts CHANGED
@@ -945,10 +945,11 @@ declare const defineNode: (opts: NodeTypeDefOptions) => NodeTypeDef;
945
945
  */
946
946
  type InlineType = 'text' | 'bold' | 'italic' | 'underline' | 'strike' | 'highlight' | 'code' | 'link'
947
947
  /**
948
- * LaTeX math expression, content is the source between `$...$` (no
949
- * leading/trailing whitespace, no line breaks). Rendered via MathJax
950
- * SVG output and rasterized to an inline bitmap at paint time. See
951
- * `text/math/`.
948
+ * LaTeX math expression, content is the source between `$$...$$` (no
949
+ * line breaks). Rendered via MathJax SVG output and rasterized to an
950
+ * inline bitmap at paint time. See `text/math/`. Double-dollar
951
+ * delimiters avoid the false-positives of single `$` in prose that
952
+ * mentions currency (e.g. `$5 to $10`).
952
953
  */
953
954
  | 'math';
954
955
  type Token = {
@@ -1075,7 +1076,7 @@ declare const getFontEpoch: () => number;
1075
1076
  * Lazy loader for MathJax — same pattern as `render/rough/loader.ts`.
1076
1077
  *
1077
1078
  * MathJax is ~600KB and only useful for scenes with LaTeX math. We
1078
- * defer loading until the first `$...$` token requests a compile,
1079
+ * defer loading until the first `$$...$$` token requests a compile,
1079
1080
  * then convert LaTeX → SVG strings off the main rAF path.
1080
1081
  *
1081
1082
  * Loaded from jsDelivr CDN rather than bundled because the v4
@@ -1454,9 +1455,33 @@ type InteractionState = {
1454
1455
  resizeLockAspect: boolean;
1455
1456
  /** Whether the user is holding Alt during a resize (resize from center). */
1456
1457
  resizeFromCenter: boolean;
1458
+ /**
1459
+ * Live in-progress geometry of the resized node — written every
1460
+ * pointermove, committed to the store once on pointer-up. While
1461
+ * present, `store.getNode(id)` still returns the original geometry;
1462
+ * the renderer overlays this draft via `mapDragPositions` for the
1463
+ * interactive layer paint. Mirrors how `dragDelta` works for drag.
1464
+ */
1465
+ resizeDraft: {
1466
+ x: number;
1467
+ y: number;
1468
+ w: number;
1469
+ h: number;
1470
+ angle: number;
1471
+ } | null;
1457
1472
  marqueeRect: WorldRect | null;
1458
1473
  /** Whether the marquee should add to (true, shift held) or replace selection. */
1459
1474
  marqueeAdditive: boolean;
1475
+ /**
1476
+ * Live in-progress cubic controls of an edge being mid-point-dragged.
1477
+ * Written every pointermove, committed to the store once on
1478
+ * pointer-up. Same draft+commit model as `resizeDraft` and
1479
+ * `dragDelta` — keeps mid-gesture mutations off the 'change' bus.
1480
+ */
1481
+ midpointDraft: {
1482
+ edgeId: EdgeId;
1483
+ control: [Vec2, Vec2];
1484
+ } | null;
1460
1485
  draftEdge: {
1461
1486
  source: EdgeEnd;
1462
1487
  target: EdgeEnd;
package/dist/index.js CHANGED
@@ -231,53 +231,6 @@ var rotatePoint = (px, py, cx, cy, cos, sin) => {
231
231
  return { x: cx + dx * cos - dy * sin, y: cy + dx * sin + dy * cos };
232
232
  };
233
233
 
234
- // src/edges/auto-route.ts
235
- var CONTROL_MAX = 200;
236
- var CONTROL_FRACTION = 0.4;
237
- var sideOf = (node, localX, localY) => {
238
- const distLeft = localX;
239
- const distRight = node.w - localX;
240
- const distTop = localY;
241
- const distBottom = node.h - localY;
242
- const minDist = Math.min(distLeft, distRight, distTop, distBottom);
243
- if (minDist === distLeft) return "w";
244
- if (minDist === distRight) return "e";
245
- if (minDist === distTop) return "n";
246
- return "s";
247
- };
248
- var sideNormalLocal = (side) => {
249
- switch (side) {
250
- case "n":
251
- return { x: 0, y: -1 };
252
- case "s":
253
- return { x: 0, y: 1 };
254
- case "e":
255
- return { x: 1, y: 0 };
256
- case "w":
257
- return { x: -1, y: 0 };
258
- }
259
- };
260
- var rotateVecByAngle = (v, angle) => {
261
- if (angle === 0) return v;
262
- const cos = Math.cos(angle);
263
- const sin = Math.sin(angle);
264
- return { x: v.x * cos - v.y * sin, y: v.x * sin + v.y * cos };
265
- };
266
- var autoRouteControls = (sourceWorld, targetWorld, sourceNormalWorld, targetNormalWorld) => {
267
- const dx = targetWorld.x - sourceWorld.x;
268
- const dy = targetWorld.y - sourceWorld.y;
269
- const dist = Math.hypot(dx, dy);
270
- const offset = Math.min(CONTROL_MAX, CONTROL_FRACTION * dist);
271
- const fallbackSource = dist > 0 ? { x: dx / dist, y: dy / dist } : { x: 1, y: 0 };
272
- const fallbackTarget = dist > 0 ? { x: -dx / dist, y: -dy / dist } : { x: -1, y: 0 };
273
- const ns = sourceNormalWorld ?? fallbackSource;
274
- const nt = targetNormalWorld ?? fallbackTarget;
275
- return {
276
- c1: { x: sourceWorld.x + ns.x * offset, y: sourceWorld.y + ns.y * offset },
277
- c2: { x: targetWorld.x + nt.x * offset, y: targetWorld.y + nt.y * offset }
278
- };
279
- };
280
-
281
234
  // src/edges/project.ts
282
235
  var projectEndToWorld = (end, getNode) => {
283
236
  if (!isAttached(end)) return end.worldPoint;
@@ -326,6 +279,112 @@ var projectToNodeBoundary = (world, node) => {
326
279
  return { x: clampedX, y: node.h };
327
280
  };
328
281
 
282
+ // src/edges/auto-route.ts
283
+ var CONTROL_MAX = 200;
284
+ var CONTROL_FRACTION = 0.4;
285
+ var BOUNDARY_EPS = 0.5;
286
+ var isLocalOffsetInsideBody = (localOffset, node) => {
287
+ const onLeft = Math.abs(localOffset.x) <= BOUNDARY_EPS;
288
+ const onRight = Math.abs(localOffset.x - node.w) <= BOUNDARY_EPS;
289
+ const onTop = Math.abs(localOffset.y) <= BOUNDARY_EPS;
290
+ const onBottom = Math.abs(localOffset.y - node.h) <= BOUNDARY_EPS;
291
+ const inside = localOffset.x > -BOUNDARY_EPS && localOffset.x < node.w + BOUNDARY_EPS && localOffset.y > -BOUNDARY_EPS && localOffset.y < node.h + BOUNDARY_EPS;
292
+ return inside && !onLeft && !onRight && !onTop && !onBottom;
293
+ };
294
+ var sideOf = (node, localX, localY) => {
295
+ const distLeft = localX;
296
+ const distRight = node.w - localX;
297
+ const distTop = localY;
298
+ const distBottom = node.h - localY;
299
+ const minDist = Math.min(distLeft, distRight, distTop, distBottom);
300
+ if (minDist === distLeft) return "w";
301
+ if (minDist === distRight) return "e";
302
+ if (minDist === distTop) return "n";
303
+ return "s";
304
+ };
305
+ var sideNormalLocal = (side) => {
306
+ switch (side) {
307
+ case "n":
308
+ return { x: 0, y: -1 };
309
+ case "s":
310
+ return { x: 0, y: 1 };
311
+ case "e":
312
+ return { x: 1, y: 0 };
313
+ case "w":
314
+ return { x: -1, y: 0 };
315
+ }
316
+ };
317
+ var rotateVecByAngle = (v, angle) => {
318
+ if (angle === 0) return v;
319
+ const cos = Math.cos(angle);
320
+ const sin = Math.sin(angle);
321
+ return { x: v.x * cos - v.y * sin, y: v.x * sin + v.y * cos };
322
+ };
323
+ var autoRouteControls = (sourceWorld, targetWorld, sourceNormalWorld, targetNormalWorld) => {
324
+ const dx = targetWorld.x - sourceWorld.x;
325
+ const dy = targetWorld.y - sourceWorld.y;
326
+ const dist = Math.hypot(dx, dy);
327
+ const offset = Math.min(CONTROL_MAX, CONTROL_FRACTION * dist);
328
+ const fallbackSource = dist > 0 ? { x: dx / dist, y: dy / dist } : { x: 1, y: 0 };
329
+ const fallbackTarget = dist > 0 ? { x: -dx / dist, y: -dy / dist } : { x: -1, y: 0 };
330
+ const ns = sourceNormalWorld ?? fallbackSource;
331
+ const nt = targetNormalWorld ?? fallbackTarget;
332
+ return {
333
+ c1: { x: sourceWorld.x + ns.x * offset, y: sourceWorld.y + ns.y * offset },
334
+ c2: { x: targetWorld.x + nt.x * offset, y: targetWorld.y + nt.y * offset }
335
+ };
336
+ };
337
+ var computeAsymmetricRoute = (sourceNode, targetNode) => {
338
+ const srcCenterWorld = {
339
+ x: sourceNode.x + sourceNode.w / 2,
340
+ y: sourceNode.y + sourceNode.h / 2
341
+ };
342
+ const srcInTgtLocal = worldToNodeLocal(srcCenterWorld, targetNode);
343
+ const tgtHalfW = targetNode.w / 2;
344
+ const tgtHalfH = targetNode.h / 2;
345
+ const dxNorm = (srcInTgtLocal.x - tgtHalfW) / Math.max(1, tgtHalfW);
346
+ const dyNorm = (srcInTgtLocal.y - tgtHalfH) / Math.max(1, tgtHalfH);
347
+ const targetSide = Math.abs(dxNorm) >= Math.abs(dyNorm) ? dxNorm > 0 ? "e" : "w" : dyNorm > 0 ? "s" : "n";
348
+ let tgtEntryLocal;
349
+ if (targetSide === "n" || targetSide === "s") {
350
+ const sideY = targetSide === "n" ? 0 : targetNode.h;
351
+ const clampX = Math.max(0, Math.min(targetNode.w, srcInTgtLocal.x));
352
+ tgtEntryLocal = { x: clampX, y: sideY };
353
+ } else {
354
+ const sideX = targetSide === "w" ? 0 : targetNode.w;
355
+ const clampY = Math.max(0, Math.min(targetNode.h, srcInTgtLocal.y));
356
+ tgtEntryLocal = { x: sideX, y: clampY };
357
+ }
358
+ const targetEntryWorld = nodeLocalToWorld(tgtEntryLocal, targetNode);
359
+ const tgtEntryInSrcLocal = worldToNodeLocal(targetEntryWorld, sourceNode);
360
+ const srcHalfW = sourceNode.w / 2;
361
+ const srcHalfH = sourceNode.h / 2;
362
+ const rayDx = tgtEntryInSrcLocal.x - srcHalfW;
363
+ const rayDy = tgtEntryInSrcLocal.y - srcHalfH;
364
+ const tx = rayDx === 0 ? Number.POSITIVE_INFINITY : (rayDx > 0 ? srcHalfW : -srcHalfW) / rayDx;
365
+ const ty = rayDy === 0 ? Number.POSITIVE_INFINITY : (rayDy > 0 ? srcHalfH : -srcHalfH) / rayDy;
366
+ const t = Math.min(tx, ty);
367
+ const srcExitLocal = {
368
+ x: srcHalfW + rayDx * t,
369
+ y: srcHalfH + rayDy * t
370
+ };
371
+ const sourceExitWorld = nodeLocalToWorld(srcExitLocal, sourceNode);
372
+ const dxWorld = targetEntryWorld.x - sourceExitWorld.x;
373
+ const dyWorld = targetEntryWorld.y - sourceExitWorld.y;
374
+ const distance2 = Math.hypot(dxWorld, dyWorld);
375
+ const offset = Math.min(CONTROL_MAX, CONTROL_FRACTION * distance2);
376
+ const c1 = distance2 > 0 ? {
377
+ x: sourceExitWorld.x + dxWorld / distance2 * offset,
378
+ y: sourceExitWorld.y + dyWorld / distance2 * offset
379
+ } : { ...sourceExitWorld };
380
+ const tgtNormalWorld = rotateVecByAngle(sideNormalLocal(targetSide), targetNode.angle);
381
+ const c2 = {
382
+ x: targetEntryWorld.x + tgtNormalWorld.x * offset,
383
+ y: targetEntryWorld.y + tgtNormalWorld.y * offset
384
+ };
385
+ return { source: sourceExitWorld, target: targetEntryWorld, c1, c2 };
386
+ };
387
+
329
388
  // src/edges/clip.ts
330
389
  var fullVisibleClipResult = (samples) => ({
331
390
  startIndex: 0,
@@ -586,8 +645,8 @@ var computeEdgeGeometry = (edge, getNode) => {
586
645
  targetNodeId
587
646
  };
588
647
  }
589
- const sourceWorld = projectEndToWorld(edge.source, getNode);
590
- const targetWorld = projectEndToWorld(edge.target, getNode);
648
+ let sourceWorld = projectEndToWorld(edge.source, getNode);
649
+ let targetWorld = projectEndToWorld(edge.target, getNode);
591
650
  if (!sourceWorld || !targetWorld) return null;
592
651
  let samples;
593
652
  if (edge.pathStyle === "bezier") {
@@ -596,6 +655,12 @@ var computeEdgeGeometry = (edge, getNode) => {
596
655
  if (edge.control && edge.control.length >= 2) {
597
656
  c1 = edge.control[0];
598
657
  c2 = edge.control[1];
658
+ } else if (sourceNode && targetNode && isAttached(edge.source) && isAttached(edge.target) && isLocalOffsetInsideBody(edge.source.localOffset, sourceNode) && isLocalOffsetInsideBody(edge.target.localOffset, targetNode)) {
659
+ const r = computeAsymmetricRoute(sourceNode, targetNode);
660
+ sourceWorld = r.source;
661
+ targetWorld = r.target;
662
+ c1 = r.c1;
663
+ c2 = r.c2;
599
664
  } else {
600
665
  const sourceNormal = sourceNode && isAttached(edge.source) ? rotateVecByAngle(
601
666
  sideNormalLocal(
@@ -1048,7 +1113,6 @@ var COMPOSITE = /* @__PURE__ */ new Set([
1048
1113
  var isCompositePrimitive = (type) => COMPOSITE.has(type);
1049
1114
  var isDrawablePrimitive = (type) => ATOMIC.has(type) || COMPOSITE.has(type);
1050
1115
  var PLAIN_RECT_CORNER_THRESHOLD_PX = 1.5;
1051
- var STROKE_VISIBILITY_THRESHOLD_PX = 0.5;
1052
1116
  var LAYERED_OFFSET = 12;
1053
1117
  var drawShape = (ctx, node, scale, theme, opts) => {
1054
1118
  if (!isDrawablePrimitive(node.type)) return;
@@ -1067,7 +1131,7 @@ var drawAtomic = (ctx, type, w, h, style, scale, theme, opts) => {
1067
1131
  const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
1068
1132
  const stroke = resolveColor(style, "strokeColor", DEFAULT_STYLE.strokeColor, theme);
1069
1133
  const fillVisible = !isFullyTransparent(fill);
1070
- const strokeVisible = strokeWidth > 0 && strokeWidth * scale >= STROKE_VISIBILITY_THRESHOLD_PX && !isFullyTransparent(stroke);
1134
+ const strokeVisible = strokeWidth > 0 && !isFullyTransparent(stroke);
1071
1135
  if (!fillVisible && !strokeVisible) return;
1072
1136
  const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
1073
1137
  switch (type) {
@@ -1104,7 +1168,7 @@ var drawAtomic = (ctx, type, w, h, style, scale, theme, opts) => {
1104
1168
  }
1105
1169
  if (strokeVisible && !opts?.skipStroke) {
1106
1170
  ctx.strokeStyle = stroke;
1107
- ctx.lineWidth = strokeWidth;
1171
+ ctx.lineWidth = Math.max(strokeWidth, 1 / scale);
1108
1172
  ctx.setLineDash(dashPatternFor(style?.strokeStyle, strokeWidth));
1109
1173
  ctx.stroke();
1110
1174
  }
@@ -1480,13 +1544,16 @@ var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
1480
1544
  const isDark = theme?.("mode") === "dark";
1481
1545
  const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
1482
1546
  const strokeColor = deriveRoughStrokeColor(rawStroke, fill, isDark);
1483
- const strokeWidth = resolveStrokeWidth(style, theme);
1484
- if (strokeWidth <= 0) return;
1547
+ const rawStrokeWidth = resolveStrokeWidth(style, theme);
1548
+ if (rawStrokeWidth <= 0) return;
1485
1549
  const roughness = style?.roughness ?? 0;
1486
1550
  if (roughness <= 0) return;
1551
+ const isNoBorderIntent = isFullyTransparent(rawStroke);
1552
+ const effectiveStrokeStyle = isNoBorderIntent ? "solid" : style?.strokeStyle ?? "solid";
1553
+ const strokeWidth = isNoBorderIntent ? DEFAULT_STYLE.strokeWidth : rawStrokeWidth;
1487
1554
  const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
1488
1555
  const radius = Math.max(0, Math.min(cornerRadius, w / 2, h / 2));
1489
- const dash = dashPatternFor(style?.strokeStyle, strokeWidth);
1556
+ const dash = dashPatternFor(effectiveStrokeStyle, strokeWidth);
1490
1557
  const detail = apparentDetail(Math.max(w, h), scale);
1491
1558
  const cacheKey = [
1492
1559
  type,
@@ -1495,7 +1562,7 @@ var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
1495
1562
  radius.toFixed(1),
1496
1563
  strokeColor,
1497
1564
  strokeWidth.toFixed(2),
1498
- style?.strokeStyle ?? "solid",
1565
+ effectiveStrokeStyle,
1499
1566
  roughness.toFixed(2),
1500
1567
  seed,
1501
1568
  detail.curveStepCount,
@@ -1603,7 +1670,7 @@ var buildPath = (type, x, y, w, h, radius) => {
1603
1670
  };
1604
1671
 
1605
1672
  // src/text/tokens.ts
1606
- var INLINE_PATTERN = /(\$[^$\n]+?\$|\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1673
+ var INLINE_PATTERN = /(\$\$[^\n]+?\$\$|\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1607
1674
  var HR_LINE_PATTERN = /^[ \t]*---[ \t]*$/;
1608
1675
  var DOUBLE_HR_LINE_PATTERN = /^[ \t]*===[ \t]*$/;
1609
1676
  var transformSymbols = (value) => value.replace(/<=>|<->|<-|->|\[\]|\[[vx]\]/gi, (match) => {
@@ -1641,8 +1708,8 @@ var tokenizeInline = (segment) => {
1641
1708
  tokens.push({ type: "link", content: transformSymbols(match.slice(1, splitIndex)) });
1642
1709
  } else if (match.startsWith("`") && match.endsWith("`")) {
1643
1710
  tokens.push({ type: "code", content: match.slice(1, -1) });
1644
- } else if (match.startsWith("$") && match.endsWith("$")) {
1645
- tokens.push({ type: "math", content: match.slice(1, -1) });
1711
+ } else if (match.startsWith("$$") && match.endsWith("$$")) {
1712
+ tokens.push({ type: "math", content: match.slice(2, -2) });
1646
1713
  } else {
1647
1714
  tokens.push({ type: "text", content: transformSymbols(match) });
1648
1715
  }
@@ -2646,7 +2713,7 @@ var DEFAULT_EDGE_STYLE = {
2646
2713
  sourceArrowhead: "none",
2647
2714
  targetArrowhead: "arrow-filled"
2648
2715
  };
2649
- var STROKE_VISIBILITY_THRESHOLD_PX2 = 0.5;
2716
+ var STROKE_VISIBILITY_THRESHOLD_PX = 0.5;
2650
2717
  var ARROWHEAD_VISIBILITY_THRESHOLD_PX = 2;
2651
2718
  var samplePaintStride = (scale) => {
2652
2719
  if (scale < 0.15) return 8;
@@ -2660,7 +2727,7 @@ var drawEdge = (ctx, edge, geom, sourceNode, targetNode, scale, theme, opts) =>
2660
2727
  if (samples.length < 2) return;
2661
2728
  const style = edge.style;
2662
2729
  const strokeWidth = typeof style?.strokeWidth === "number" ? style.strokeWidth : theme?.("strokeWidth") ?? DEFAULT_EDGE_STYLE.strokeWidth;
2663
- if (strokeWidth * scale < STROKE_VISIBILITY_THRESHOLD_PX2) return;
2730
+ if (strokeWidth * scale < STROKE_VISIBILITY_THRESHOLD_PX) return;
2664
2731
  const strokeColor = typeof style?.strokeColor === "string" ? style.strokeColor : theme?.("edge.strokeColor") ?? DEFAULT_EDGE_STYLE.strokeColor;
2665
2732
  const sourceArrowhead = style?.sourceArrowhead ?? DEFAULT_EDGE_STYLE.sourceArrowhead;
2666
2733
  const targetArrowhead = style?.targetArrowhead ?? DEFAULT_EDGE_STYLE.targetArrowhead;
@@ -3376,6 +3443,8 @@ var idleInteractionState = () => ({
3376
3443
  resizeHandle: null,
3377
3444
  resizeLockAspect: false,
3378
3445
  resizeFromCenter: false,
3446
+ resizeDraft: null,
3447
+ midpointDraft: null,
3379
3448
  marqueeRect: null,
3380
3449
  marqueeAdditive: false,
3381
3450
  draftEdge: null,
@@ -4952,7 +5021,9 @@ var createRenderer = (opts) => {
4952
5021
  const scale = camera.z * surface.dpr;
4953
5022
  const interaction = store.getInteractionState();
4954
5023
  const excludedNodes = interaction.mode === "dragging" || interaction.mode === "resizing" ? new Set(interaction.draggedIds) : null;
4955
- const excludedEdges = excludedNodes ? incidentEdgeIds(excludedNodes) : null;
5024
+ const baseExcludedEdges = excludedNodes ? incidentEdgeIds(excludedNodes) : null;
5025
+ const midpointEdgeId = interaction.midpointDraft?.edgeId ?? null;
5026
+ const excludedEdges = midpointEdgeId !== null ? /* @__PURE__ */ new Set([...baseExcludedEdges ?? [], midpointEdgeId]) : baseExcludedEdges;
4956
5027
  paintBackground(surface.ctx, { viewport, zoom: camera.z, background });
4957
5028
  const visible = visibleNodes(camera, viewport);
4958
5029
  const isMoving2 = interaction.mode === "panning" || interaction.mode === "zooming" || interaction.mode === "dragging" || interaction.mode === "resizing" || interaction.mode === "rotating";
@@ -5364,6 +5435,23 @@ var createRenderer = (opts) => {
5364
5435
  }
5365
5436
  }
5366
5437
  }
5438
+ if (interaction.midpointDraft) {
5439
+ const { edgeId, control } = interaction.midpointDraft;
5440
+ const edge = store.getEdge(edgeId);
5441
+ if (edge) {
5442
+ const draftEdge = { ...edge, control };
5443
+ const geom = computeEdgeGeometry(draftEdge, (id) => store.getNode(id));
5444
+ if (geom) {
5445
+ const sourceNode = geom.sourceNodeId ? store.getNode(geom.sourceNodeId) ?? null : null;
5446
+ const targetNode = geom.targetNodeId ? store.getNode(geom.targetNodeId) ?? null : null;
5447
+ drawEdge(ctx, draftEdge, geom, sourceNode, targetNode, scale, theme, {
5448
+ zoom: camera.z,
5449
+ dpr: interactiveSurface.dpr,
5450
+ isMoving: true
5451
+ });
5452
+ }
5453
+ }
5454
+ }
5367
5455
  const selection = store.getSelection();
5368
5456
  const selectedNodeIds = [];
5369
5457
  const selectedEdgeIds = [];
@@ -5436,7 +5524,12 @@ var createRenderer = (opts) => {
5436
5524
  y: orig.y + interaction.dragDelta.y
5437
5525
  });
5438
5526
  } else {
5439
- m.set(orig.id, live);
5527
+ const d = interaction.resizeDraft;
5528
+ if (d) {
5529
+ m.set(orig.id, { ...live, x: d.x, y: d.y, w: d.w, h: d.h, angle: d.angle });
5530
+ } else {
5531
+ m.set(orig.id, live);
5532
+ }
5440
5533
  }
5441
5534
  }
5442
5535
  return m;