* perf(canvas): add traced navigation benchmarks - Record and replay timestamped pan and zoom gestures through DOM and CDP input paths\n- Correlate input, viewport, render, long-task, and retained-backing events in Chromium traces\n- Report frame pacing, latency, jump, anchor drift, and crisp-settlement metrics * perf(canvas): stabilize navigation comparisons - Separate low-overhead metric runs from optional CPU-profile traces\n- Warm scenarios before recording and use a consistent SwiftShader browser configuration\n- Add a canonical momentum-pan reversal gesture alongside pinch reversal * fix(canvas): require hardware GPU navigation benchmarks - Run macOS performance captures through Metal-backed ANGLE and reject accidental SwiftShader fallback\n- Record the GL renderer and reserve software GPU mode for portable correctness smoke runs * perf(canvas): cache shadow rasters for crisp backing - Rasterize local drop and inner shadows only while constructing retained scene backing\n- Bound native image memory and invalidate cached entries with node and renderer lifecycle changes\n- Quantize zoom-aware raster resolution and reuse nearby scales without lowering normal scene quality * test(canvas): verify retained shadow raster fidelity - Compare settled retained-backing shadow output with direct CanvasKit rendering\n- Keep backdrop blur on the picture fallback and exercise graph-driven cache invalidation\n- Cover updates, deletion, and reparenting through actual SceneGraph events * perf(canvas): benchmark real FIG fixtures - Serve exact local fixture bytes through an isolated Playwright route for production preview runs\n- Wait for document loading and page population before zooming to fit and recording navigation\n- Record the resolved fixture path in benchmark environment artifacts * fix(canvas): preserve nested effect subtree pictures - Keep deeply nested shadow documents on one retained subtree picture instead of exploding them into per-node image draws\n- Restrict shadow raster acceleration to effect-bearing page children\n- Cover nested shadow fallback and restore gold-preview FIG pinch performance to master levels * refactor(canvas): share recorded wheel sample type * perf(canvas): defer backing settlement across zoom reversals - Track explicit navigation phases and gesture generations instead of inferring idle from viewport timing\n- Cancel or defer retained backing construction while pan, zoom, momentum, or tentative settlement is active\n- Add a repeated short-pause pinch reversal fixture based on the user trace * perf(canvas): index bounded render chunks - Split oversized painter subtrees into self-paint and bounded descendant chunks without dropping container visuals\n- Bulk-load chunk visual bounds into RBush for selective world-space queries\n- Cover bounded updates and gold-preview build/query complexity before tile rendering consumes the index * refactor(canvas): namespace render chunk coverage * perf(canvas): model chunk paint context - Preserve ancestor transform and clip dependencies for independently renderable chunks\n- Keep opacity, blend, blur, and mask isolation subtrees atomic until command-level splitting exists\n- Report oversized atomic chunks and lock gold-preview to bounded painter units * perf(canvas): record pixel-correct render chunks - Record interruptible chunks in world coordinates with ancestor transforms, clips, and chunk-local culling bounds\n- Draw opacity, blend, blur, and mask isolation chunks directly into destination surfaces in painter order\n- Compare composited chunk output with direct CanvasKit rendering instead of relaxing visual thresholds * perf(canvas): render selective world tiles - Map world regions to fixed 256-device-pixel tile targets and quantized sharpness levels\n- Query only intersecting render chunks and preserve atomic destination compositing\n- Match multi-tile CanvasKit output against direct rendering and measure gold-preview tile cost * perf(canvas): cache chunk pictures across tiles - Reuse world-space chunk command pictures for every intersecting tile\n- Pool 256-pixel tile surfaces and expose allocation, draw, flush, and snapshot timings\n- Keep expensive atomic foreground blur visible as an over-budget scheduler constraint * perf(canvas): schedule cached tile rendering - Bound tile images with an LRU cache and reuse pooled CanvasKit surfaces\n- Plan mandatory holes, stale visible refreshes, and overscan by navigation and content generation\n- Stop jobs at a strict deadline while reporting fallbacks, stale work, overruns, and over-budget effects * perf(canvas): integrate progressive tiled rendering - Keep retained scene output as the interaction fallback while exact tiles refine only after navigation becomes idle - Centralize runtime URL flags and pass renderer selection through the typed Vue canvas API - Replace benchmark sleeps with explicit mode-aware renderer settlement and report exact tiled coverage - Preserve bounded scheduler metrics, generation cancellation, native resource cleanup, and shared visual-bounds logic * refactor(app): centralize runtime query configuration - Parse collaboration, recent-files, benchmark, presentation, and renderer flags in one typed app module - Remove ad hoc URL parsing from workspace and collaboration runtime consumers - Cover supported values and production-safe defaults without adding a repository lint rule * fix(canvas): replace fallback pixels with exact tiles - Render opaque page-background tile cells and install them with source replacement instead of double-compositing translucent scene content - Exercise the live progressive controller against direct rendering across masks, effects, blend isolation, images, fallback text, transforms, and clipping - Preserve the bounded reversal path with zero Long Tasks and exact settlement near 128 ms on gold-preview.fig * perf(canvas): invalidate tiled content selectively - Index chunk dependencies across contained nodes and transform or clipping ancestors - Re-record affected chunk pictures and invalidate tiles intersecting old or new visual bounds - Advance unaffected cached tiles to the new scene generation instead of rebuilding the full chunk index and tile cache - Keep structural graph mutations on the safe full-rebuild path and cover selective refresh end to end * perf(canvas): bound atomic blur tile refresh - Render atomic blur chunks with tile-local isolation bounds and blur halos instead of replaying full-subtree layers - Keep content refresh behind the retained fallback, cap GPU submissions to four tile jobs per frame, and adapt estimates from measured work - Preserve large-radius CPU over-budget visibility while preventing Metal-backed refresh bursts and deferred GPU overload - Add deterministic node-mutation benchmarks and summarize scheduler throughput, job duration, overruns, and exact content settlement * perf(canvas): cancel obsolete tile refresh generations - Count and report queued jobs removed by content or navigation generation changes - Add deterministic mutation-then-reversal benchmark support without sleeps - Assert exact tile work remains suspended during navigation and resumes for the final viewport - Summarize cancellation alongside scheduler throughput, overruns, and settlement metrics * test(canvas): cover live tiled blur settlement - Load gold-preview.fig through the real tiled canvas surface and wait on explicit renderer settlement - Commit the settled radius-210 large-blur browser snapshot - Replay the canonical zoom reversal during refresh and require byte-identical final canvas convergence * fix(canvas): harden renderer resource lifecycle - Release tiled surfaces, images, pictures, and queued work across surface, font, graph, page, structure, and renderer lifecycle boundaries - Restore pooled canvas, viewport, and backing state through exception-safe native recording and raster paths - Rebuild tiled chunk topology only when isolation requirements actually change, preserving selective blur mutation performance - Document deterministic active-renderer settlement and add lifecycle, graph replacement, cache failure, and surface replacement regressions * test(canvas): remove source-matching renderer claims - Delete the autopsy suite that inferred runtime correctness from source text, regexes, line placement, and symbol counts - Keep renderer ordering, cache cleanup, effect behavior, and pixel fidelity covered by executable behavioral and lifecycle tests * perf(canvas): present retained backing during tiled navigation - Profile production reversal traces and attribute tiled p95 cost to GPU command-buffer flushes from full-scene fallback replay and tile presentation - Use the retained backing as the moving fallback while tile scheduling and cached lookup remain allocation-free - Defer tile image presentation until idle and expose visible versus presented tile counts in navigation telemetry - Reduce tiled reversal render p95 from about 8ms to 0.3ms while preserving exact idle replacement and visual parity * perf(canvas): prioritize visible tile settlement - Profile per-tile allocation, draw, flush, snapshot, and chunk costs through scheduler telemetry - Defer overscan until all visible exact tiles are covered - Replace the four-job idle cap with a higher safety ceiling while the measured five-millisecond deadline controls cheap work - Reduce mutation-plus-reversal exact settlement from about 272ms to 160ms without Long Tasks, overruns, or over-budget jobs * refactor(canvas): clarify renderer lifecycle boundaries - Extract retained backing state types and navigation preview timing\n- Isolate tiled scheduler telemetry from frame orchestration\n- Document settlement and CanvasKit ownership invariants\n- Preserve hot drawing loops, budgets, cache limits, and rendering decisions * fix(canvas): preserve current label rendering Retain the merged paragraph-label cache lifecycle and substituted-font readiness while reconstructing the renderer stack on current master. * test(canvas): keep tile benchmark assertions deterministic Keep performance timing in benchmark telemetry while asserting structural tile selectivity and cache behavior in CI. * feat(canvas): expose experimental tiled rendering - Persist retained or tiled canvas mode in General settings\n- Keep retained rendering as the default and apply changes after reload\n- Preserve URL overrides for deterministic benchmarks and support reproduction * refactor(app): centralize renderer preference state Expose renderer override provenance from runtime configuration and keep the settings control's derived state separate from its explicit persistence action. * refactor(app): share settings layout anatomy Reuse slot-based section headers and bordered groups while keeping each settings control row explicit. * fix(canvas): harden tiled renderer boundaries - Bound low-zoom tile planning and handle failed tile surface allocation\n- Preserve effect raster dependencies, runtime-safe clocks, and navigation timing contracts\n- Keep benchmarks deterministic, backward compatible, and accurately localized * fix(canvas): invalidate dependent node pictures Track first-child shadow dependencies for retained node pictures so child geometry updates cannot leave stale parent shadows.
143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
|
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
|
|
|
test('language can be changed from General settings and persists', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-language').click()
|
|
await page.getByRole('option', { name: 'Русский' }).click()
|
|
await expect(page.getByTestId('app-settings-dialog')).toContainText('Настройки')
|
|
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await expect(page.getByTestId('settings-language')).toContainText('Русский')
|
|
|
|
await page.getByTestId('settings-language').click()
|
|
await page.getByRole('option', { name: 'English' }).click()
|
|
await expect(page.getByTestId('app-settings-dialog')).toContainText('Settings')
|
|
})
|
|
|
|
test('general snapping preferences persist and apply to editor sessions', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await expect(page.getByTestId('settings-general-panel')).toBeVisible()
|
|
const geometry = page.getByRole('switch', { name: 'Snap to geometry' })
|
|
const objects = page.getByRole('switch', { name: 'Snap to objects' })
|
|
const pixelGrid = page.getByRole('switch', { name: 'Snap to pixel grid' })
|
|
await expect(geometry).toBeChecked()
|
|
await expect(objects).toBeChecked()
|
|
await expect(pixelGrid).toBeChecked()
|
|
|
|
await geometry.click()
|
|
await objects.click()
|
|
await pixelGrid.click()
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => window.openPencil?.getStore?.().state.snappingPreferences ?? null)
|
|
)
|
|
.toEqual({ geometry: false, objects: false, pixelGrid: false })
|
|
|
|
await page.getByTestId('app-settings-done').click()
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await expect(geometry).not.toBeChecked()
|
|
await expect(objects).not.toBeChecked()
|
|
await expect(pixelGrid).not.toBeChecked()
|
|
await expect(
|
|
page.evaluate(() => window.openPencil?.getStore?.().state.snappingPreferences ?? null)
|
|
).resolves.toEqual({ geometry: false, objects: false, pixelGrid: false })
|
|
|
|
await geometry.click()
|
|
await objects.click()
|
|
await pixelGrid.click()
|
|
})
|
|
|
|
test('progressive tiled rendering preference persists and URL overrides take precedence', async ({
|
|
page
|
|
}) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
const tiled = page.getByRole('switch', { name: 'Progressive tiled canvas rendering' })
|
|
await expect(tiled).not.toBeChecked()
|
|
await tiled.click()
|
|
await expect(page.getByText('Reload OpenPencil to apply this change.')).toBeVisible()
|
|
|
|
await page.reload()
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await expect(tiled).toBeChecked()
|
|
await expect(
|
|
page.evaluate(() =>
|
|
window.openPencil
|
|
?.getStore?.()
|
|
.canvasRenderers.some(
|
|
(renderer) => renderer.tracksSceneSettlement && renderer.tiledSceneEnabled
|
|
)
|
|
)
|
|
).resolves.toBe(true)
|
|
|
|
await page.goto('/?test&renderer=retained')
|
|
await canvas.waitForInit()
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await expect(tiled).toBeChecked()
|
|
await expect(
|
|
page.getByText(/current session renderer is controlled by a URL override/i)
|
|
).toBeVisible()
|
|
await expect(
|
|
page.evaluate(() =>
|
|
window.openPencil
|
|
?.getStore?.()
|
|
.canvasRenderers.every(
|
|
(renderer) => !renderer.tracksSceneSettlement || !renderer.tiledSceneEnabled
|
|
)
|
|
)
|
|
).resolves.toBe(true)
|
|
|
|
await tiled.click()
|
|
})
|
|
|
|
test('Preferences menu snapping controls use the same preferences', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByTestId('menubar-view').click()
|
|
await page.getByRole('menuitem', { name: 'Preferences' }).hover()
|
|
const geometry = page.getByRole('menuitemcheckbox', { name: 'Snap to geometry' })
|
|
const objects = page.getByRole('menuitemcheckbox', { name: 'Snap to objects' })
|
|
const pixelGrid = page.getByRole('menuitemcheckbox', { name: 'Snap to pixel grid' })
|
|
await expect(geometry).toBeChecked()
|
|
await expect(objects).toBeChecked()
|
|
await expect(pixelGrid).toBeChecked()
|
|
await objects.click()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await expect(page.getByRole('switch', { name: 'Snap to objects' })).not.toBeChecked()
|
|
await page.getByRole('switch', { name: 'Snap to objects' }).click()
|
|
})
|
|
|
|
test('settings shortcut preserves the last visited section', async ({ page }) => {
|
|
await page.goto('/?test')
|
|
const canvas = new CanvasHelper(page)
|
|
await canvas.waitForInit()
|
|
|
|
await page.getByTestId('app-settings-trigger').click()
|
|
await page.getByTestId('settings-section-media').click()
|
|
await page.getByTestId('app-settings-done').click()
|
|
|
|
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+,' : 'Control+,')
|
|
await expect(page.getByTestId('settings-media-panel')).toBeVisible()
|
|
})
|