@canvas-harness/core 0.1.1 → 0.1.2

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
@@ -817,7 +817,28 @@ type SnapshotEnv = {
817
817
  type NodeTypeDefOptions = {
818
818
  /** Unique type id, e.g. 'chart-card'. */
819
819
  type: string;
820
+ /**
821
+ * Canvas paint for the node body. Caller has already applied the
822
+ * camera + node transform, so paint at `(0, 0, node.w, node.h)`.
823
+ *
824
+ * Context state contract:
825
+ * - The renderer wraps this call in `ctx.save()` / `ctx.restore()`
826
+ * so any state you change (fillStyle, strokeStyle, lineWidth,
827
+ * setLineDash, globalAlpha, font, …) is automatically rolled
828
+ * back before the next node draws — set whatever you need
829
+ * without worrying about cleanup.
830
+ * - Conversely, **do NOT assume default state on entry.** Always
831
+ * set the styles you depend on; the previous node's values
832
+ * may still be in effect.
833
+ * - The transform is NOT save/restore-protected at this level
834
+ * (it's managed one frame up by the renderer). Don't leave
835
+ * `translate` / `rotate` / `scale` calls un-paired.
836
+ */
820
837
  renderCanvas?: (ctx: CanvasRenderingContext2D, node: Node, env: RenderEnv) => void;
838
+ /**
839
+ * Low-zoom / motion fallback paint — see ARCHITECTURE.md §5.3 LOD.
840
+ * Same context-state contract as `renderCanvas`.
841
+ */
821
842
  drawPlaceholder?: (ctx: CanvasRenderingContext2D, node: Node, env: RenderEnv) => void;
822
843
  /**
823
844
  * The React view component reference. Stored as `unknown` here because the
@@ -2328,6 +2349,15 @@ declare const worldViewport: (surface: CanvasSurface, camera: CameraState) => Wo
2328
2349
  * Wraps a draw callback in the local-frame transform for one node:
2329
2350
  * translates to the node's center, rotates by node.angle, then translates
2330
2351
  * back to the node's top-left so the drawer can build paths in (0..w, 0..h).
2352
+ *
2353
+ * Fast path: when `node.angle === 0` (the common case) we skip the
2354
+ * canvas2d save/restore pair entirely and manually un-translate after
2355
+ * the callback. `save()`/`restore()` allocate + swap a full graphics
2356
+ * state record; at 10k+ nodes/frame that's ~1ms of the paint budget.
2357
+ * The callback is responsible for not leaking transform state — all
2358
+ * built-in drawers (drawShape, drawCompositeRough, paintFrameNode,
2359
+ * paintImageNode, paintIconNode) honor this contract by either
2360
+ * leaving the transform untouched or restoring their own inner pushes.
2331
2361
  */
2332
2362
  declare const drawWithNodeTransform: (ctx: CanvasRenderingContext2D, node: Node, fn: () => void) => void;
2333
2363
 
package/dist/index.d.ts CHANGED
@@ -817,7 +817,28 @@ type SnapshotEnv = {
817
817
  type NodeTypeDefOptions = {
818
818
  /** Unique type id, e.g. 'chart-card'. */
819
819
  type: string;
820
+ /**
821
+ * Canvas paint for the node body. Caller has already applied the
822
+ * camera + node transform, so paint at `(0, 0, node.w, node.h)`.
823
+ *
824
+ * Context state contract:
825
+ * - The renderer wraps this call in `ctx.save()` / `ctx.restore()`
826
+ * so any state you change (fillStyle, strokeStyle, lineWidth,
827
+ * setLineDash, globalAlpha, font, …) is automatically rolled
828
+ * back before the next node draws — set whatever you need
829
+ * without worrying about cleanup.
830
+ * - Conversely, **do NOT assume default state on entry.** Always
831
+ * set the styles you depend on; the previous node's values
832
+ * may still be in effect.
833
+ * - The transform is NOT save/restore-protected at this level
834
+ * (it's managed one frame up by the renderer). Don't leave
835
+ * `translate` / `rotate` / `scale` calls un-paired.
836
+ */
820
837
  renderCanvas?: (ctx: CanvasRenderingContext2D, node: Node, env: RenderEnv) => void;
838
+ /**
839
+ * Low-zoom / motion fallback paint — see ARCHITECTURE.md §5.3 LOD.
840
+ * Same context-state contract as `renderCanvas`.
841
+ */
821
842
  drawPlaceholder?: (ctx: CanvasRenderingContext2D, node: Node, env: RenderEnv) => void;
822
843
  /**
823
844
  * The React view component reference. Stored as `unknown` here because the
@@ -2328,6 +2349,15 @@ declare const worldViewport: (surface: CanvasSurface, camera: CameraState) => Wo
2328
2349
  * Wraps a draw callback in the local-frame transform for one node:
2329
2350
  * translates to the node's center, rotates by node.angle, then translates
2330
2351
  * back to the node's top-left so the drawer can build paths in (0..w, 0..h).
2352
+ *
2353
+ * Fast path: when `node.angle === 0` (the common case) we skip the
2354
+ * canvas2d save/restore pair entirely and manually un-translate after
2355
+ * the callback. `save()`/`restore()` allocate + swap a full graphics
2356
+ * state record; at 10k+ nodes/frame that's ~1ms of the paint budget.
2357
+ * The callback is responsible for not leaking transform state — all
2358
+ * built-in drawers (drawShape, drawCompositeRough, paintFrameNode,
2359
+ * paintImageNode, paintIconNode) honor this contract by either
2360
+ * leaving the transform untouched or restoring their own inner pushes.
2331
2361
  */
2332
2362
  declare const drawWithNodeTransform: (ctx: CanvasRenderingContext2D, node: Node, fn: () => void) => void;
2333
2363
 
package/dist/index.js CHANGED
@@ -4869,16 +4869,18 @@ var applyCameraTransform = (surface, camera) => {
4869
4869
  };
4870
4870
  var worldViewport = (surface, camera) => viewportWorldRect(camera, surface.cssWidth, surface.cssHeight);
4871
4871
  var drawWithNodeTransform = (ctx, node, fn) => {
4872
- ctx.save();
4873
4872
  if (node.angle === 0) {
4874
4873
  ctx.translate(node.x, node.y);
4875
- } else {
4876
- const cx = node.x + node.w / 2;
4877
- const cy = node.y + node.h / 2;
4878
- ctx.translate(cx, cy);
4879
- ctx.rotate(node.angle);
4880
- ctx.translate(-node.w / 2, -node.h / 2);
4874
+ fn();
4875
+ ctx.translate(-node.x, -node.y);
4876
+ return;
4881
4877
  }
4878
+ ctx.save();
4879
+ const cx = node.x + node.w / 2;
4880
+ const cy = node.y + node.h / 2;
4881
+ ctx.translate(cx, cy);
4882
+ ctx.rotate(node.angle);
4883
+ ctx.translate(-node.w / 2, -node.h / 2);
4882
4884
  fn();
4883
4885
  ctx.restore();
4884
4886
  };
@@ -4900,6 +4902,12 @@ var createRenderer = (opts) => {
4900
4902
  let interactiveDirty = false;
4901
4903
  let overlaySet = /* @__PURE__ */ new Set();
4902
4904
  let lastDrawn = 0;
4905
+ let sortedNodeIdsCache = null;
4906
+ let sortedEdgeIdsCache = null;
4907
+ const invalidateSortedCaches = () => {
4908
+ sortedNodeIdsCache = null;
4909
+ sortedEdgeIdsCache = null;
4910
+ };
4903
4911
  const requestRepaint = () => {
4904
4912
  staticDirty = true;
4905
4913
  loop.requestFrame();
@@ -5029,7 +5037,9 @@ var createRenderer = (opts) => {
5029
5037
  }
5030
5038
  if (def.renderCanvas) {
5031
5039
  drawWithNodeTransform(staticSurface.ctx, node, () => {
5040
+ staticSurface.ctx.save();
5032
5041
  def.renderCanvas(staticSurface.ctx, node, renderEnv);
5042
+ staticSurface.ctx.restore();
5033
5043
  });
5034
5044
  drawn++;
5035
5045
  }
@@ -5062,11 +5072,19 @@ var createRenderer = (opts) => {
5062
5072
  }
5063
5073
  }
5064
5074
  if (def.drawPlaceholder) {
5065
- drawWithNodeTransform(ctx, node, () => def.drawPlaceholder(ctx, node, env));
5075
+ drawWithNodeTransform(ctx, node, () => {
5076
+ ctx.save();
5077
+ def.drawPlaceholder(ctx, node, env);
5078
+ ctx.restore();
5079
+ });
5066
5080
  return true;
5067
5081
  }
5068
5082
  if (def.renderCanvas) {
5069
- drawWithNodeTransform(ctx, node, () => def.renderCanvas(ctx, node, env));
5083
+ drawWithNodeTransform(ctx, node, () => {
5084
+ ctx.save();
5085
+ def.renderCanvas(ctx, node, env);
5086
+ ctx.restore();
5087
+ });
5070
5088
  return true;
5071
5089
  }
5072
5090
  return false;
@@ -5133,14 +5151,23 @@ var createRenderer = (opts) => {
5133
5151
  isMoving: isMoving2
5134
5152
  });
5135
5153
  };
5154
+ const getSortedEdgeIds = () => {
5155
+ if (sortedEdgeIdsCache) return sortedEdgeIdsCache;
5156
+ const all = store.getAllEdges();
5157
+ sortedEdgeIdsCache = all.slice().sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1)).map((e) => e.id);
5158
+ return sortedEdgeIdsCache;
5159
+ };
5136
5160
  const visibleEdges = (viewport) => {
5137
5161
  const ids = store.querySpatial({ rect: viewport }).edges;
5162
+ if (ids.length === 0) return [];
5163
+ const visibleSet = new Set(ids);
5164
+ const sorted = getSortedEdgeIds();
5138
5165
  const result = [];
5139
- for (const id of ids) {
5166
+ for (const id of sorted) {
5167
+ if (!visibleSet.has(id)) continue;
5140
5168
  const e = store.getEdge(id);
5141
5169
  if (e) result.push(e);
5142
5170
  }
5143
- result.sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1));
5144
5171
  return result;
5145
5172
  };
5146
5173
  const paintInteractive = () => {
@@ -5288,21 +5315,31 @@ var createRenderer = (opts) => {
5288
5315
  }
5289
5316
  return m;
5290
5317
  };
5318
+ const getSortedNodeIds = () => {
5319
+ if (sortedNodeIdsCache) return sortedNodeIdsCache;
5320
+ const all = store.getAllNodes();
5321
+ sortedNodeIdsCache = all.slice().sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1)).map((n) => n.id);
5322
+ return sortedNodeIdsCache;
5323
+ };
5291
5324
  const visibleNodes = (camera, viewport) => {
5292
5325
  const ids = store.querySpatial({ rect: viewport }).nodes;
5326
+ if (ids.length === 0) return [];
5327
+ const visibleSet = new Set(ids);
5328
+ const sorted = getSortedNodeIds();
5293
5329
  const result = [];
5294
5330
  const minWorldSize = MIN_ON_SCREEN_SIZE_PX / camera.z;
5295
- for (const id of ids) {
5331
+ for (const id of sorted) {
5332
+ if (!visibleSet.has(id)) continue;
5296
5333
  const n = store.getNode(id);
5297
5334
  if (!n) continue;
5298
5335
  if (n.w < minWorldSize && n.h < minWorldSize) continue;
5299
5336
  if (intersectsViewport(n, viewport)) result.push(n);
5300
5337
  }
5301
- result.sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1));
5302
5338
  return result;
5303
5339
  };
5304
5340
  const loop = createFrameLoop({ draw: drawFrame });
5305
5341
  const onStoreChange = () => {
5342
+ invalidateSortedCaches();
5306
5343
  staticDirty = true;
5307
5344
  interactiveDirty = true;
5308
5345
  loop.requestFrame();