@canvas-harness/core 0.1.20 → 0.1.22

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;
@@ -4893,6 +4947,8 @@ var createRenderer = (opts) => {
4893
4947
  paintBackground(surface.ctx, { viewport, zoom: camera.z, background });
4894
4948
  const visible = visibleNodes(camera, viewport);
4895
4949
  const isMoving2 = isMoving(interaction);
4950
+ const viewMotion = interaction.mode === "panning" || interaction.mode === "zooming" || interaction.mode === "marqueeing";
4951
+ const isStripRender = !fullRender;
4896
4952
  const minOnScreen = MIN_ON_SCREEN_SIZE_PX;
4897
4953
  const nextOverlaySet = /* @__PURE__ */ new Set();
4898
4954
  let drawn = 0;
@@ -4905,9 +4961,8 @@ var createRenderer = (opts) => {
4905
4961
  theme: (token) => theme ? theme(token) : void 0
4906
4962
  };
4907
4963
  const editingNodeId = interaction.editingTarget?.kind === "node" ? interaction.editingTarget.id : null;
4908
- const cameraIsMoving = interaction.mode === "panning" || interaction.mode === "zooming";
4909
4964
  const movingNodeCount = excludedNodes?.size ?? 0;
4910
- const roughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
4965
+ const roughEnabled = movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
4911
4966
  if (!hideFrames) {
4912
4967
  for (const node of visible) {
4913
4968
  if (node.type !== "frame") continue;
@@ -4982,7 +5037,7 @@ var createRenderer = (opts) => {
4982
5037
  if (!def) continue;
4983
5038
  if (node.w * camera.z < minOnScreen && node.h * camera.z < minOnScreen) continue;
4984
5039
  if (camera.z < def.lod.minZoomForPlaceholder) continue;
4985
- const preferCanvas = camera.z < def.lod.minZoomForReact || isMoving2;
5040
+ const preferCanvas = camera.z < def.lod.minZoomForReact || viewMotion && isStripRender && !overlaySet.has(node.id);
4986
5041
  if (preferCanvas) {
4987
5042
  if (paintCustomCanvasFallback(surface.ctx, node, def, scale, renderEnv)) {
4988
5043
  drawn++;
@@ -5003,7 +5058,7 @@ var createRenderer = (opts) => {
5003
5058
  }
5004
5059
  }
5005
5060
  const visEdges = visibleEdges(viewport);
5006
- const edgeRoughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visEdges.length <= ROUGH_MAX_NODES;
5061
+ const edgeRoughEnabled = movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visEdges.length <= ROUGH_MAX_NODES;
5007
5062
  for (const edge of visEdges) {
5008
5063
  if (excludedEdges?.has(edge.id)) continue;
5009
5064
  paintOneEdge(surface.ctx, edge, scale, edgeRoughEnabled, camera.z, isMoving2);
@@ -5083,6 +5138,44 @@ var createRenderer = (opts) => {
5083
5138
  if (hw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, hx, 0, hw, cacheH);
5084
5139
  if (vh > 0 && vw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, vx, vy, vw, vh);
5085
5140
  };
5141
+ let scaledExtendScratch = null;
5142
+ const extendCacheScaled = (camera, layout) => {
5143
+ const cache5 = ensureCacheSurface();
5144
+ const cacheW = cache5.canvas.width;
5145
+ const cacheH = cache5.canvas.height;
5146
+ if (!scaledExtendScratch || scaledExtendScratch.width !== cacheW || scaledExtendScratch.height !== cacheH) {
5147
+ scaledExtendScratch = document.createElement("canvas");
5148
+ scaledExtendScratch.width = cacheW;
5149
+ scaledExtendScratch.height = cacheH;
5150
+ }
5151
+ const sctx = scaledExtendScratch.getContext("2d");
5152
+ sctx.setTransform(1, 0, 0, 1, 0, 0);
5153
+ sctx.clearRect(0, 0, cacheW, cacheH);
5154
+ sctx.drawImage(cache5.canvas, 0, 0);
5155
+ cache5.ctx.setTransform(1, 0, 0, 1, 0, 0);
5156
+ cache5.ctx.clearRect(0, 0, cacheW, cacheH);
5157
+ cache5.ctx.drawImage(
5158
+ scaledExtendScratch,
5159
+ layout.dest.x,
5160
+ layout.dest.y,
5161
+ layout.dest.w,
5162
+ layout.dest.h
5163
+ );
5164
+ cacheCamX = camera.x;
5165
+ cacheCamY = camera.y;
5166
+ cacheCamZ = camera.z;
5167
+ const strips = [
5168
+ layout.strips.top,
5169
+ layout.strips.bottom,
5170
+ layout.strips.left,
5171
+ layout.strips.right
5172
+ ];
5173
+ for (const s of strips) {
5174
+ if (s.w > 0 && s.h > 0) {
5175
+ renderCacheStrip(cache5, camera.x, camera.y, camera.z, s.x, s.y, s.w, s.h);
5176
+ }
5177
+ }
5178
+ };
5086
5179
  const cacheSourceOffset = (camera) => {
5087
5180
  const dpr = staticSurface.dpr;
5088
5181
  return {
@@ -5104,21 +5197,72 @@ var createRenderer = (opts) => {
5104
5197
  staticSurface.ctx.clearRect(0, 0, w, h);
5105
5198
  staticSurface.ctx.drawImage(cache5.canvas, srcX, srcY, w, h, 0, 0, w, h);
5106
5199
  };
5200
+ const snapshotCacheCamera = (cache5) => ({
5201
+ camX: cacheCamX,
5202
+ camY: cacheCamY,
5203
+ camZ: cacheCamZ,
5204
+ widthDevicePx: cache5.canvas.width,
5205
+ heightDevicePx: cache5.canvas.height,
5206
+ dpr: cache5.dpr,
5207
+ marginCssPx: SCENE_CACHE_MARGIN_PX
5208
+ });
5209
+ const snapshotView = (camera) => ({
5210
+ camX: camera.x,
5211
+ camY: camera.y,
5212
+ camZ: camera.z,
5213
+ widthCssPx: staticSurface.cssWidth,
5214
+ heightCssPx: staticSurface.cssHeight
5215
+ });
5216
+ const presentStaticScaled = (camera) => {
5217
+ const cache5 = ensureCacheSurface();
5218
+ const w = staticSurface.canvas.width;
5219
+ const h = staticSurface.canvas.height;
5220
+ const { srcX, srcY, srcW, srcH } = computeCacheSourceRect(
5221
+ snapshotCacheCamera(cache5),
5222
+ snapshotView(camera)
5223
+ );
5224
+ staticSurface.ctx.setTransform(1, 0, 0, 1, 0, 0);
5225
+ staticSurface.ctx.clearRect(0, 0, w, h);
5226
+ staticSurface.ctx.drawImage(cache5.canvas, srcX, srcY, srcW, srcH, 0, 0, w, h);
5227
+ };
5228
+ const SCALED_BLIT_MAX_RATIO = 4;
5229
+ const SCALED_EXTEND_MIN_RATIO = 0.5;
5107
5230
  const paintStatic = () => {
5108
5231
  const camera = store.getCamera();
5109
5232
  if (!cacheStale && camera.z === cacheCamZ) {
5110
5233
  if (viewportFitsInCache(camera)) {
5111
5234
  presentStatic(camera);
5235
+ lastDrawPath = "present";
5112
5236
  return;
5113
5237
  }
5114
5238
  if (canExtend(camera)) {
5115
5239
  extendCache(camera);
5116
5240
  presentStatic(camera);
5241
+ lastDrawPath = "extend";
5242
+ return;
5243
+ }
5244
+ }
5245
+ if (!cacheStale && camera.z !== cacheCamZ && store.getInteractionState().mode === "zooming" && cacheSurface) {
5246
+ const cacheCam = snapshotCacheCamera(cacheSurface);
5247
+ const view = snapshotView(camera);
5248
+ if (scaleRatioInBounds(cacheCam.camZ, view.camZ, SCALED_BLIT_MAX_RATIO) && cacheCoversViewport(cacheCam, view)) {
5249
+ presentStaticScaled(camera);
5250
+ lastDrawPath = "scaled";
5117
5251
  return;
5118
5252
  }
5253
+ if (zoomExtendRatioInBounds(cacheCam.camZ, view.camZ, SCALED_EXTEND_MIN_RATIO)) {
5254
+ const layout = cacheReuseLayout(cacheCam, view);
5255
+ if (layout.valid) {
5256
+ extendCacheScaled(camera, layout);
5257
+ presentStatic(camera);
5258
+ lastDrawPath = "scaled-extend";
5259
+ return;
5260
+ }
5261
+ }
5119
5262
  }
5120
5263
  renderFullCache(camera);
5121
5264
  presentStatic(camera);
5265
+ lastDrawPath = "full";
5122
5266
  };
5123
5267
  const paintCustomCanvasFallback = (ctx, node, def, drawScale, env) => {
5124
5268
  if (def.getSnapshot) {
@@ -5441,9 +5585,11 @@ var createRenderer = (opts) => {
5441
5585
  };
5442
5586
  const onInteractionChange = (state) => {
5443
5587
  interactiveDirty = true;
5444
- if (state.mode === "dragging" || state.mode === "resizing" || state.mode === "rotating" || state.mode === "panning" || state.mode === "zooming" || state.mode === "idle") {
5588
+ if (state.mode === "dragging" || state.mode === "resizing" || state.mode === "rotating" || state.mode === "panning" || state.mode === "idle") {
5445
5589
  staticDirty = true;
5446
5590
  cacheStale = true;
5591
+ } else if (state.mode === "zooming") {
5592
+ staticDirty = true;
5447
5593
  }
5448
5594
  loop.requestFrame();
5449
5595
  };
@@ -5507,6 +5653,7 @@ var createRenderer = (opts) => {
5507
5653
  },
5508
5654
  stats: () => loop.stats(),
5509
5655
  lastDrawCount: () => lastDrawn,
5656
+ getLastDrawPath: () => lastDrawPath,
5510
5657
  getOverlaySet: () => [...overlaySet],
5511
5658
  getAssetCache: () => assetCache,
5512
5659
  dispose() {