@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.cjs CHANGED
@@ -233,53 +233,6 @@ var rotatePoint = (px, py, cx, cy, cos, sin) => {
233
233
  return { x: cx + dx * cos - dy * sin, y: cy + dx * sin + dy * cos };
234
234
  };
235
235
 
236
- // src/edges/auto-route.ts
237
- var CONTROL_MAX = 200;
238
- var CONTROL_FRACTION = 0.4;
239
- var sideOf = (node, localX, localY) => {
240
- const distLeft = localX;
241
- const distRight = node.w - localX;
242
- const distTop = localY;
243
- const distBottom = node.h - localY;
244
- const minDist = Math.min(distLeft, distRight, distTop, distBottom);
245
- if (minDist === distLeft) return "w";
246
- if (minDist === distRight) return "e";
247
- if (minDist === distTop) return "n";
248
- return "s";
249
- };
250
- var sideNormalLocal = (side) => {
251
- switch (side) {
252
- case "n":
253
- return { x: 0, y: -1 };
254
- case "s":
255
- return { x: 0, y: 1 };
256
- case "e":
257
- return { x: 1, y: 0 };
258
- case "w":
259
- return { x: -1, y: 0 };
260
- }
261
- };
262
- var rotateVecByAngle = (v, angle) => {
263
- if (angle === 0) return v;
264
- const cos = Math.cos(angle);
265
- const sin = Math.sin(angle);
266
- return { x: v.x * cos - v.y * sin, y: v.x * sin + v.y * cos };
267
- };
268
- var autoRouteControls = (sourceWorld, targetWorld, sourceNormalWorld, targetNormalWorld) => {
269
- const dx = targetWorld.x - sourceWorld.x;
270
- const dy = targetWorld.y - sourceWorld.y;
271
- const dist = Math.hypot(dx, dy);
272
- const offset = Math.min(CONTROL_MAX, CONTROL_FRACTION * dist);
273
- const fallbackSource = dist > 0 ? { x: dx / dist, y: dy / dist } : { x: 1, y: 0 };
274
- const fallbackTarget = dist > 0 ? { x: -dx / dist, y: -dy / dist } : { x: -1, y: 0 };
275
- const ns = sourceNormalWorld ?? fallbackSource;
276
- const nt = targetNormalWorld ?? fallbackTarget;
277
- return {
278
- c1: { x: sourceWorld.x + ns.x * offset, y: sourceWorld.y + ns.y * offset },
279
- c2: { x: targetWorld.x + nt.x * offset, y: targetWorld.y + nt.y * offset }
280
- };
281
- };
282
-
283
236
  // src/edges/project.ts
284
237
  var projectEndToWorld = (end, getNode) => {
285
238
  if (!isAttached(end)) return end.worldPoint;
@@ -328,6 +281,112 @@ var projectToNodeBoundary = (world, node) => {
328
281
  return { x: clampedX, y: node.h };
329
282
  };
330
283
 
284
+ // src/edges/auto-route.ts
285
+ var CONTROL_MAX = 200;
286
+ var CONTROL_FRACTION = 0.4;
287
+ var BOUNDARY_EPS = 0.5;
288
+ var isLocalOffsetInsideBody = (localOffset, node) => {
289
+ const onLeft = Math.abs(localOffset.x) <= BOUNDARY_EPS;
290
+ const onRight = Math.abs(localOffset.x - node.w) <= BOUNDARY_EPS;
291
+ const onTop = Math.abs(localOffset.y) <= BOUNDARY_EPS;
292
+ const onBottom = Math.abs(localOffset.y - node.h) <= BOUNDARY_EPS;
293
+ const inside = localOffset.x > -BOUNDARY_EPS && localOffset.x < node.w + BOUNDARY_EPS && localOffset.y > -BOUNDARY_EPS && localOffset.y < node.h + BOUNDARY_EPS;
294
+ return inside && !onLeft && !onRight && !onTop && !onBottom;
295
+ };
296
+ var sideOf = (node, localX, localY) => {
297
+ const distLeft = localX;
298
+ const distRight = node.w - localX;
299
+ const distTop = localY;
300
+ const distBottom = node.h - localY;
301
+ const minDist = Math.min(distLeft, distRight, distTop, distBottom);
302
+ if (minDist === distLeft) return "w";
303
+ if (minDist === distRight) return "e";
304
+ if (minDist === distTop) return "n";
305
+ return "s";
306
+ };
307
+ var sideNormalLocal = (side) => {
308
+ switch (side) {
309
+ case "n":
310
+ return { x: 0, y: -1 };
311
+ case "s":
312
+ return { x: 0, y: 1 };
313
+ case "e":
314
+ return { x: 1, y: 0 };
315
+ case "w":
316
+ return { x: -1, y: 0 };
317
+ }
318
+ };
319
+ var rotateVecByAngle = (v, angle) => {
320
+ if (angle === 0) return v;
321
+ const cos = Math.cos(angle);
322
+ const sin = Math.sin(angle);
323
+ return { x: v.x * cos - v.y * sin, y: v.x * sin + v.y * cos };
324
+ };
325
+ var autoRouteControls = (sourceWorld, targetWorld, sourceNormalWorld, targetNormalWorld) => {
326
+ const dx = targetWorld.x - sourceWorld.x;
327
+ const dy = targetWorld.y - sourceWorld.y;
328
+ const dist = Math.hypot(dx, dy);
329
+ const offset = Math.min(CONTROL_MAX, CONTROL_FRACTION * dist);
330
+ const fallbackSource = dist > 0 ? { x: dx / dist, y: dy / dist } : { x: 1, y: 0 };
331
+ const fallbackTarget = dist > 0 ? { x: -dx / dist, y: -dy / dist } : { x: -1, y: 0 };
332
+ const ns = sourceNormalWorld ?? fallbackSource;
333
+ const nt = targetNormalWorld ?? fallbackTarget;
334
+ return {
335
+ c1: { x: sourceWorld.x + ns.x * offset, y: sourceWorld.y + ns.y * offset },
336
+ c2: { x: targetWorld.x + nt.x * offset, y: targetWorld.y + nt.y * offset }
337
+ };
338
+ };
339
+ var computeAsymmetricRoute = (sourceNode, targetNode) => {
340
+ const srcCenterWorld = {
341
+ x: sourceNode.x + sourceNode.w / 2,
342
+ y: sourceNode.y + sourceNode.h / 2
343
+ };
344
+ const srcInTgtLocal = worldToNodeLocal(srcCenterWorld, targetNode);
345
+ const tgtHalfW = targetNode.w / 2;
346
+ const tgtHalfH = targetNode.h / 2;
347
+ const dxNorm = (srcInTgtLocal.x - tgtHalfW) / Math.max(1, tgtHalfW);
348
+ const dyNorm = (srcInTgtLocal.y - tgtHalfH) / Math.max(1, tgtHalfH);
349
+ const targetSide = Math.abs(dxNorm) >= Math.abs(dyNorm) ? dxNorm > 0 ? "e" : "w" : dyNorm > 0 ? "s" : "n";
350
+ let tgtEntryLocal;
351
+ if (targetSide === "n" || targetSide === "s") {
352
+ const sideY = targetSide === "n" ? 0 : targetNode.h;
353
+ const clampX = Math.max(0, Math.min(targetNode.w, srcInTgtLocal.x));
354
+ tgtEntryLocal = { x: clampX, y: sideY };
355
+ } else {
356
+ const sideX = targetSide === "w" ? 0 : targetNode.w;
357
+ const clampY = Math.max(0, Math.min(targetNode.h, srcInTgtLocal.y));
358
+ tgtEntryLocal = { x: sideX, y: clampY };
359
+ }
360
+ const targetEntryWorld = nodeLocalToWorld(tgtEntryLocal, targetNode);
361
+ const tgtEntryInSrcLocal = worldToNodeLocal(targetEntryWorld, sourceNode);
362
+ const srcHalfW = sourceNode.w / 2;
363
+ const srcHalfH = sourceNode.h / 2;
364
+ const rayDx = tgtEntryInSrcLocal.x - srcHalfW;
365
+ const rayDy = tgtEntryInSrcLocal.y - srcHalfH;
366
+ const tx = rayDx === 0 ? Number.POSITIVE_INFINITY : (rayDx > 0 ? srcHalfW : -srcHalfW) / rayDx;
367
+ const ty = rayDy === 0 ? Number.POSITIVE_INFINITY : (rayDy > 0 ? srcHalfH : -srcHalfH) / rayDy;
368
+ const t = Math.min(tx, ty);
369
+ const srcExitLocal = {
370
+ x: srcHalfW + rayDx * t,
371
+ y: srcHalfH + rayDy * t
372
+ };
373
+ const sourceExitWorld = nodeLocalToWorld(srcExitLocal, sourceNode);
374
+ const dxWorld = targetEntryWorld.x - sourceExitWorld.x;
375
+ const dyWorld = targetEntryWorld.y - sourceExitWorld.y;
376
+ const distance2 = Math.hypot(dxWorld, dyWorld);
377
+ const offset = Math.min(CONTROL_MAX, CONTROL_FRACTION * distance2);
378
+ const c1 = distance2 > 0 ? {
379
+ x: sourceExitWorld.x + dxWorld / distance2 * offset,
380
+ y: sourceExitWorld.y + dyWorld / distance2 * offset
381
+ } : { ...sourceExitWorld };
382
+ const tgtNormalWorld = rotateVecByAngle(sideNormalLocal(targetSide), targetNode.angle);
383
+ const c2 = {
384
+ x: targetEntryWorld.x + tgtNormalWorld.x * offset,
385
+ y: targetEntryWorld.y + tgtNormalWorld.y * offset
386
+ };
387
+ return { source: sourceExitWorld, target: targetEntryWorld, c1, c2 };
388
+ };
389
+
331
390
  // src/edges/clip.ts
332
391
  var fullVisibleClipResult = (samples) => ({
333
392
  startIndex: 0,
@@ -588,8 +647,8 @@ var computeEdgeGeometry = (edge, getNode) => {
588
647
  targetNodeId
589
648
  };
590
649
  }
591
- const sourceWorld = projectEndToWorld(edge.source, getNode);
592
- const targetWorld = projectEndToWorld(edge.target, getNode);
650
+ let sourceWorld = projectEndToWorld(edge.source, getNode);
651
+ let targetWorld = projectEndToWorld(edge.target, getNode);
593
652
  if (!sourceWorld || !targetWorld) return null;
594
653
  let samples;
595
654
  if (edge.pathStyle === "bezier") {
@@ -598,6 +657,12 @@ var computeEdgeGeometry = (edge, getNode) => {
598
657
  if (edge.control && edge.control.length >= 2) {
599
658
  c1 = edge.control[0];
600
659
  c2 = edge.control[1];
660
+ } else if (sourceNode && targetNode && isAttached(edge.source) && isAttached(edge.target) && isLocalOffsetInsideBody(edge.source.localOffset, sourceNode) && isLocalOffsetInsideBody(edge.target.localOffset, targetNode)) {
661
+ const r = computeAsymmetricRoute(sourceNode, targetNode);
662
+ sourceWorld = r.source;
663
+ targetWorld = r.target;
664
+ c1 = r.c1;
665
+ c2 = r.c2;
601
666
  } else {
602
667
  const sourceNormal = sourceNode && isAttached(edge.source) ? rotateVecByAngle(
603
668
  sideNormalLocal(
@@ -1050,7 +1115,6 @@ var COMPOSITE = /* @__PURE__ */ new Set([
1050
1115
  var isCompositePrimitive = (type) => COMPOSITE.has(type);
1051
1116
  var isDrawablePrimitive = (type) => ATOMIC.has(type) || COMPOSITE.has(type);
1052
1117
  var PLAIN_RECT_CORNER_THRESHOLD_PX = 1.5;
1053
- var STROKE_VISIBILITY_THRESHOLD_PX = 0.5;
1054
1118
  var LAYERED_OFFSET = 12;
1055
1119
  var drawShape = (ctx, node, scale, theme, opts) => {
1056
1120
  if (!isDrawablePrimitive(node.type)) return;
@@ -1069,7 +1133,7 @@ var drawAtomic = (ctx, type, w, h, style, scale, theme, opts) => {
1069
1133
  const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
1070
1134
  const stroke = resolveColor(style, "strokeColor", DEFAULT_STYLE.strokeColor, theme);
1071
1135
  const fillVisible = !isFullyTransparent(fill);
1072
- const strokeVisible = strokeWidth > 0 && strokeWidth * scale >= STROKE_VISIBILITY_THRESHOLD_PX && !isFullyTransparent(stroke);
1136
+ const strokeVisible = strokeWidth > 0 && !isFullyTransparent(stroke);
1073
1137
  if (!fillVisible && !strokeVisible) return;
1074
1138
  const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
1075
1139
  switch (type) {
@@ -1106,7 +1170,7 @@ var drawAtomic = (ctx, type, w, h, style, scale, theme, opts) => {
1106
1170
  }
1107
1171
  if (strokeVisible && !opts?.skipStroke) {
1108
1172
  ctx.strokeStyle = stroke;
1109
- ctx.lineWidth = strokeWidth;
1173
+ ctx.lineWidth = Math.max(strokeWidth, 1 / scale);
1110
1174
  ctx.setLineDash(dashPatternFor(style?.strokeStyle, strokeWidth));
1111
1175
  ctx.stroke();
1112
1176
  }
@@ -1482,13 +1546,16 @@ var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
1482
1546
  const isDark = theme?.("mode") === "dark";
1483
1547
  const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
1484
1548
  const strokeColor = deriveRoughStrokeColor(rawStroke, fill, isDark);
1485
- const strokeWidth = resolveStrokeWidth(style, theme);
1486
- if (strokeWidth <= 0) return;
1549
+ const rawStrokeWidth = resolveStrokeWidth(style, theme);
1550
+ if (rawStrokeWidth <= 0) return;
1487
1551
  const roughness = style?.roughness ?? 0;
1488
1552
  if (roughness <= 0) return;
1553
+ const isNoBorderIntent = isFullyTransparent(rawStroke);
1554
+ const effectiveStrokeStyle = isNoBorderIntent ? "solid" : style?.strokeStyle ?? "solid";
1555
+ const strokeWidth = isNoBorderIntent ? DEFAULT_STYLE.strokeWidth : rawStrokeWidth;
1489
1556
  const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
1490
1557
  const radius = Math.max(0, Math.min(cornerRadius, w / 2, h / 2));
1491
- const dash = dashPatternFor(style?.strokeStyle, strokeWidth);
1558
+ const dash = dashPatternFor(effectiveStrokeStyle, strokeWidth);
1492
1559
  const detail = apparentDetail(Math.max(w, h), scale);
1493
1560
  const cacheKey = [
1494
1561
  type,
@@ -1497,7 +1564,7 @@ var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
1497
1564
  radius.toFixed(1),
1498
1565
  strokeColor,
1499
1566
  strokeWidth.toFixed(2),
1500
- style?.strokeStyle ?? "solid",
1567
+ effectiveStrokeStyle,
1501
1568
  roughness.toFixed(2),
1502
1569
  seed,
1503
1570
  detail.curveStepCount,
@@ -1605,7 +1672,7 @@ var buildPath = (type, x, y, w, h, radius) => {
1605
1672
  };
1606
1673
 
1607
1674
  // src/text/tokens.ts
1608
- var INLINE_PATTERN = /(\$[^$\n]+?\$|\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1675
+ var INLINE_PATTERN = /(\$\$[^\n]+?\$\$|\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1609
1676
  var HR_LINE_PATTERN = /^[ \t]*---[ \t]*$/;
1610
1677
  var DOUBLE_HR_LINE_PATTERN = /^[ \t]*===[ \t]*$/;
1611
1678
  var transformSymbols = (value) => value.replace(/<=>|<->|<-|->|\[\]|\[[vx]\]/gi, (match) => {
@@ -1643,8 +1710,8 @@ var tokenizeInline = (segment) => {
1643
1710
  tokens.push({ type: "link", content: transformSymbols(match.slice(1, splitIndex)) });
1644
1711
  } else if (match.startsWith("`") && match.endsWith("`")) {
1645
1712
  tokens.push({ type: "code", content: match.slice(1, -1) });
1646
- } else if (match.startsWith("$") && match.endsWith("$")) {
1647
- tokens.push({ type: "math", content: match.slice(1, -1) });
1713
+ } else if (match.startsWith("$$") && match.endsWith("$$")) {
1714
+ tokens.push({ type: "math", content: match.slice(2, -2) });
1648
1715
  } else {
1649
1716
  tokens.push({ type: "text", content: transformSymbols(match) });
1650
1717
  }
@@ -2648,7 +2715,7 @@ var DEFAULT_EDGE_STYLE = {
2648
2715
  sourceArrowhead: "none",
2649
2716
  targetArrowhead: "arrow-filled"
2650
2717
  };
2651
- var STROKE_VISIBILITY_THRESHOLD_PX2 = 0.5;
2718
+ var STROKE_VISIBILITY_THRESHOLD_PX = 0.5;
2652
2719
  var ARROWHEAD_VISIBILITY_THRESHOLD_PX = 2;
2653
2720
  var samplePaintStride = (scale) => {
2654
2721
  if (scale < 0.15) return 8;
@@ -2662,7 +2729,7 @@ var drawEdge = (ctx, edge, geom, sourceNode, targetNode, scale, theme, opts) =>
2662
2729
  if (samples.length < 2) return;
2663
2730
  const style = edge.style;
2664
2731
  const strokeWidth = typeof style?.strokeWidth === "number" ? style.strokeWidth : theme?.("strokeWidth") ?? DEFAULT_EDGE_STYLE.strokeWidth;
2665
- if (strokeWidth * scale < STROKE_VISIBILITY_THRESHOLD_PX2) return;
2732
+ if (strokeWidth * scale < STROKE_VISIBILITY_THRESHOLD_PX) return;
2666
2733
  const strokeColor = typeof style?.strokeColor === "string" ? style.strokeColor : theme?.("edge.strokeColor") ?? DEFAULT_EDGE_STYLE.strokeColor;
2667
2734
  const sourceArrowhead = style?.sourceArrowhead ?? DEFAULT_EDGE_STYLE.sourceArrowhead;
2668
2735
  const targetArrowhead = style?.targetArrowhead ?? DEFAULT_EDGE_STYLE.targetArrowhead;
@@ -3378,6 +3445,8 @@ var idleInteractionState = () => ({
3378
3445
  resizeHandle: null,
3379
3446
  resizeLockAspect: false,
3380
3447
  resizeFromCenter: false,
3448
+ resizeDraft: null,
3449
+ midpointDraft: null,
3381
3450
  marqueeRect: null,
3382
3451
  marqueeAdditive: false,
3383
3452
  draftEdge: null,
@@ -4954,7 +5023,9 @@ var createRenderer = (opts) => {
4954
5023
  const scale = camera.z * surface.dpr;
4955
5024
  const interaction = store.getInteractionState();
4956
5025
  const excludedNodes = interaction.mode === "dragging" || interaction.mode === "resizing" ? new Set(interaction.draggedIds) : null;
4957
- const excludedEdges = excludedNodes ? incidentEdgeIds(excludedNodes) : null;
5026
+ const baseExcludedEdges = excludedNodes ? incidentEdgeIds(excludedNodes) : null;
5027
+ const midpointEdgeId = interaction.midpointDraft?.edgeId ?? null;
5028
+ const excludedEdges = midpointEdgeId !== null ? /* @__PURE__ */ new Set([...baseExcludedEdges ?? [], midpointEdgeId]) : baseExcludedEdges;
4958
5029
  paintBackground(surface.ctx, { viewport, zoom: camera.z, background });
4959
5030
  const visible = visibleNodes(camera, viewport);
4960
5031
  const isMoving2 = interaction.mode === "panning" || interaction.mode === "zooming" || interaction.mode === "dragging" || interaction.mode === "resizing" || interaction.mode === "rotating";
@@ -5366,6 +5437,23 @@ var createRenderer = (opts) => {
5366
5437
  }
5367
5438
  }
5368
5439
  }
5440
+ if (interaction.midpointDraft) {
5441
+ const { edgeId, control } = interaction.midpointDraft;
5442
+ const edge = store.getEdge(edgeId);
5443
+ if (edge) {
5444
+ const draftEdge = { ...edge, control };
5445
+ const geom = computeEdgeGeometry(draftEdge, (id) => store.getNode(id));
5446
+ if (geom) {
5447
+ const sourceNode = geom.sourceNodeId ? store.getNode(geom.sourceNodeId) ?? null : null;
5448
+ const targetNode = geom.targetNodeId ? store.getNode(geom.targetNodeId) ?? null : null;
5449
+ drawEdge(ctx, draftEdge, geom, sourceNode, targetNode, scale, theme, {
5450
+ zoom: camera.z,
5451
+ dpr: interactiveSurface.dpr,
5452
+ isMoving: true
5453
+ });
5454
+ }
5455
+ }
5456
+ }
5369
5457
  const selection = store.getSelection();
5370
5458
  const selectedNodeIds = [];
5371
5459
  const selectedEdgeIds = [];
@@ -5438,7 +5526,12 @@ var createRenderer = (opts) => {
5438
5526
  y: orig.y + interaction.dragDelta.y
5439
5527
  });
5440
5528
  } else {
5441
- m.set(orig.id, live);
5529
+ const d = interaction.resizeDraft;
5530
+ if (d) {
5531
+ m.set(orig.id, { ...live, x: d.x, y: d.y, w: d.w, h: d.h, angle: d.angle });
5532
+ } else {
5533
+ m.set(orig.id, live);
5534
+ }
5442
5535
  }
5443
5536
  }
5444
5537
  return m;