@canvas-harness/core 0.1.20 → 0.1.21

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
@@ -2311,6 +2311,17 @@ type RendererOptions = {
2311
2311
  */
2312
2312
  onOverlayChange?: (mountedIds: NodeId[]) => void;
2313
2313
  };
2314
+ /**
2315
+ * Path the last static-layer paint took through the cache tiers.
2316
+ * Returned by {@link Renderer.getLastDrawPath} for test instrumentation.
2317
+ * - `'idle'` no static paint has happened yet
2318
+ * - `'present'` cache hit, 1:1 blit (cheapest path)
2319
+ * - `'extend'` same-zoom pan past margin, shift + strip blit
2320
+ * - `'scaled'` mid-zoom scaled blit (no re-rasterization)
2321
+ * - `'scaled-extend'` mid-zoom-out, scale-blit center + redraw perimeter
2322
+ * - `'full'` full re-render (cache invalidated)
2323
+ */
2324
+ type StaticDrawPath = 'idle' | 'present' | 'extend' | 'scaled' | 'scaled-extend' | 'full';
2314
2325
  type Renderer = {
2315
2326
  /** Begin the rAF loop. Idempotent. */
2316
2327
  start(): void;
@@ -2334,6 +2345,13 @@ type Renderer = {
2334
2345
  stats(): FrameStats;
2335
2346
  /** Number of items the most recent paint actually drew. */
2336
2347
  lastDrawCount(): number;
2348
+ /**
2349
+ * Path the last static-layer paint took. Test instrumentation —
2350
+ * lets browser tests assert which cache tier fired. `'present'` =
2351
+ * 1:1 blit, `'extend'` = strip-extend + blit, `'scaled'` = mid-zoom
2352
+ * scaled blit, `'full'` = full re-render.
2353
+ */
2354
+ getLastDrawPath(): StaticDrawPath;
2337
2355
  /** Current overlay-mounted custom-node ids. */
2338
2356
  getOverlaySet(): NodeId[];
2339
2357
  /**
package/dist/index.d.ts CHANGED
@@ -2311,6 +2311,17 @@ type RendererOptions = {
2311
2311
  */
2312
2312
  onOverlayChange?: (mountedIds: NodeId[]) => void;
2313
2313
  };
2314
+ /**
2315
+ * Path the last static-layer paint took through the cache tiers.
2316
+ * Returned by {@link Renderer.getLastDrawPath} for test instrumentation.
2317
+ * - `'idle'` no static paint has happened yet
2318
+ * - `'present'` cache hit, 1:1 blit (cheapest path)
2319
+ * - `'extend'` same-zoom pan past margin, shift + strip blit
2320
+ * - `'scaled'` mid-zoom scaled blit (no re-rasterization)
2321
+ * - `'scaled-extend'` mid-zoom-out, scale-blit center + redraw perimeter
2322
+ * - `'full'` full re-render (cache invalidated)
2323
+ */
2324
+ type StaticDrawPath = 'idle' | 'present' | 'extend' | 'scaled' | 'scaled-extend' | 'full';
2314
2325
  type Renderer = {
2315
2326
  /** Begin the rAF loop. Idempotent. */
2316
2327
  start(): void;
@@ -2334,6 +2345,13 @@ type Renderer = {
2334
2345
  stats(): FrameStats;
2335
2346
  /** Number of items the most recent paint actually drew. */
2336
2347
  lastDrawCount(): number;
2348
+ /**
2349
+ * Path the last static-layer paint took. Test instrumentation —
2350
+ * lets browser tests assert which cache tier fired. `'present'` =
2351
+ * 1:1 blit, `'extend'` = strip-extend + blit, `'scaled'` = mid-zoom
2352
+ * scaled blit, `'full'` = full re-render.
2353
+ */
2354
+ getLastDrawPath(): StaticDrawPath;
2337
2355
  /** Current overlay-mounted custom-node ids. */
2338
2356
  getOverlaySet(): NodeId[];
2339
2357
  /**
package/dist/index.js CHANGED
@@ -4752,6 +4752,59 @@ var buildPath = (type, x, y, w, h, radius) => {
4752
4752
  }
4753
4753
  };
4754
4754
 
4755
+ // src/render/scene-cache-math.ts
4756
+ var computeCacheSourceRect = (cache5, view) => {
4757
+ const ratio = cache5.camZ / view.camZ;
4758
+ const srcX = Math.round(((view.camX - cache5.camX) * cache5.camZ + cache5.marginCssPx) * cache5.dpr);
4759
+ const srcY = Math.round(((view.camY - cache5.camY) * cache5.camZ + cache5.marginCssPx) * cache5.dpr);
4760
+ const srcW = view.widthCssPx * ratio * cache5.dpr;
4761
+ const srcH = view.heightCssPx * ratio * cache5.dpr;
4762
+ return { srcX, srcY, srcW, srcH };
4763
+ };
4764
+ var cacheCoversViewport = (cache5, view) => {
4765
+ const { srcX, srcY, srcW, srcH } = computeCacheSourceRect(cache5, view);
4766
+ return srcX >= 0 && srcY >= 0 && srcX + srcW <= cache5.widthDevicePx && srcY + srcH <= cache5.heightDevicePx;
4767
+ };
4768
+ var scaleRatioInBounds = (cacheCamZ, viewCamZ, maxRatio) => {
4769
+ if (viewCamZ <= 0 || cacheCamZ <= 0 || maxRatio <= 0) return false;
4770
+ const ratio = viewCamZ >= cacheCamZ ? viewCamZ / cacheCamZ : cacheCamZ / viewCamZ;
4771
+ return ratio <= maxRatio;
4772
+ };
4773
+ var cacheReuseLayout = (cache5, view) => {
4774
+ const ratio = view.camZ / cache5.camZ;
4775
+ const cacheW = cache5.widthDevicePx;
4776
+ const cacheH = cache5.heightDevicePx;
4777
+ const marginDev = cache5.marginCssPx * cache5.dpr;
4778
+ const destW = cacheW * ratio;
4779
+ const destH = cacheH * ratio;
4780
+ const destX = (cache5.camX - view.camX) * view.camZ * cache5.dpr + marginDev * (1 - ratio);
4781
+ const destY = (cache5.camY - view.camY) * view.camZ * cache5.dpr + marginDev * (1 - ratio);
4782
+ const dest = { x: destX, y: destY, w: destW, h: destH };
4783
+ const strips = {
4784
+ top: { x: 0, y: 0, w: cacheW, h: Math.max(0, destY) },
4785
+ bottom: {
4786
+ x: 0,
4787
+ y: destY + destH,
4788
+ w: cacheW,
4789
+ h: Math.max(0, cacheH - destY - destH)
4790
+ },
4791
+ left: { x: 0, y: destY, w: Math.max(0, destX), h: destH },
4792
+ right: {
4793
+ x: destX + destW,
4794
+ y: destY,
4795
+ w: Math.max(0, cacheW - destX - destW),
4796
+ h: destH
4797
+ }
4798
+ };
4799
+ const valid = destX >= 0 && destY >= 0 && destX + destW <= cacheW && destY + destH <= cacheH;
4800
+ return { dest, strips, valid };
4801
+ };
4802
+ var zoomExtendRatioInBounds = (cacheCamZ, viewCamZ, minRatio) => {
4803
+ if (viewCamZ <= 0 || cacheCamZ <= 0 || minRatio <= 0 || minRatio >= 1) return false;
4804
+ const ratio = viewCamZ / cacheCamZ;
4805
+ return ratio >= minRatio && ratio < 1;
4806
+ };
4807
+
4755
4808
  // src/render/shapes/content-bounds.ts
4756
4809
  var SQRT2_INV = 1 / Math.SQRT2;
4757
4810
  var contentBounds = (node) => {
@@ -4836,6 +4889,7 @@ var createRenderer = (opts) => {
4836
4889
  let interactiveDirty = false;
4837
4890
  let overlaySet = /* @__PURE__ */ new Set();
4838
4891
  let lastDrawn = 0;
4892
+ let lastDrawPath = "idle";
4839
4893
  let cacheSurface = null;
4840
4894
  let cacheCamX = 0;
4841
4895
  let cacheCamY = 0;
@@ -4905,9 +4959,8 @@ var createRenderer = (opts) => {
4905
4959
  theme: (token) => theme ? theme(token) : void 0
4906
4960
  };
4907
4961
  const editingNodeId = interaction.editingTarget?.kind === "node" ? interaction.editingTarget.id : null;
4908
- const cameraIsMoving = interaction.mode === "panning" || interaction.mode === "zooming";
4909
4962
  const movingNodeCount = excludedNodes?.size ?? 0;
4910
- const roughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
4963
+ const roughEnabled = movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
4911
4964
  if (!hideFrames) {
4912
4965
  for (const node of visible) {
4913
4966
  if (node.type !== "frame") continue;
@@ -5003,7 +5056,7 @@ var createRenderer = (opts) => {
5003
5056
  }
5004
5057
  }
5005
5058
  const visEdges = visibleEdges(viewport);
5006
- const edgeRoughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visEdges.length <= ROUGH_MAX_NODES;
5059
+ const edgeRoughEnabled = movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visEdges.length <= ROUGH_MAX_NODES;
5007
5060
  for (const edge of visEdges) {
5008
5061
  if (excludedEdges?.has(edge.id)) continue;
5009
5062
  paintOneEdge(surface.ctx, edge, scale, edgeRoughEnabled, camera.z, isMoving2);
@@ -5083,6 +5136,44 @@ var createRenderer = (opts) => {
5083
5136
  if (hw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, hx, 0, hw, cacheH);
5084
5137
  if (vh > 0 && vw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, vx, vy, vw, vh);
5085
5138
  };
5139
+ let scaledExtendScratch = null;
5140
+ const extendCacheScaled = (camera, layout) => {
5141
+ const cache5 = ensureCacheSurface();
5142
+ const cacheW = cache5.canvas.width;
5143
+ const cacheH = cache5.canvas.height;
5144
+ if (!scaledExtendScratch || scaledExtendScratch.width !== cacheW || scaledExtendScratch.height !== cacheH) {
5145
+ scaledExtendScratch = document.createElement("canvas");
5146
+ scaledExtendScratch.width = cacheW;
5147
+ scaledExtendScratch.height = cacheH;
5148
+ }
5149
+ const sctx = scaledExtendScratch.getContext("2d");
5150
+ sctx.setTransform(1, 0, 0, 1, 0, 0);
5151
+ sctx.clearRect(0, 0, cacheW, cacheH);
5152
+ sctx.drawImage(cache5.canvas, 0, 0);
5153
+ cache5.ctx.setTransform(1, 0, 0, 1, 0, 0);
5154
+ cache5.ctx.clearRect(0, 0, cacheW, cacheH);
5155
+ cache5.ctx.drawImage(
5156
+ scaledExtendScratch,
5157
+ layout.dest.x,
5158
+ layout.dest.y,
5159
+ layout.dest.w,
5160
+ layout.dest.h
5161
+ );
5162
+ cacheCamX = camera.x;
5163
+ cacheCamY = camera.y;
5164
+ cacheCamZ = camera.z;
5165
+ const strips = [
5166
+ layout.strips.top,
5167
+ layout.strips.bottom,
5168
+ layout.strips.left,
5169
+ layout.strips.right
5170
+ ];
5171
+ for (const s of strips) {
5172
+ if (s.w > 0 && s.h > 0) {
5173
+ renderCacheStrip(cache5, camera.x, camera.y, camera.z, s.x, s.y, s.w, s.h);
5174
+ }
5175
+ }
5176
+ };
5086
5177
  const cacheSourceOffset = (camera) => {
5087
5178
  const dpr = staticSurface.dpr;
5088
5179
  return {
@@ -5104,21 +5195,72 @@ var createRenderer = (opts) => {
5104
5195
  staticSurface.ctx.clearRect(0, 0, w, h);
5105
5196
  staticSurface.ctx.drawImage(cache5.canvas, srcX, srcY, w, h, 0, 0, w, h);
5106
5197
  };
5198
+ const snapshotCacheCamera = (cache5) => ({
5199
+ camX: cacheCamX,
5200
+ camY: cacheCamY,
5201
+ camZ: cacheCamZ,
5202
+ widthDevicePx: cache5.canvas.width,
5203
+ heightDevicePx: cache5.canvas.height,
5204
+ dpr: cache5.dpr,
5205
+ marginCssPx: SCENE_CACHE_MARGIN_PX
5206
+ });
5207
+ const snapshotView = (camera) => ({
5208
+ camX: camera.x,
5209
+ camY: camera.y,
5210
+ camZ: camera.z,
5211
+ widthCssPx: staticSurface.cssWidth,
5212
+ heightCssPx: staticSurface.cssHeight
5213
+ });
5214
+ const presentStaticScaled = (camera) => {
5215
+ const cache5 = ensureCacheSurface();
5216
+ const w = staticSurface.canvas.width;
5217
+ const h = staticSurface.canvas.height;
5218
+ const { srcX, srcY, srcW, srcH } = computeCacheSourceRect(
5219
+ snapshotCacheCamera(cache5),
5220
+ snapshotView(camera)
5221
+ );
5222
+ staticSurface.ctx.setTransform(1, 0, 0, 1, 0, 0);
5223
+ staticSurface.ctx.clearRect(0, 0, w, h);
5224
+ staticSurface.ctx.drawImage(cache5.canvas, srcX, srcY, srcW, srcH, 0, 0, w, h);
5225
+ };
5226
+ const SCALED_BLIT_MAX_RATIO = 4;
5227
+ const SCALED_EXTEND_MIN_RATIO = 0.5;
5107
5228
  const paintStatic = () => {
5108
5229
  const camera = store.getCamera();
5109
5230
  if (!cacheStale && camera.z === cacheCamZ) {
5110
5231
  if (viewportFitsInCache(camera)) {
5111
5232
  presentStatic(camera);
5233
+ lastDrawPath = "present";
5112
5234
  return;
5113
5235
  }
5114
5236
  if (canExtend(camera)) {
5115
5237
  extendCache(camera);
5116
5238
  presentStatic(camera);
5239
+ lastDrawPath = "extend";
5240
+ return;
5241
+ }
5242
+ }
5243
+ if (!cacheStale && camera.z !== cacheCamZ && store.getInteractionState().mode === "zooming" && cacheSurface) {
5244
+ const cacheCam = snapshotCacheCamera(cacheSurface);
5245
+ const view = snapshotView(camera);
5246
+ if (scaleRatioInBounds(cacheCam.camZ, view.camZ, SCALED_BLIT_MAX_RATIO) && cacheCoversViewport(cacheCam, view)) {
5247
+ presentStaticScaled(camera);
5248
+ lastDrawPath = "scaled";
5117
5249
  return;
5118
5250
  }
5251
+ if (zoomExtendRatioInBounds(cacheCam.camZ, view.camZ, SCALED_EXTEND_MIN_RATIO)) {
5252
+ const layout = cacheReuseLayout(cacheCam, view);
5253
+ if (layout.valid) {
5254
+ extendCacheScaled(camera, layout);
5255
+ presentStatic(camera);
5256
+ lastDrawPath = "scaled-extend";
5257
+ return;
5258
+ }
5259
+ }
5119
5260
  }
5120
5261
  renderFullCache(camera);
5121
5262
  presentStatic(camera);
5263
+ lastDrawPath = "full";
5122
5264
  };
5123
5265
  const paintCustomCanvasFallback = (ctx, node, def, drawScale, env) => {
5124
5266
  if (def.getSnapshot) {
@@ -5441,9 +5583,11 @@ var createRenderer = (opts) => {
5441
5583
  };
5442
5584
  const onInteractionChange = (state) => {
5443
5585
  interactiveDirty = true;
5444
- if (state.mode === "dragging" || state.mode === "resizing" || state.mode === "rotating" || state.mode === "panning" || state.mode === "zooming" || state.mode === "idle") {
5586
+ if (state.mode === "dragging" || state.mode === "resizing" || state.mode === "rotating" || state.mode === "panning" || state.mode === "idle") {
5445
5587
  staticDirty = true;
5446
5588
  cacheStale = true;
5589
+ } else if (state.mode === "zooming") {
5590
+ staticDirty = true;
5447
5591
  }
5448
5592
  loop.requestFrame();
5449
5593
  };
@@ -5507,6 +5651,7 @@ var createRenderer = (opts) => {
5507
5651
  },
5508
5652
  stats: () => loop.stats(),
5509
5653
  lastDrawCount: () => lastDrawn,
5654
+ getLastDrawPath: () => lastDrawPath,
5510
5655
  getOverlaySet: () => [...overlaySet],
5511
5656
  getAssetCache: () => assetCache,
5512
5657
  dispose() {