fix: Overdraw lookback

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs
2026-04-14 16:00:37 +00:00
parent 25ec8d0869
commit 400d98d95d
+173 -121
View File
@@ -146,6 +146,13 @@ export interface ExcalidrawElementWithCanvas {
boundTextCanvas: HTMLCanvasElement; boundTextCanvas: HTMLCanvasElement;
canvasOriginSceneX?: number; canvasOriginSceneX?: number;
canvasOriginSceneY?: number; canvasOriginSceneY?: number;
/**
* Tip canvas for incremental freedraw rendering. Contains only the last
* unfinalised segment (whose Catmull-Rom right-hand tangent changes with
* each new point) and is cleared + redrawn every frame. Composited on top
* of `canvas` (the committed accumulation canvas) in drawElementFromCanvas.
*/
tipCanvas?: HTMLCanvasElement;
} }
const cappedElementCanvasSize = ( const cappedElementCanvasSize = (
@@ -410,7 +417,7 @@ const drawTaperedCapsule = (
const r = Math.max(r0, r1); const r = Math.max(r0, r1);
if (len < r / 2) { if (len < r / 2) {
// Degenerate segment draw a filled circle at the larger radius // Degenerate segment - draw a filled circle at the larger radius
context.beginPath(); context.beginPath();
context.arc((x0 + x1) / 2, (y0 + y1) / 2, r, 0, Math.PI * 2); context.arc((x0 + x1) / 2, (y0 + y1) / 2, r, 0, Math.PI * 2);
context.fill(); context.fill();
@@ -424,11 +431,11 @@ const drawTaperedCapsule = (
context.beginPath(); context.beginPath();
// Back semicircle at P0: clockwise from (P0 + perp*r0) through (back of P0) to (P0 - perp*r0) // Back semicircle at P0: clockwise from (P0 + perp*r0) through (back of P0) to (P0 - perp*r0)
context.arc(x0, y0, r0, angle + Math.PI / 2, angle - Math.PI / 2, false); context.arc(x0, y0, r0, angle + Math.PI / 2, angle - Math.PI / 2, false);
// Neg-perp side: P0 - perp*r0 P1 - perp*r1 (arc endpoint is already P0 - perp*r0) // Neg-perp side: P0 - perp*r0 -> P1 - perp*r1 (arc endpoint is already P0 - perp*r0)
context.lineTo(x1 - px * r1, y1 - py * r1); context.lineTo(x1 - px * r1, y1 - py * r1);
// Front semicircle at P1: clockwise from (P1 - perp*r1) through (front of P1) to (P1 + perp*r1) // Front semicircle at P1: clockwise from (P1 - perp*r1) through (front of P1) to (P1 + perp*r1)
context.arc(x1, y1, r1, angle - Math.PI / 2, angle + Math.PI / 2, false); context.arc(x1, y1, r1, angle - Math.PI / 2, angle + Math.PI / 2, false);
// Perp side: P1 + perp*r1 P0 + perp*r0 // Perp side: P1 + perp*r1 -> P0 + perp*r0
context.lineTo(x0 + px * r0, y0 + py * r0); context.lineTo(x0 + px * r0, y0 + py * r0);
context.closePath(); context.closePath();
context.fill(); context.fill();
@@ -453,14 +460,11 @@ const PRESSURE_SMOOTHING_RADIUS = 6;
/** /**
* Returns the Catmull-Rom tangent vector at points[i], using the neighbouring * Returns the Catmull-Rom tangent vector at points[i], using the neighbouring
* points for a uniform parameterisation. At the first point a one-sided * points for a uniform parameterisation. At the first point a one-sided
* forward tangent is used. At the last real point, `predictedPoint` (if * forward tangent is used.
* supplied) stands in for the missing next neighbour so the stroke tip curves
* smoothly toward the predicted pen position.
*/ */
const getCatmullRomTangent = ( const getCatmullRomTangent = (
points: readonly (readonly [number, number])[], points: readonly (readonly [number, number])[],
i: number, i: number,
predictedPoint: readonly [number, number] | undefined,
): [number, number] => { ): [number, number] => {
const N = points.length; const N = points.length;
const cur = points[i]; const cur = points[i];
@@ -469,8 +473,6 @@ const getCatmullRomTangent = (
let next: readonly [number, number]; let next: readonly [number, number];
if (i < N - 1) { if (i < N - 1) {
next = points[i + 1]; next = points[i + 1];
} else if (predictedPoint !== undefined) {
next = predictedPoint;
} else { } else {
// Mirror back across cur to get a forward tangent at the last point. // Mirror back across cur to get a forward tangent at the last point.
const prev2 = i > 0 ? points[i - 1] : cur; const prev2 = i > 0 ? points[i - 1] : cur;
@@ -576,21 +578,23 @@ const drawSubdividedSegment = (
* Draws freedraw points as bezier-subdivided, pressure-aware tapered capsule * Draws freedraw points as bezier-subdivided, pressure-aware tapered capsule
* segments. Consecutive real points are connected with Catmull-Rom cubic * segments. Consecutive real points are connected with Catmull-Rom cubic
* bezier curves so the rendered stroke is smooth even when input samples are * bezier curves so the rendered stroke is smooth even when input samples are
* sparse. When `predictedPoint` is supplied it is used as the tangent hint at * sparse.
* the tip of the stroke and an additional ghost segment is drawn toward it,
* compensating for pointer-event latency.
* *
* @param fromIndex Draw segments starting from this index (inclusive). * @param fromIndex Draw segments starting from this point index (inclusive).
* Pass 0 to draw everything. * Pass 0 to draw from the beginning.
* @param predictedPoint Element-local scene coords of the first predicted * @param upToIndex Draw segments only up to (but not including) this point
* pointer event, used for tangent and ghost rendering. * index. Omit or pass `undefined` to draw all remaining
* points. Used by the incremental canvas to stop short of
* the last segment so the committed canvas only contains
* segments whose Catmull-Rom tangents are fully finalised
* (i.e. the right-hand neighbour is known).
*/ */
const drawFreeDrawSegments = ( const drawFreeDrawSegments = (
element: ExcalidrawFreeDrawElement, element: ExcalidrawFreeDrawElement,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,
fromIndex: number, fromIndex: number,
predictedPoint?: readonly [number, number], upToIndex?: number,
) => { ) => {
const { points, pressures } = element; const { points, pressures } = element;
const N = points.length; const N = points.length;
@@ -628,23 +632,23 @@ const drawFreeDrawSegments = (
return totalWeight > 0 ? sum / totalWeight : DEFAULT_FREEDRAW_PRESSURE; return totalWeight > 0 ? sum / totalWeight : DEFAULT_FREEDRAW_PRESSURE;
}; };
if (fromIndex === 0 && N === 1) { if (
// Single-point stroke → filled circle (dot) fromIndex === 0 &&
N === 1 &&
(upToIndex === undefined || upToIndex >= 1)
) {
// Single-point stroke -> filled circle (dot)
const r = baseRadius * getSmoothedPressure(0) * 2; const r = baseRadius * getSmoothedPressure(0) * 2;
context.beginPath(); context.beginPath();
context.arc(points[0][0], points[0][1], r, 0, Math.PI * 2); context.arc(points[0][0], points[0][1], r, 0, Math.PI * 2);
context.fill(); context.fill();
// Draw ghost circle at predicted point if available
if (predictedPoint !== undefined) {
context.beginPath();
context.arc(predictedPoint[0], predictedPoint[1], r, 0, Math.PI * 2);
context.fill();
}
return; return;
} }
const end = upToIndex !== undefined ? Math.min(upToIndex, N) : N;
const start = Math.max(fromIndex, 1); const start = Math.max(fromIndex, 1);
for (let i = start; i < N; i++) { for (let i = start; i < end; i++) {
const p0 = points[i - 1]; const p0 = points[i - 1];
const p1 = points[i]; const p1 = points[i];
// Very first pressure values are often unreliable, // Very first pressure values are often unreliable,
@@ -652,15 +656,8 @@ const drawFreeDrawSegments = (
const r0 = baseRadius * getSmoothedPressure(i - 1) * 2; const r0 = baseRadius * getSmoothedPressure(i - 1) * 2;
const r1 = baseRadius * getSmoothedPressure(i) * 2; const r1 = baseRadius * getSmoothedPressure(i) * 2;
// Catmull-Rom tangents. At the last real point, use predictedPoint as const t0 = getCatmullRomTangent(points, i - 1);
// the look-ahead so the tip curves smoothly toward the expected position. const t1 = getCatmullRomTangent(points, i);
const isLastPoint = i === N - 1;
const t0 = getCatmullRomTangent(points, i - 1, undefined);
const t1 = getCatmullRomTangent(
points,
i,
isLastPoint ? predictedPoint : undefined,
);
drawSubdividedSegment( drawSubdividedSegment(
context, context,
@@ -676,34 +673,6 @@ const drawFreeDrawSegments = (
t1[1], t1[1],
); );
} }
// Ghost segment: extend the visible stroke toward the predicted point to
// compensate for pointer-event latency. This segment is overwritten when
// the next real pointer event arrives.
if (predictedPoint !== undefined && N >= 1) {
const lastPt = points[N - 1];
const r0 = baseRadius * getSmoothedPressure(N - 1) * 2;
// Tangent at the last real point (with predicted look-ahead)
const t0 = getCatmullRomTangent(points, N - 1, predictedPoint);
// Forward tangent at the predicted point itself
const fwdX = predictedPoint[0] - lastPt[0];
const fwdY = predictedPoint[1] - lastPt[1];
drawSubdividedSegment(
context,
lastPt[0],
lastPt[1],
r0,
predictedPoint[0],
predictedPoint[1],
r0, // hold pressure at the tip
t0[0],
t0[1],
fwdX,
fwdY,
);
}
}; };
const drawElementOnCanvas = ( const drawElementOnCanvas = (
@@ -917,7 +886,7 @@ export const elementWithCanvasCache = new WeakMap<
// accumulates new capsule segments without full regeneration on every added // accumulates new capsule segments without full regeneration on every added
// point. // point.
// screen pixels minimum extra lookahead space on each side // screen pixels - minimum extra lookahead space on each side
// (divided by scale at use) // (divided by scale at use)
const FREEDRAW_CANVAS_OVERSHOOT_MIN = 200; const FREEDRAW_CANVAS_OVERSHOOT_MIN = 200;
@@ -925,8 +894,31 @@ const FREEDRAW_CANVAS_OVERSHOOT_MIN = 200;
const FREEDRAW_CANVAS_OVERSHOOT_FACTOR = 0.5; const FREEDRAW_CANVAS_OVERSHOOT_FACTOR = 0.5;
interface FreeDrawIncrementalCanvas { interface FreeDrawIncrementalCanvas {
canvas: HTMLCanvasElement; /**
lastRenderedPointCount: number; * Accumulation canvas - contains all segments whose Catmull-Rom tangents are
* fully finalised (right-hand neighbour is known). With N points the last
* finalised segment ends at index `committedPointCount - 1`, meaning segment
* `[committedPointCount-2 -> committedPointCount-1]` has been drawn with the
* correct tangent at `committedPointCount-1` (since point
* `committedPointCount` existed when it was drawn). Never cleared; only
* appended to (or copied when bounds grow).
*/
committedCanvas: HTMLCanvasElement;
/**
* Tip canvas - same pixel dimensions and scene origin as `committedCanvas`.
* Cleared and redrawn every frame to contain only the last segment
* `[committedPointCount-1 -> N-1]` whose tangent at `N-1` is still
* provisional (no right-hand neighbour yet). Composited on top of
* `committedCanvas` at display time.
*/
tipCanvas: HTMLCanvasElement;
/**
* Number of points that have been permanently drawn on `committedCanvas`.
* The committed canvas contains segments through point index
* `committedPointCount - 1` with final tangents. Always lags the current
* point count by 1 (the tip holds the last unfinalisable segment).
*/
committedPointCount: number;
canvasOriginSceneX: number; canvasOriginSceneX: number;
canvasOriginSceneY: number; canvasOriginSceneY: number;
canvasAllocX1: number; canvasAllocX1: number;
@@ -943,13 +935,26 @@ const freedrawIncrementalCache = new WeakMap<
>(); >();
/** /**
* Generates or incrementally updates a raster canvas for a freedraw element * Generates or incrementally updates the two-canvas (committed + tip) raster
* that is being actively drawn. Unlike the standard element canvas cache, this * for a freedraw element being actively drawn.
* cache is NOT cleared on each mutateElement call, allowing new segments to be
* appended without full regeneration.
* *
* When element bounds grow beyond the over-allocated canvas, the existing * ## Two-canvas split
* raster is copied into a new larger canvas before appending the next segments *
* A Catmull-Rom tangent at point `i` depends on `points[i+1]`. Until
* `points[i+1]` arrives, the tangent at `i` uses a mirrored fallback and is
* therefore provisional. The segment ending at the current tip `[N-2 -> N-1]`
* is the only one with a provisional tangent.
*
* - **`committedCanvas`** - contains all segments whose tangents are final.
* With N points: segments `[0->1, ..., N-3->N-2]` (`committedPointCount =
* N-1`). This canvas is append-only; its pixels are never invalidated.
* When a new point `N` arrives, the segment `[N-2 -> N-1]` is now
* finalised (tangent at `N-1` uses `N` as the right-hand neighbour) and is
* drawn onto the committed canvas. `committedPointCount` advances to `N`.
*
* - **`tipCanvas`** - cleared and redrawn every frame to contain only the
* last provisional segment `[committedPointCount-1 -> N-1]`. Composited on
* top of `committedCanvas` at display time.
*/ */
const generateOrUpdateFreeDrawIncrementalCanvas = ( const generateOrUpdateFreeDrawIncrementalCanvas = (
element: ExcalidrawFreeDrawElement, element: ExcalidrawFreeDrawElement,
@@ -964,6 +969,7 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const containingFrameOpacity = const containingFrameOpacity =
getContainingFrame(element, elementsMap)?.opacity || 100; getContainingFrame(element, elementsMap)?.opacity || 100;
const N = element.points.length;
const prevInc = freedrawIncrementalCache.get(element); const prevInc = freedrawIncrementalCache.get(element);
@@ -980,22 +986,26 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
prevInc.scale !== scale || prevInc.scale !== scale ||
prevInc.theme !== appState.theme; prevInc.theme !== appState.theme;
let canvas: HTMLCanvasElement; // ── Canvas allocation / reallocation ──────────────────────────────────────
let committedCanvas: HTMLCanvasElement;
let tipCanvas: HTMLCanvasElement;
let canvasOriginSceneX: number; let canvasOriginSceneX: number;
let canvasOriginSceneY: number; let canvasOriginSceneY: number;
let fromIndex: number;
let canvasScale: number; let canvasScale: number;
// How many points to start the committed-canvas update from. On a full
// regen this is 0; on a bounds-exceeded realloc it is the existing committed
// count so we only append the new segments.
let committedFromIndex: number;
if (needsAlloc) { if (needsAlloc) {
// Over-allocate proportionally to the current bounding box, like std::vector doubling, // Over-allocate proportionally to the current bounding box so fast large
// so fast large strokes trigger far fewer reallocations. The over-sized canvas is // strokes trigger far fewer reallocations.
// discarded on stroke finalisation so the memory waste is only transient.
const overshootX = Math.max( const overshootX = Math.max(
FREEDRAW_CANVAS_OVERSHOOT_MIN / scale, // convert screen-pixel budget to scene units FREEDRAW_CANVAS_OVERSHOOT_MIN / scale,
(x2 - x1) * FREEDRAW_CANVAS_OVERSHOOT_FACTOR, (x2 - x1) * FREEDRAW_CANVAS_OVERSHOOT_FACTOR,
); );
const overshootY = Math.max( const overshootY = Math.max(
FREEDRAW_CANVAS_OVERSHOOT_MIN / scale, // convert screen-pixel budget to scene units FREEDRAW_CANVAS_OVERSHOOT_MIN / scale,
(y2 - y1) * FREEDRAW_CANVAS_OVERSHOOT_FACTOR, (y2 - y1) * FREEDRAW_CANVAS_OVERSHOOT_FACTOR,
); );
const allocX1 = x1 - overshootX; const allocX1 = x1 - overshootX;
@@ -1006,11 +1016,10 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
canvasOriginSceneX = allocX1 - padding / dpr; canvasOriginSceneX = allocX1 - padding / dpr;
canvasOriginSceneY = allocY1 - padding / dpr; canvasOriginSceneY = allocY1 - padding / dpr;
// Raw canvas pixels before zoom scale
const rawW = (allocX2 - allocX1) * dpr + padding * 2; const rawW = (allocX2 - allocX1) * dpr + padding * 2;
const rawH = (allocY2 - allocY1) * dpr + padding * 2; const rawH = (allocY2 - allocY1) * dpr + padding * 2;
// Respect browser canvas size limits // Respect browser canvas size limits.
const AREA_LIMIT = 16777216; const AREA_LIMIT = 16777216;
const WIDTH_HEIGHT_LIMIT = 32767; const WIDTH_HEIGHT_LIMIT = 32767;
canvasScale = scale; canvasScale = scale;
@@ -1033,10 +1042,13 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
return null; return null;
} }
canvas = document.createElement("canvas"); committedCanvas = document.createElement("canvas");
canvas.width = canvasWidth; committedCanvas.width = canvasWidth;
canvas.height = canvasHeight; committedCanvas.height = canvasHeight;
const context = canvas.getContext("2d")!;
tipCanvas = document.createElement("canvas");
tipCanvas.width = canvasWidth;
tipCanvas.height = canvasHeight;
if ( if (
prevInc !== undefined && prevInc !== undefined &&
@@ -1044,22 +1056,25 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
prevInc.scale === canvasScale && prevInc.scale === canvasScale &&
prevInc.theme === appState.theme prevInc.theme === appState.theme
) { ) {
// Copy existing raster to the new canvas at the correct pixel offset. // Bounds grew: copy committed raster to new canvas at the correct offset
// The shift is determined by how much the canvas origin moved in scene coords. // and keep accumulating. Tip will be redrawn below.
const copyX = const copyX =
(prevInc.canvasOriginSceneX - canvasOriginSceneX) * dpr * canvasScale; (prevInc.canvasOriginSceneX - canvasOriginSceneX) * dpr * canvasScale;
const copyY = const copyY =
(prevInc.canvasOriginSceneY - canvasOriginSceneY) * dpr * canvasScale; (prevInc.canvasOriginSceneY - canvasOriginSceneY) * dpr * canvasScale;
context.drawImage(prevInc.canvas, copyX, copyY); committedCanvas
fromIndex = prevInc.lastRenderedPointCount; .getContext("2d")!
.drawImage(prevInc.committedCanvas, copyX, copyY);
committedFromIndex = prevInc.committedPointCount - 1;
} else { } else {
// Full regeneration on first canvas, zoom change, or theme change // Full regeneration: zoom/theme change or first frame.
fromIndex = 0; committedFromIndex = 0;
} }
freedrawIncrementalCache.set(element, { freedrawIncrementalCache.set(element, {
canvas, committedCanvas,
lastRenderedPointCount: fromIndex, tipCanvas,
committedPointCount: committedFromIndex,
canvasOriginSceneX, canvasOriginSceneX,
canvasOriginSceneY, canvasOriginSceneY,
canvasAllocX1: allocX1, canvasAllocX1: allocX1,
@@ -1070,49 +1085,73 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
theme: appState.theme, theme: appState.theme,
}); });
} else { } else {
canvas = prevInc.canvas; committedCanvas = prevInc.committedCanvas;
tipCanvas = prevInc.tipCanvas;
canvasOriginSceneX = prevInc.canvasOriginSceneX; canvasOriginSceneX = prevInc.canvasOriginSceneX;
canvasOriginSceneY = prevInc.canvasOriginSceneY; canvasOriginSceneY = prevInc.canvasOriginSceneY;
fromIndex = prevInc.lastRenderedPointCount;
canvasScale = prevInc.scale; canvasScale = prevInc.scale;
committedFromIndex = prevInc.committedPointCount - 1;
} }
// Roll back 2 points so the Catmull-Rom look-ahead tangent at the last const inc = freedrawIncrementalCache.get(element)!;
// committed segment is redrawn correctly when a new point arrives.
// Pressure smoothing is causal (one-sided) so it requires no extra rollback.
// Overpainting with an opaque fill is harmless.
const drawFrom = Math.max(0, fromIndex - 2);
// Draw new (and possibly revised) segments plus the ghost toward the // ── Helper: draw onto a canvas with the element's scene->pixel transform ──
// predicted point. const withElementContext = (
if (drawFrom < element.points.length) { target: HTMLCanvasElement,
const ctx = canvas.getContext("2d")!; fn: (ctx: CanvasRenderingContext2D) => void,
const canvasElemOffsetX = ) => {
(element.x - canvasOriginSceneX) * dpr * canvasScale; const ctx = target.getContext("2d")!;
const canvasElemOffsetY = const offsetX = (element.x - canvasOriginSceneX) * dpr * canvasScale;
(element.y - canvasOriginSceneY) * dpr * canvasScale; const offsetY = (element.y - canvasOriginSceneY) * dpr * canvasScale;
ctx.save(); ctx.save();
ctx.translate(canvasElemOffsetX, canvasElemOffsetY); ctx.translate(offsetX, offsetY);
ctx.scale(dpr * canvasScale, dpr * canvasScale); ctx.scale(dpr * canvasScale, dpr * canvasScale);
fn(ctx);
ctx.restore();
};
// ── Update committed canvas ───────────────────────────────────────────────
// With N points the last finalisable segment ends at N-2 (needs N-1 as
// right-hand neighbour for the tangent at N-2, and N-1 is always present).
// We draw from `committedFromIndex` up to (but not including) point N-1,
// so the committed canvas contains segments [0->1, ..., N-3->N-2].
const newCommittedCount = Math.max(1, N - 1);
if (committedFromIndex < newCommittedCount) {
withElementContext(committedCanvas, (ctx) => {
drawFreeDrawSegments(
element,
ctx,
renderConfig,
committedFromIndex,
newCommittedCount, // upToIndex - stop before the last provisional segment
);
});
inc.committedPointCount = newCommittedCount;
}
// ── Redraw tip canvas ─────────────────────────────────────────────────────
// Always cleared and redrawn: contains the single provisional segment
// [committedPointCount-1 -> N-1] with a predicted-point ghost if available.
withElementContext(tipCanvas, (ctx) => {
ctx.clearRect(
-(element.x - canvasOriginSceneX),
-(element.y - canvasOriginSceneY),
tipCanvas.width / (dpr * canvasScale),
tipCanvas.height / (dpr * canvasScale),
);
drawFreeDrawSegments( drawFreeDrawSegments(
element, element,
ctx, ctx,
renderConfig, renderConfig,
drawFrom, inc.committedPointCount - 1,
element.points[element.points.length - 1], undefined, // draw to natural end (the tip segment)
//predictedPoint,
); );
ctx.restore(); });
// Update lastRenderedPointCount in the cache entry.
const inc = freedrawIncrementalCache.get(element)!;
inc.lastRenderedPointCount = element.points.length;
}
const inc = freedrawIncrementalCache.get(element)!;
return { return {
element, element,
canvas, canvas: committedCanvas,
tipCanvas,
theme: appState.theme, theme: appState.theme,
scale: canvasScale, scale: canvasScale,
angle: element.angle, angle: element.angle,
@@ -1279,6 +1318,19 @@ const drawElementFromCanvas = (
elementWithCanvas.canvas!.height / elementWithCanvas.scale, elementWithCanvas.canvas!.height / elementWithCanvas.scale,
); );
// Composite the tip canvas (incremental freedraw path) on top. It is
// the same size and at the same scene origin as the committed canvas, so
// it uses identical destX / destY / dimensions.
if (elementWithCanvas.tipCanvas) {
context.drawImage(
elementWithCanvas.tipCanvas,
destX,
destY,
elementWithCanvas.tipCanvas.width / elementWithCanvas.scale,
elementWithCanvas.tipCanvas.height / elementWithCanvas.scale,
);
}
if ( if (
import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX === import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
"true" && "true" &&
@@ -1555,7 +1607,7 @@ export const renderElement = (
} }
context.restore(); context.restore();
// not exporting optimized rendering (cache & render from element // not exporting -> optimized rendering (cache & render from element
// canvases) // canvases)
} else { } else {
const elementWithCanvas = generateElementWithCanvas( const elementWithCanvas = generateElementWithCanvas(