From c044d2643153cd8003fe22f31284f8d82c4cc5d9 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 17 May 2026 03:27:19 +0300 Subject: [PATCH] perf(canvas): cache label hit tests and trace scene renderer --- packages/core/src/canvas/labels/cache.ts | 8 ++ packages/core/src/canvas/labels/hit-test.ts | 86 +++++++++++++------ packages/core/src/canvas/renderer.ts | 6 +- packages/core/src/editor/create.ts | 3 + packages/core/src/profiler/render-profiler.ts | 10 ++- src/app/editor/profiler/index.ts | 5 +- 6 files changed, 88 insertions(+), 30 deletions(-) diff --git a/packages/core/src/canvas/labels/cache.ts b/packages/core/src/canvas/labels/cache.ts index 3d7b1fb9d..ac2fde61c 100644 --- a/packages/core/src/canvas/labels/cache.ts +++ b/packages/core/src/canvas/labels/cache.ts @@ -98,6 +98,14 @@ export class LabelCache { })) } + getAllSections(): readonly CachedSection[] { + return this.sections + } + + getAllComponents(): readonly CachedComponent[] { + return this.components + } + private rebuild(graph: SceneGraph, pageId: string | null): void { this.sections = [] this.components = [] diff --git a/packages/core/src/canvas/labels/hit-test.ts b/packages/core/src/canvas/labels/hit-test.ts index a5ac10455..e9e0dea34 100644 --- a/packages/core/src/canvas/labels/hit-test.ts +++ b/packages/core/src/canvas/labels/hit-test.ts @@ -14,6 +14,8 @@ import { import type { SceneGraph, SceneNode } from '#core/scene-graph' import type { Vector } from '#core/types' +import type { LabelCache } from './cache' + function measureGlyphWidth(font: Font, text: string): number { const glyphIds = font.getGlyphIDs(text) const widths = font.getGlyphWidths(glyphIds) @@ -76,52 +78,88 @@ function walkLabelTree( return result } -export function hitTestSectionTitle( +function hitCachedLabel( graph: SceneGraph, + items: readonly T[], + hit: (node: SceneNode, item: T) => SceneNode | null +): SceneNode | null { + for (let i = items.length - 1; i >= 0; i--) { + const item = items[i] + const node = graph.getNode(item.nodeId) + if (!node || !node.visible) continue + const result = hit(node, item) + if (result) return result + } + return null +} + +function hitSectionTitle( + child: SceneNode, + ax: number, + ay: number, + insideSection: boolean, canvasX: number, canvasY: number, zoom: number, - pageId: string, - font: Font | null + font: Font ): SceneNode | null { + const textW = measureGlyphWidth(font, child.name) + const pillW = Math.min(textW + SECTION_TITLE_PADDING_X * 2, child.width * zoom) / zoom + const pillH = SECTION_TITLE_HEIGHT / zoom + const gap = SECTION_TITLE_GAP / zoom + const hit = rotatePoint(canvasX - ax, canvasY - ay, child.rotation) + const pillY = insideSection ? gap : -pillH - gap + + return hitInRect(hit.x, hit.y, 0, pillY, pillW, pillH) ? child : null +} + +export function hitTestSectionTitle(graph: SceneGraph, canvasX: number, canvasY: number, zoom: number, pageId: string, font: Font | null, labelCache?: LabelCache): SceneNode | null { if (!font) return null + if (labelCache) { + return hitCachedLabel(graph, labelCache.getAllSections(), (child, section) => + hitSectionTitle(child, section.absX, section.absY, section.nested, canvasX, canvasY, zoom, font) + ) + } + return walkLabelTree(graph, pageId, (child, _parent, ax, ay, insideSection) => { if (child.type !== 'SECTION') return undefined - - const textW = measureGlyphWidth(font, child.name) - const pillW = Math.min(textW + SECTION_TITLE_PADDING_X * 2, child.width * zoom) / zoom - const pillH = SECTION_TITLE_HEIGHT / zoom - const gap = SECTION_TITLE_GAP / zoom - const hit = rotatePoint(canvasX - ax, canvasY - ay, child.rotation) - const pillY = insideSection ? gap : -pillH - gap - - return hitInRect(hit.x, hit.y, 0, pillY, pillW, pillH) ? child : undefined + return hitSectionTitle(child, ax, ay, insideSection, canvasX, canvasY, zoom, font) }) } -export function hitTestComponentLabel( - graph: SceneGraph, +function hitComponentLabel( + child: SceneNode, + ax: number, + ay: number, canvasX: number, canvasY: number, zoom: number, - pageId: string, - font: Font | null + font: Font ): SceneNode | null { + const textW = measureGlyphWidth(font, child.name) + const labelW = (COMPONENT_LABEL_ICON_SIZE + COMPONENT_LABEL_ICON_GAP + textW) / zoom + const labelH = COMPONENT_LABEL_FONT_SIZE / zoom + const gap = COMPONENT_LABEL_GAP / zoom + const labelY = ay - labelH - gap + + return hitInRect(canvasX, canvasY, ax, labelY, labelW, labelH) ? child : null +} + +export function hitTestComponentLabel(graph: SceneGraph, canvasX: number, canvasY: number, zoom: number, pageId: string, font: Font | null, labelCache?: LabelCache): SceneNode | null { if (!font) return null + if (labelCache) { + return hitCachedLabel(graph, labelCache.getAllComponents(), (child, component) => + hitComponentLabel(child, component.absX, component.absY, canvasX, canvasY, zoom, font) + ) + } + const LABEL_TYPES = new Set(['COMPONENT', 'COMPONENT_SET']) return walkLabelTree(graph, pageId, (child, _parent, ax, ay) => { if (!LABEL_TYPES.has(child.type)) return undefined - - const textW = measureGlyphWidth(font, child.name) - const labelW = (COMPONENT_LABEL_ICON_SIZE + COMPONENT_LABEL_ICON_GAP + textW) / zoom - const labelH = COMPONENT_LABEL_FONT_SIZE / zoom - const gap = COMPONENT_LABEL_GAP / zoom - const labelY = ay - labelH - gap - - return hitInRect(canvasX, canvasY, ax, labelY, labelW, labelH) ? child : undefined + return hitComponentLabel(child, ax, ay, canvasX, canvasY, zoom, font) }) } diff --git a/packages/core/src/canvas/renderer.ts b/packages/core/src/canvas/renderer.ts index f14faf772..c33c303a1 100644 --- a/packages/core/src/canvas/renderer.ts +++ b/packages/core/src/canvas/renderer.ts @@ -416,7 +416,8 @@ export class SkiaRenderer { canvasY, this.zoom, this.pageId ?? graph.rootId, - this.sectionTitleFont + this.sectionTitleFont, + this.labelCache ) } @@ -427,7 +428,8 @@ export class SkiaRenderer { canvasY, this.zoom, this.pageId ?? graph.rootId, - this.componentLabelFont + this.componentLabelFont, + this.labelCache ) } diff --git a/packages/core/src/editor/create.ts b/packages/core/src/editor/create.ts index 22fd8a28e..42d3f77c0 100644 --- a/packages/core/src/editor/create.ts +++ b/packages/core/src/editor/create.ts @@ -206,6 +206,9 @@ export function createEditor(options?: EditorOptions) { get renderer() { return _renderer }, + get canvasRenderers() { + return [..._renderers] + }, get textEditor() { return _textEditor }, diff --git a/packages/core/src/profiler/render-profiler.ts b/packages/core/src/profiler/render-profiler.ts index cdf711536..56d42e571 100644 --- a/packages/core/src/profiler/render-profiler.ts +++ b/packages/core/src/profiler/render-profiler.ts @@ -33,13 +33,17 @@ export class RenderProfiler { this.hud = new HudController(ck) } - toggle(): void { - this.hudVisible = !this.hudVisible - this.enabled = this.hudVisible + setVisible(visible: boolean): void { + this.hudVisible = visible + this.enabled = visible this.phases.enabled = this.enabled this.syncInstrumentation() } + toggle(): void { + this.setVisible(!this.hudVisible) + } + beginFrame(): void { if (!this.enabled) return this.renderStartTime = now() diff --git a/src/app/editor/profiler/index.ts b/src/app/editor/profiler/index.ts index 813d551c5..cba0271f0 100644 --- a/src/app/editor/profiler/index.ts +++ b/src/app/editor/profiler/index.ts @@ -20,7 +20,10 @@ export function createProfilerActions(editor: Editor) { } function toggleProfiler() { - editor.renderer?.profiler.toggle() + const visible = !(editor.renderer?.profiler.hudVisible ?? false) + for (const renderer of editor.canvasRenderers) { + renderer.profiler.setVisible(visible) + } editor.requestRepaint() }