@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.cjs CHANGED
@@ -4754,6 +4754,59 @@ var buildPath = (type, x, y, w, h, radius) => {
4754
4754
  }
4755
4755
  };
4756
4756
 
4757
+ // src/render/scene-cache-math.ts
4758
+ var computeCacheSourceRect = (cache5, view) => {
4759
+ const ratio = cache5.camZ / view.camZ;
4760
+ const srcX = Math.round(((view.camX - cache5.camX) * cache5.camZ + cache5.marginCssPx) * cache5.dpr);
4761
+ const srcY = Math.round(((view.camY - cache5.camY) * cache5.camZ + cache5.marginCssPx) * cache5.dpr);
4762
+ const srcW = view.widthCssPx * ratio * cache5.dpr;
4763
+ const srcH = view.heightCssPx * ratio * cache5.dpr;
4764
+ return { srcX, srcY, srcW, srcH };
4765
+ };
4766
+ var cacheCoversViewport = (cache5, view) => {
4767
+ const { srcX, srcY, srcW, srcH } = computeCacheSourceRect(cache5, view);
4768
+ return srcX >= 0 && srcY >= 0 && srcX + srcW <= cache5.widthDevicePx && srcY + srcH <= cache5.heightDevicePx;
4769
+ };
4770
+ var scaleRatioInBounds = (cacheCamZ, viewCamZ, maxRatio) => {
4771
+ if (viewCamZ <= 0 || cacheCamZ <= 0 || maxRatio <= 0) return false;
4772
+ const ratio = viewCamZ >= cacheCamZ ? viewCamZ / cacheCamZ : cacheCamZ / viewCamZ;
4773
+ return ratio <= maxRatio;
4774
+ };
4775
+ var cacheReuseLayout = (cache5, view) => {
4776
+ const ratio = view.camZ / cache5.camZ;
4777
+ const cacheW = cache5.widthDevicePx;
4778
+ const cacheH = cache5.heightDevicePx;
4779
+ const marginDev = cache5.marginCssPx * cache5.dpr;
4780
+ const destW = cacheW * ratio;
4781
+ const destH = cacheH * ratio;
4782
+ const destX = (cache5.camX - view.camX) * view.camZ * cache5.dpr + marginDev * (1 - ratio);
4783
+ const destY = (cache5.camY - view.camY) * view.camZ * cache5.dpr + marginDev * (1 - ratio);
4784
+ const dest = { x: destX, y: destY, w: destW, h: destH };
4785
+ const strips = {
4786
+ top: { x: 0, y: 0, w: cacheW, h: Math.max(0, destY) },
4787
+ bottom: {
4788
+ x: 0,
4789
+ y: destY + destH,
4790
+ w: cacheW,
4791
+ h: Math.max(0, cacheH - destY - destH)
4792
+ },
4793
+ left: { x: 0, y: destY, w: Math.max(0, destX), h: destH },
4794
+ right: {
4795
+ x: destX + destW,
4796
+ y: destY,
4797
+ w: Math.max(0, cacheW - destX - destW),
4798
+ h: destH
4799
+ }
4800
+ };
4801
+ const valid = destX >= 0 && destY >= 0 && destX + destW <= cacheW && destY + destH <= cacheH;
4802
+ return { dest, strips, valid };
4803
+ };
4804
+ var zoomExtendRatioInBounds = (cacheCamZ, viewCamZ, minRatio) => {
4805
+ if (viewCamZ <= 0 || cacheCamZ <= 0 || minRatio <= 0 || minRatio >= 1) return false;
4806
+ const ratio = viewCamZ / cacheCamZ;
4807
+ return ratio >= minRatio && ratio < 1;
4808
+ };
4809
+
4757
4810
  // src/render/shapes/content-bounds.ts
4758
4811
  var SQRT2_INV = 1 / Math.SQRT2;
4759
4812
  var contentBounds = (node) => {
@@ -4838,6 +4891,7 @@ var createRenderer = (opts) => {
4838
4891
  let interactiveDirty = false;
4839
4892
  let overlaySet = /* @__PURE__ */ new Set();
4840
4893
  let lastDrawn = 0;
4894
+ let lastDrawPath = "idle";
4841
4895
  let cacheSurface = null;
4842
4896
  let cacheCamX = 0;
4843
4897
  let cacheCamY = 0;
@@ -4895,6 +4949,8 @@ var createRenderer = (opts) => {
4895
4949
  paintBackground(surface.ctx, { viewport, zoom: camera.z, background });
4896
4950
  const visible = visibleNodes(camera, viewport);
4897
4951
  const isMoving2 = isMoving(interaction);
4952
+ const viewMotion = interaction.mode === "panning" || interaction.mode === "zooming" || interaction.mode === "marqueeing";
4953
+ const isStripRender = !fullRender;
4898
4954
  const minOnScreen = MIN_ON_SCREEN_SIZE_PX;
4899
4955
  const nextOverlaySet = /* @__PURE__ */ new Set();
4900
4956
  let drawn = 0;
@@ -4907,9 +4963,8 @@ var createRenderer = (opts) => {
4907
4963
  theme: (token) => theme ? theme(token) : void 0
4908
4964
  };
4909
4965
  const editingNodeId = interaction.editingTarget?.kind === "node" ? interaction.editingTarget.id : null;
4910
- const cameraIsMoving = interaction.mode === "panning" || interaction.mode === "zooming";
4911
4966
  const movingNodeCount = excludedNodes?.size ?? 0;
4912
- const roughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
4967
+ const roughEnabled = movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
4913
4968
  if (!hideFrames) {
4914
4969
  for (const node of visible) {
4915
4970
  if (node.type !== "frame") continue;
@@ -4984,7 +5039,7 @@ var createRenderer = (opts) => {
4984
5039
  if (!def) continue;
4985
5040
  if (node.w * camera.z < minOnScreen && node.h * camera.z < minOnScreen) continue;
4986
5041
  if (camera.z < def.lod.minZoomForPlaceholder) continue;
4987
- const preferCanvas = camera.z < def.lod.minZoomForReact || isMoving2;
5042
+ const preferCanvas = camera.z < def.lod.minZoomForReact || viewMotion && isStripRender && !overlaySet.has(node.id);
4988
5043
  if (preferCanvas) {
4989
5044
  if (paintCustomCanvasFallback(surface.ctx, node, def, scale, renderEnv)) {
4990
5045
  drawn++;
@@ -5005,7 +5060,7 @@ var createRenderer = (opts) => {
5005
5060
  }
5006
5061
  }
5007
5062
  const visEdges = visibleEdges(viewport);
5008
- const edgeRoughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visEdges.length <= ROUGH_MAX_NODES;
5063
+ const edgeRoughEnabled = movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visEdges.length <= ROUGH_MAX_NODES;
5009
5064
  for (const edge of visEdges) {
5010
5065
  if (excludedEdges?.has(edge.id)) continue;
5011
5066
  paintOneEdge(surface.ctx, edge, scale, edgeRoughEnabled, camera.z, isMoving2);
@@ -5085,6 +5140,44 @@ var createRenderer = (opts) => {
5085
5140
  if (hw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, hx, 0, hw, cacheH);
5086
5141
  if (vh > 0 && vw > 0) renderCacheStrip(cache5, newCamX, newCamY, camera.z, vx, vy, vw, vh);
5087
5142
  };
5143
+ let scaledExtendScratch = null;
5144
+ const extendCacheScaled = (camera, layout) => {
5145
+ const cache5 = ensureCacheSurface();
5146
+ const cacheW = cache5.canvas.width;
5147
+ const cacheH = cache5.canvas.height;
5148
+ if (!scaledExtendScratch || scaledExtendScratch.width !== cacheW || scaledExtendScratch.height !== cacheH) {
5149
+ scaledExtendScratch = document.createElement("canvas");
5150
+ scaledExtendScratch.width = cacheW;
5151
+ scaledExtendScratch.height = cacheH;
5152
+ }
5153
+ const sctx = scaledExtendScratch.getContext("2d");
5154
+ sctx.setTransform(1, 0, 0, 1, 0, 0);
5155
+ sctx.clearRect(0, 0, cacheW, cacheH);
5156
+ sctx.drawImage(cache5.canvas, 0, 0);
5157
+ cache5.ctx.setTransform(1, 0, 0, 1, 0, 0);
5158
+ cache5.ctx.clearRect(0, 0, cacheW, cacheH);
5159
+ cache5.ctx.drawImage(
5160
+ scaledExtendScratch,
5161
+ layout.dest.x,
5162
+ layout.dest.y,
5163
+ layout.dest.w,
5164
+ layout.dest.h
5165
+ );
5166
+ cacheCamX = camera.x;
5167
+ cacheCamY = camera.y;
5168
+ cacheCamZ = camera.z;
5169
+ const strips = [
5170
+ layout.strips.top,
5171
+ layout.strips.bottom,
5172
+ layout.strips.left,
5173
+ layout.strips.right
5174
+ ];
5175
+ for (const s of strips) {
5176
+ if (s.w > 0 && s.h > 0) {
5177
+ renderCacheStrip(cache5, camera.x, camera.y, camera.z, s.x, s.y, s.w, s.h);
5178
+ }
5179
+ }
5180
+ };
5088
5181
  const cacheSourceOffset = (camera) => {
5089
5182
  const dpr = staticSurface.dpr;
5090
5183
  return {
@@ -5106,21 +5199,72 @@ var createRenderer = (opts) => {
5106
5199
  staticSurface.ctx.clearRect(0, 0, w, h);
5107
5200
  staticSurface.ctx.drawImage(cache5.canvas, srcX, srcY, w, h, 0, 0, w, h);
5108
5201
  };
5202
+ const snapshotCacheCamera = (cache5) => ({
5203
+ camX: cacheCamX,
5204
+ camY: cacheCamY,
5205
+ camZ: cacheCamZ,
5206
+ widthDevicePx: cache5.canvas.width,
5207
+ heightDevicePx: cache5.canvas.height,
5208
+ dpr: cache5.dpr,
5209
+ marginCssPx: SCENE_CACHE_MARGIN_PX
5210
+ });
5211
+ const snapshotView = (camera) => ({
5212
+ camX: camera.x,
5213
+ camY: camera.y,
5214
+ camZ: camera.z,
5215
+ widthCssPx: staticSurface.cssWidth,
5216
+ heightCssPx: staticSurface.cssHeight
5217
+ });
5218
+ const presentStaticScaled = (camera) => {
5219
+ const cache5 = ensureCacheSurface();
5220
+ const w = staticSurface.canvas.width;
5221
+ const h = staticSurface.canvas.height;
5222
+ const { srcX, srcY, srcW, srcH } = computeCacheSourceRect(
5223
+ snapshotCacheCamera(cache5),
5224
+ snapshotView(camera)
5225
+ );
5226
+ staticSurface.ctx.setTransform(1, 0, 0, 1, 0, 0);
5227
+ staticSurface.ctx.clearRect(0, 0, w, h);
5228
+ staticSurface.ctx.drawImage(cache5.canvas, srcX, srcY, srcW, srcH, 0, 0, w, h);
5229
+ };
5230
+ const SCALED_BLIT_MAX_RATIO = 4;
5231
+ const SCALED_EXTEND_MIN_RATIO = 0.5;
5109
5232
  const paintStatic = () => {
5110
5233
  const camera = store.getCamera();
5111
5234
  if (!cacheStale && camera.z === cacheCamZ) {
5112
5235
  if (viewportFitsInCache(camera)) {
5113
5236
  presentStatic(camera);
5237
+ lastDrawPath = "present";
5114
5238
  return;
5115
5239
  }
5116
5240
  if (canExtend(camera)) {
5117
5241
  extendCache(camera);
5118
5242
  presentStatic(camera);
5243
+ lastDrawPath = "extend";
5244
+ return;
5245
+ }
5246
+ }
5247
+ if (!cacheStale && camera.z !== cacheCamZ && store.getInteractionState().mode === "zooming" && cacheSurface) {
5248
+ const cacheCam = snapshotCacheCamera(cacheSurface);
5249
+ const view = snapshotView(camera);
5250
+ if (scaleRatioInBounds(cacheCam.camZ, view.camZ, SCALED_BLIT_MAX_RATIO) && cacheCoversViewport(cacheCam, view)) {
5251
+ presentStaticScaled(camera);
5252
+ lastDrawPath = "scaled";
5119
5253
  return;
5120
5254
  }
5255
+ if (zoomExtendRatioInBounds(cacheCam.camZ, view.camZ, SCALED_EXTEND_MIN_RATIO)) {
5256
+ const layout = cacheReuseLayout(cacheCam, view);
5257
+ if (layout.valid) {
5258
+ extendCacheScaled(camera, layout);
5259
+ presentStatic(camera);
5260
+ lastDrawPath = "scaled-extend";
5261
+ return;
5262
+ }
5263
+ }
5121
5264
  }
5122
5265
  renderFullCache(camera);
5123
5266
  presentStatic(camera);
5267
+ lastDrawPath = "full";
5124
5268
  };
5125
5269
  const paintCustomCanvasFallback = (ctx, node, def, drawScale, env) => {
5126
5270
  if (def.getSnapshot) {
@@ -5443,9 +5587,11 @@ var createRenderer = (opts) => {
5443
5587
  };
5444
5588
  const onInteractionChange = (state) => {
5445
5589
  interactiveDirty = true;
5446
- if (state.mode === "dragging" || state.mode === "resizing" || state.mode === "rotating" || state.mode === "panning" || state.mode === "zooming" || state.mode === "idle") {
5590
+ if (state.mode === "dragging" || state.mode === "resizing" || state.mode === "rotating" || state.mode === "panning" || state.mode === "idle") {
5447
5591
  staticDirty = true;
5448
5592
  cacheStale = true;
5593
+ } else if (state.mode === "zooming") {
5594
+ staticDirty = true;
5449
5595
  }
5450
5596
  loop.requestFrame();
5451
5597
  };
@@ -5509,6 +5655,7 @@ var createRenderer = (opts) => {
5509
5655
  },
5510
5656
  stats: () => loop.stats(),
5511
5657
  lastDrawCount: () => lastDrawn,
5658
+ getLastDrawPath: () => lastDrawPath,
5512
5659
  getOverlaySet: () => [...overlaySet],
5513
5660
  getAssetCache: () => assetCache,
5514
5661
  dispose() {