diff --git a/templates/design/app/components/design/DesignCanvas.tsx b/templates/design/app/components/design/DesignCanvas.tsx index adcac045c8..a4467c0a07 100644 --- a/templates/design/app/components/design/DesignCanvas.tsx +++ b/templates/design/app/components/design/DesignCanvas.tsx @@ -1293,6 +1293,10 @@ export function DesignCanvas({ [probeBridgeReadinessUntilDrained], ); const [renderedContent, setRenderedContent] = useState(content); + // What a freshly loaded document already contains, since srcdoc is built from + // it. The load handler below needs this to skip redundant pushes. + const renderedContentRef = useRef(renderedContent); + renderedContentRef.current = renderedContent; // True while a drawing send is capturing/compositing/uploading the // annotated screenshot (see design-canvas/annotation-snapshot.ts). Drives // SharedDrawOverlay's busy Send state so a slow capture can't be triggered @@ -4043,6 +4047,9 @@ export function DesignCanvas({ const replaceLatestRuntimeContent = () => { const nextContent = runtimeReplacementContentRef.current; if (nextContent === undefined) return; + // The document that just loaded was built from these bytes; swapping it + // for itself only costs a blank frame. + if (renderedContentRef.current === nextContent) return; if (replaceRuntimeContentInPlace(nextContent)) { lastRuntimeReplacementKeyRef.current = runtimeReplacementKeyRef.current; lastRuntimeReplacementContentRef.current = nextContent; diff --git a/templates/design/app/components/design/MultiScreenCanvas.primitives.test.ts b/templates/design/app/components/design/MultiScreenCanvas.primitives.test.ts index 346cb4d8b7..017ead5aa4 100644 --- a/templates/design/app/components/design/MultiScreenCanvas.primitives.test.ts +++ b/templates/design/app/components/design/MultiScreenCanvas.primitives.test.ts @@ -243,6 +243,7 @@ describe("board surface pointer capture", () => { expect( shouldRenderBoardSurfaceStaticPreview({ zoom: 2, + hasSurfaceContent: true, viewportGeometry: viewport, renderGeometry: active, }), @@ -312,6 +313,33 @@ describe("board surface pointer capture", () => { } }); + it("keeps the opaque board replica off when the board has nothing on it", () => { + // A board file can be a full HTML document with an empty
— truthy as + // a string, nothing to show. The replica paints itself in the board colour, + // so rendering it there covers the canvas in a full-board slab at low zoom. + const logical = makeGeom(-65536, -65536, 131072, 131072); + const active = makeGeom(-12288, -12288, 24576, 24576); + const viewport = makeGeom(-36000, -22500, 72000, 45000); + + expect( + shouldRenderBoardSurfaceStaticPreview({ + zoom: 2, + hasSurfaceContent: false, + viewportGeometry: viewport, + renderGeometry: active, + }), + ).toBe(false); + // Also below the pre-measurement zoom fallback. + expect( + shouldRenderBoardSurfaceStaticPreview({ + zoom: 2, + hasSurfaceContent: false, + viewportGeometry: null, + renderGeometry: active, + }), + ).toBe(false); + }); + it("treats empty board documents as having no surface content", () => { expect( hasBoardSurfaceContent( diff --git a/templates/design/app/components/design/MultiScreenCanvas.tsx b/templates/design/app/components/design/MultiScreenCanvas.tsx index 2acd776a24..821d219621 100644 --- a/templates/design/app/components/design/MultiScreenCanvas.tsx +++ b/templates/design/app/components/design/MultiScreenCanvas.tsx @@ -243,7 +243,10 @@ import { sameResolvedMetadata, type ResolvedMetadataCacheEntry, } from "./multi-screen/screen-content-cache"; -import { setWheelCameraGestureActive } from "./multi-screen/wheel-gesture-state"; +import { + isWheelCameraGestureActive, + setWheelCameraGestureActive, +} from "./multi-screen/wheel-gesture-state"; // Figma parity: a plain click (no drag) with the rectangle or ellipse tool // places a 100x100 shape. Both tools share this default via the "else" @@ -339,6 +342,13 @@ import { screenPxToCanvasPx, stopPercentFromDraggedPoint, } from "./multi-screen/gradient-overlay-geometry"; +import { + applyScreenPaintSuppression, + collectScreenPaintTargets, + resolveSuppressedScreenIds, + type ScreenPaintCandidate, + type ScreenPaintTarget, +} from "./multi-screen/paint-suppression"; import { getPrimitiveDropTargetForPoint, getPrimitiveLowZoomHitRect, @@ -648,26 +658,33 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({ : null, [boardFrameGeometry], ); + // Both board layers must ask this one question: an empty is a truthy + // string, and the replica is opaque, so a string-only gate slabs the board. + const boardSurfaceHtml = hasBoardSurfaceContent(boardFileContent) + ? boardFileContent + : undefined; + const boardHasSurfaceContent = boardSurfaceHtml !== undefined; const boardStaticPreviewContent = useMemo(() => { if ( !boardFrameGeometry || !boardStaticPreviewViewport || - !boardFileContent + !boardSurfaceHtml ) { return null; } return getBoardSurfaceStaticPreviewContent({ - html: boardFileContent, + html: boardSurfaceHtml, logicalGeometry: boardFrameGeometry, viewport: boardStaticPreviewViewport, }); - }, [boardFileContent, boardFrameGeometry, boardStaticPreviewViewport]); + }, [boardSurfaceHtml, boardFrameGeometry, boardStaticPreviewViewport]); const showBoardStaticPreview = Boolean( boardFrameGeometry && boardSurfaceRenderGeometry && boardStaticPreviewContent && shouldRenderBoardSurfaceStaticPreview({ zoom: canvasZoom, + hasSurfaceContent: boardHasSurfaceContent, viewportGeometry: boardViewportGeometry, renderGeometry: boardSurfaceRenderGeometry, }), @@ -957,6 +974,12 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({ const liveScreenIdsRef = useRef