@canvas-harness/core 0.0.2 → 0.0.4

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
@@ -3133,6 +3133,8 @@ var inverseOp = (op) => {
3133
3133
  return { type: "group.remove", group: op.group };
3134
3134
  case "group.remove":
3135
3135
  return { type: "group.upsert", group: op.group };
3136
+ case "frame.reorder":
3137
+ return { type: "frame.reorder", ids: op.prev, prev: op.ids };
3136
3138
  }
3137
3139
  };
3138
3140
  var inverseBatch = (batch) => {
@@ -3200,6 +3202,7 @@ var createCanvasStore = (opts = {}) => {
3200
3202
  const groupIdsAtom = signia.atom("groupIds", []);
3201
3203
  const cameraAtom = signia.atom("camera", initial.camera);
3202
3204
  const selectionAtom = signia.atom("selection", initial.selection);
3205
+ const frameOrderAtom = signia.atom("frameOrder", initial.frameOrder ?? []);
3203
3206
  const interactionAtom = signia.atom("interaction", idleInteractionState());
3204
3207
  const localPresenceAtom = signia.atom("presence", emptyPresenceState(clientId));
3205
3208
  const remotePresence = /* @__PURE__ */ new Map();
@@ -3306,6 +3309,9 @@ var createCanvasStore = (opts = {}) => {
3306
3309
  nodeAtoms.set(op.node.id, a);
3307
3310
  nodeIdsAtom.update((ids) => [...ids, op.node.id]);
3308
3311
  reindexNode(op.node);
3312
+ if (op.node.type === "frame") {
3313
+ frameOrderAtom.update((ids) => ids.includes(op.node.id) ? ids : [...ids, op.node.id]);
3314
+ }
3309
3315
  break;
3310
3316
  }
3311
3317
  case "node.update": {
@@ -3330,6 +3336,9 @@ var createCanvasStore = (opts = {}) => {
3330
3336
  nodeIdsAtom.update((ids) => ids.filter((x) => x !== id));
3331
3337
  unindexNode(id);
3332
3338
  incidentEdges.delete(id);
3339
+ if (op.node.type === "frame") {
3340
+ frameOrderAtom.update((ids) => ids.filter((x) => x !== id));
3341
+ }
3333
3342
  break;
3334
3343
  }
3335
3344
  case "edge.add": {
@@ -3379,6 +3388,10 @@ var createCanvasStore = (opts = {}) => {
3379
3388
  groupIdsAtom.update((ids) => ids.filter((x) => x !== id));
3380
3389
  break;
3381
3390
  }
3391
+ case "frame.reorder": {
3392
+ frameOrderAtom.set([...op.ids]);
3393
+ break;
3394
+ }
3382
3395
  }
3383
3396
  };
3384
3397
  const enqueueOp = (op) => {
@@ -3401,6 +3414,7 @@ var createCanvasStore = (opts = {}) => {
3401
3414
  return prev;
3402
3415
  };
3403
3416
  const populateInitial = (scene) => {
3417
+ const seededFrameOrder = [];
3404
3418
  for (const id of Object.keys(scene.nodes)) {
3405
3419
  const node = scene.nodes[id];
3406
3420
  if (!node) continue;
@@ -3408,7 +3422,9 @@ var createCanvasStore = (opts = {}) => {
3408
3422
  nodeAtoms.set(node.id, a);
3409
3423
  nodeIdsAtom.update((ids) => [...ids, node.id]);
3410
3424
  reindexNode(node);
3425
+ if (node.type === "frame") seededFrameOrder.push(node.id);
3411
3426
  }
3427
+ if (!scene.frameOrder) frameOrderAtom.set(seededFrameOrder);
3412
3428
  for (const id of Object.keys(scene.edges)) {
3413
3429
  const edge = scene.edges[id];
3414
3430
  if (!edge) continue;
@@ -3710,6 +3726,53 @@ var createCanvasStore = (opts = {}) => {
3710
3726
  getNodeCount: () => nodeIdsAtom.value.length,
3711
3727
  getEdgeCount: () => edgeIdsAtom.value.length,
3712
3728
  getGroupCount: () => groupIdsAtom.value.length,
3729
+ getFrames: () => {
3730
+ const out = [];
3731
+ for (const id of frameOrderAtom.value) {
3732
+ const n = nodeAtoms.get(id)?.value;
3733
+ if (n && n.type === "frame") out.push(n);
3734
+ }
3735
+ return out;
3736
+ },
3737
+ setFrameOrder(ids) {
3738
+ const valid = /* @__PURE__ */ new Set();
3739
+ for (const a of nodeAtoms.values()) {
3740
+ if (a.value.type === "frame") valid.add(a.value.id);
3741
+ }
3742
+ const filtered = [];
3743
+ const seen = /* @__PURE__ */ new Set();
3744
+ for (const id of ids) {
3745
+ if (valid.has(id) && !seen.has(id)) {
3746
+ filtered.push(id);
3747
+ seen.add(id);
3748
+ }
3749
+ }
3750
+ for (const id of valid) {
3751
+ if (!seen.has(id)) filtered.push(id);
3752
+ }
3753
+ const prev = [...frameOrderAtom.value];
3754
+ if (filtered.length === prev.length && filtered.every((id, i) => id === prev[i])) {
3755
+ return;
3756
+ }
3757
+ enqueueOp({ type: "frame.reorder", ids: filtered, prev });
3758
+ },
3759
+ getNodesInFrame(id) {
3760
+ const frame = nodeAtoms.get(id)?.value;
3761
+ if (!frame || frame.type !== "frame") return [];
3762
+ const frameAabb = nodeAABB(frame);
3763
+ const candidates = nodeIndex.queryRect(frameAabb);
3764
+ const out = [];
3765
+ for (const cid of candidates) {
3766
+ if (cid === id) continue;
3767
+ const node = nodeAtoms.get(cid)?.value;
3768
+ if (!node || node.type === "frame") continue;
3769
+ const a = nodeAABB(node);
3770
+ if (a.x >= frameAabb.x && a.y >= frameAabb.y && a.x + a.w <= frameAabb.x + frameAabb.w && a.y + a.h <= frameAabb.y + frameAabb.h) {
3771
+ out.push(node);
3772
+ }
3773
+ }
3774
+ return out;
3775
+ },
3713
3776
  getEdgeGeometry(id) {
3714
3777
  const edge = edgeAtoms.get(id)?.value;
3715
3778
  if (!edge) return void 0;
@@ -3871,7 +3934,8 @@ var toSerialized = (scene) => ({
3871
3934
  edges: Object.values(scene.edges),
3872
3935
  groups: Object.values(scene.groups),
3873
3936
  camera: scene.camera,
3874
- selection: scene.selection
3937
+ selection: scene.selection,
3938
+ ...scene.frameOrder && scene.frameOrder.length > 0 ? { frameOrder: scene.frameOrder } : {}
3875
3939
  });
3876
3940
  var fromSerialized = (raw) => {
3877
3941
  let working = raw;
@@ -3893,7 +3957,8 @@ var fromSerialized = (raw) => {
3893
3957
  edges: Object.fromEntries(ser.edges.map((e) => [asEdgeId(e.id), e])),
3894
3958
  groups: Object.fromEntries(ser.groups.map((g) => [asGroupId(g.id), g])),
3895
3959
  camera: ser.camera,
3896
- selection: ser.selection
3960
+ selection: ser.selection,
3961
+ ...ser.frameOrder ? { frameOrder: ser.frameOrder } : {}
3897
3962
  };
3898
3963
  };
3899
3964
  var storeToJSON = (store) => ({
@@ -4438,6 +4503,40 @@ var drawMarquee = (ctx, rect, scale, color) => {
4438
4503
  ctx.restore();
4439
4504
  };
4440
4505
 
4506
+ // src/render/paint-frame.ts
4507
+ var FRAME_BORDER_PX = 1.5;
4508
+ var FRAME_BORDER_COLOR_DEFAULT = "#94a3b8";
4509
+ var FRAME_FILL_DEFAULT = "rgba(148, 163, 184, 0.06)";
4510
+ var FRAME_LABEL_FONT_PX = 12;
4511
+ var FRAME_LABEL_GAP_PX = 6;
4512
+ var FRAME_LABEL_COLOR = "#64748b";
4513
+ var paintFrameNode = (ctx, node, scale, theme) => {
4514
+ if (node.w <= 0 || node.h <= 0) return;
4515
+ const opacity = resolveOpacity(node.style, theme);
4516
+ const needsScope = opacity !== 1;
4517
+ if (needsScope) {
4518
+ ctx.save();
4519
+ ctx.globalAlpha = opacity;
4520
+ }
4521
+ const fill = node.style?.backgroundColor ?? (theme ? theme("frame.background") : void 0) ?? FRAME_FILL_DEFAULT;
4522
+ ctx.fillStyle = fill;
4523
+ ctx.fillRect(0, 0, node.w, node.h);
4524
+ const stroke = resolveColor(node.style, "strokeColor", FRAME_BORDER_COLOR_DEFAULT, theme);
4525
+ ctx.strokeStyle = stroke;
4526
+ ctx.lineWidth = FRAME_BORDER_PX / scale;
4527
+ ctx.setLineDash([]);
4528
+ ctx.strokeRect(0, 0, node.w, node.h);
4529
+ const labelPx = FRAME_LABEL_FONT_PX / scale;
4530
+ const gapPx = FRAME_LABEL_GAP_PX / scale;
4531
+ const label = node.content?.trim() || "Frame";
4532
+ ctx.fillStyle = FRAME_LABEL_COLOR;
4533
+ ctx.textBaseline = "bottom";
4534
+ ctx.textAlign = "left";
4535
+ ctx.font = `500 ${labelPx}px system-ui, -apple-system, sans-serif`;
4536
+ ctx.fillText(label, 0, -gapPx);
4537
+ if (needsScope) ctx.restore();
4538
+ };
4539
+
4441
4540
  // src/render/shapes/content-bounds.ts
4442
4541
  var SQRT2_INV = 1 / Math.SQRT2;
4443
4542
  var contentBounds = (node) => {
@@ -4511,6 +4610,7 @@ var createRenderer = (opts) => {
4511
4610
  const interactiveSurface = setupSurface(opts.interactiveCanvas);
4512
4611
  let background = opts.background;
4513
4612
  let selectionColor = opts.selectionColor ?? DEFAULT_SELECTION_COLOR;
4613
+ let hideFrames = false;
4514
4614
  sizeSurface(staticSurface, opts.width, opts.height);
4515
4615
  sizeSurface(interactiveSurface, opts.width, opts.height);
4516
4616
  let staticDirty = true;
@@ -4560,7 +4660,18 @@ var createRenderer = (opts) => {
4560
4660
  const cameraIsMoving = interaction.mode === "panning" || interaction.mode === "zooming";
4561
4661
  const movingNodeCount = excludedNodes?.size ?? 0;
4562
4662
  const roughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
4663
+ if (!hideFrames) {
4664
+ for (const node of visible) {
4665
+ if (node.type !== "frame") continue;
4666
+ if (excludedNodes?.has(node.id)) continue;
4667
+ drawWithNodeTransform(staticSurface.ctx, node, () => {
4668
+ paintFrameNode(staticSurface.ctx, node, scale, theme);
4669
+ });
4670
+ drawn++;
4671
+ }
4672
+ }
4563
4673
  for (const node of visible) {
4674
+ if (node.type === "frame") continue;
4564
4675
  if (excludedNodes?.has(node.id)) continue;
4565
4676
  const isEditingThis = editingNodeId === node.id;
4566
4677
  if (isDrawablePrimitive(node.type)) {
@@ -4763,9 +4874,13 @@ var createRenderer = (opts) => {
4763
4874
  isMoving: true};
4764
4875
  const dragRoughEnabled = inDragMap.size <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM;
4765
4876
  for (const node of inDragMap.values()) {
4766
- if (!isDrawablePrimitive(node.type) && node.type !== "text" && node.type !== "image" && node.type !== "icon")
4877
+ if (!isDrawablePrimitive(node.type) && node.type !== "text" && node.type !== "image" && node.type !== "icon" && node.type !== "frame")
4767
4878
  continue;
4768
4879
  drawWithNodeTransform(ctx, node, () => {
4880
+ if (node.type === "frame") {
4881
+ paintFrameNode(ctx, node, scale, theme);
4882
+ return;
4883
+ }
4769
4884
  if (node.type === "image") {
4770
4885
  paintImageNode(ctx, node, assetCache, theme);
4771
4886
  return;
@@ -4967,6 +5082,11 @@ var createRenderer = (opts) => {
4967
5082
  interactiveDirty = true;
4968
5083
  loop.requestFrame();
4969
5084
  },
5085
+ setHideFrames(hidden) {
5086
+ hideFrames = hidden;
5087
+ staticDirty = true;
5088
+ loop.requestFrame();
5089
+ },
4970
5090
  stats: () => loop.stats(),
4971
5091
  lastDrawCount: () => lastDrawn,
4972
5092
  getOverlaySet: () => [...overlaySet],
@@ -4997,6 +5117,7 @@ var sceneBounds = (store) => {
4997
5117
  let maxY = Number.NEGATIVE_INFINITY;
4998
5118
  for (const n of nodes) {
4999
5119
  if (n.hidden) continue;
5120
+ if (n.type === "frame") continue;
5000
5121
  const r = nodeAABB(n);
5001
5122
  if (r.x < minX) minX = r.x;
5002
5123
  if (r.y < minY) minY = r.y;
@@ -5027,6 +5148,7 @@ var renderMinimapContent = (ctx, store, mapWidth, mapHeight, opts = {}) => {
5027
5148
  const defaultColor = opts.defaultNodeColor ?? "#94a3b8";
5028
5149
  for (const node of store.getAllNodes()) {
5029
5150
  if (node.hidden) continue;
5151
+ if (node.type === "frame") continue;
5030
5152
  const r = nodeAABB(node);
5031
5153
  const x = offX + (r.x - bx) * scale;
5032
5154
  const y = offY + (r.y - by) * scale;