From abc8521a7129e8c5b435d13368c035fe2525640f Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 22 May 2024 14:24:14 +0100 Subject: [PATCH] fix pattern fill lods (#3801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Camera options broke pattern lods. This PR adapts the more flexible take of pattern LODs i did for my version of camera controls to the new version. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix --- .../src/lib/shapes/shared/ShapeFill.tsx | 7 +- .../lib/shapes/shared/defaultStyleDefs.tsx | 138 ++++++++++-------- .../__snapshots__/getSvgString.test.ts.snap | 4 +- 3 files changed, 83 insertions(+), 66 deletions(-) diff --git a/packages/tldraw/src/lib/shapes/shared/ShapeFill.tsx b/packages/tldraw/src/lib/shapes/shared/ShapeFill.tsx index 7cf2cb256..8295cb8fd 100644 --- a/packages/tldraw/src/lib/shapes/shared/ShapeFill.tsx +++ b/packages/tldraw/src/lib/shapes/shared/ShapeFill.tsx @@ -9,7 +9,7 @@ import { useValue, } from '@tldraw/editor' import React from 'react' -import { HASH_PATTERN_ZOOM_NAMES } from './defaultStyleDefs' +import { getHashPatternZoomName } from './defaultStyleDefs' export interface ShapeFillProps { d: string @@ -45,7 +45,6 @@ export function PatternFill({ d, color, theme }: ShapeFillProps) { const svgExport = useSvgExportContext() const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor]) - const intZoom = Math.ceil(zoomLevel) const teenyTiny = editor.getZoomLevel() <= 0.18 return ( @@ -54,10 +53,10 @@ export function PatternFill({ d, color, theme }: ShapeFillProps) { diff --git a/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx b/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx index 69fb90b37..601583470 100644 --- a/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx +++ b/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx @@ -4,25 +4,18 @@ import { DefaultFontStyle, FileHelpers, SvgExportDef, + TLDefaultColorTheme, TLDefaultFillStyle, TLDefaultFontStyle, TLShapeUtilCanvasSvgDef, debugFlags, + last, useEditor, + useValue, } from '@tldraw/editor' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useDefaultColorTheme } from './ShapeFill' -/** @internal */ -export const HASH_PATTERN_ZOOM_NAMES: Record = {} - -const HASH_PATTERN_COUNT = 6 - -for (let zoom = 1; zoom <= HASH_PATTERN_COUNT; zoom++) { - HASH_PATTERN_ZOOM_NAMES[zoom + '_dark'] = `hash_pattern_zoom_${zoom}_dark` - HASH_PATTERN_ZOOM_NAMES[zoom + '_light'] = `hash_pattern_zoom_${zoom}_light` -} - /** @public */ export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef { return { @@ -80,7 +73,7 @@ function HashPatternForExport() { { - const defaultPatterns: PatternDef[] = [] - for (let i = 1; i <= HASH_PATTERN_COUNT; i++) { - const whitePixelBlob = canvasBlob([1, 1], (ctx) => { - ctx.fillStyle = DefaultColorThemePalette.lightMode.black.semi - ctx.fillRect(0, 0, 1, 1) - }) - const blackPixelBlob = canvasBlob([1, 1], (ctx) => { - ctx.fillStyle = DefaultColorThemePalette.darkMode.black.semi - ctx.fillRect(0, 0, 1, 1) - }) - defaultPatterns.push({ - zoom: i, - url: whitePixelBlob, - darkMode: false, - }) - defaultPatterns.push({ - zoom: i, - url: blackPixelBlob, - darkMode: true, - }) +let defaultPixels: { white: string; black: string } | null = null +function getDefaultPixels() { + if (!defaultPixels) { + defaultPixels = { + white: canvasBlob([1, 1], (ctx) => { + ctx.fillStyle = '#f8f9fa' + ctx.fillRect(0, 0, 1, 1) + }), + black: canvasBlob([1, 1], (ctx) => { + ctx.fillStyle = '#212529' + ctx.fillRect(0, 0, 1, 1) + }), + } } - return defaultPatterns + return defaultPixels +} + +function getPatternLodForZoomLevel(zoom: number) { + return Math.ceil(Math.log2(Math.max(1, zoom))) +} + +export function getHashPatternZoomName(zoom: number, theme: TLDefaultColorTheme['id']) { + const lod = getPatternLodForZoomLevel(zoom) + return `tldraw_hash_pattern_${theme}_${lod}` +} + +function getPatternLodsToGenerate(maxZoom: number) { + const levels = [] + const minLod = 0 + const maxLod = getPatternLodForZoomLevel(maxZoom) + for (let i = minLod; i <= maxLod; i++) { + levels.push(Math.pow(2, i)) + } + return levels +} + +function getDefaultPatterns(maxZoom: number): PatternDef[] { + const defaultPixels = getDefaultPixels() + return getPatternLodsToGenerate(maxZoom).flatMap((zoom) => [ + { zoom, url: defaultPixels.white, theme: 'light' }, + { zoom, url: defaultPixels.black, theme: 'dark' }, + ]) } function usePattern() { const editor = useEditor() - const dpr = editor.getInstanceState().devicePixelRatio + const dpr = useValue('devicePixelRatio', () => editor.getInstanceState().devicePixelRatio, [ + editor, + ]) + const maxZoom = useValue('maxZoom', () => Math.ceil(last(editor.getCameraOptions().zoomSteps)!), [ + editor, + ]) const [isReady, setIsReady] = useState(false) - const defaultPatterns = useMemo(() => getDefaultPatterns(), []) - const [backgroundUrls, setBackgroundUrls] = useState(defaultPatterns) + const [backgroundUrls, setBackgroundUrls] = useState(() => + getDefaultPatterns(maxZoom) + ) useEffect(() => { if (process.env.NODE_ENV === 'test') { @@ -194,46 +212,46 @@ function usePattern() { return } - const promises: Promise<{ zoom: number; url: string; darkMode: boolean }>[] = [] - - for (let i = 1; i <= HASH_PATTERN_COUNT; i++) { - promises.push( - generateImage(dpr, i, false).then((blob) => ({ - zoom: i, + const promise = Promise.all( + getPatternLodsToGenerate(maxZoom).flatMap>((zoom) => [ + generateImage(dpr, zoom, false).then((blob) => ({ + zoom, + theme: 'light', url: URL.createObjectURL(blob), - darkMode: false, - })) - ) - promises.push( - generateImage(dpr, i, true).then((blob) => ({ - zoom: i, + })), + generateImage(dpr, zoom, true).then((blob) => ({ + zoom, + theme: 'dark', url: URL.createObjectURL(blob), - darkMode: true, - })) - ) - } + })), + ]) + ) let isCancelled = false - Promise.all(promises).then((urls) => { + promise.then((urls) => { if (isCancelled) return setBackgroundUrls(urls) setIsReady(true) }) - return () => { isCancelled = true setIsReady(false) + promise.then((patterns) => { + for (const { url } of patterns) { + URL.revokeObjectURL(url) + } + }) } - }, [dpr]) + }, [dpr, maxZoom]) const defs = ( <> {backgroundUrls.map((item) => { - const key = item.zoom + (item.darkMode ? '_dark' : '_light') + const id = getHashPatternZoomName(item.zoom, item.theme) return ( @@ -133,7 +133,7 @@ exports[`Matches a snapshot: Basic SVG 1`] = ` />