1363 lines
40 KiB
TypeScript
1363 lines
40 KiB
TypeScript
import {
|
|
resolveNodeFillColor,
|
|
resolveNodeStrokeColor,
|
|
type ResolvedRenderColor
|
|
} from '../color-management'
|
|
/* eslint-disable max-lines -- SkiaRenderer class; text, pen-overlay, fills, scene already extracted */
|
|
import {
|
|
SELECTION_COLOR,
|
|
COMPONENT_COLOR,
|
|
SNAP_COLOR,
|
|
CANVAS_BG_COLOR,
|
|
PEN_PATH_STROKE_WIDTH,
|
|
PARENT_OUTLINE_ALPHA,
|
|
PARENT_OUTLINE_DASH,
|
|
DEFAULT_FONT_SIZE,
|
|
LABEL_FONT_SIZE,
|
|
LABEL_OFFSET_Y,
|
|
SIZE_FONT_SIZE,
|
|
SECTION_TITLE_FONT_SIZE,
|
|
SECTION_TITLE_HEIGHT,
|
|
SECTION_TITLE_PADDING_X,
|
|
SECTION_TITLE_GAP,
|
|
COMPONENT_SET_DASH,
|
|
COMPONENT_SET_DASH_GAP,
|
|
COMPONENT_SET_BORDER_WIDTH,
|
|
COMPONENT_LABEL_FONT_SIZE,
|
|
COMPONENT_LABEL_GAP,
|
|
COMPONENT_LABEL_ICON_SIZE,
|
|
COMPONENT_LABEL_ICON_GAP,
|
|
RULER_BG_COLOR,
|
|
RULER_TICK_COLOR,
|
|
RULER_TEXT_COLOR,
|
|
DEFAULT_FONT_FAMILY,
|
|
IS_BROWSER
|
|
} from '../constants'
|
|
import { computeVisualBounds } from '../geometry'
|
|
import { RenderProfiler } from '../profiler'
|
|
import { drawAiOverlays as drawAiOverlaysFn } from './ai-overlays'
|
|
import {
|
|
getCachedDropShadow as getCachedDropShadowFn,
|
|
getCachedBlur as getCachedBlurFn,
|
|
getCachedDecalBlur as getCachedDecalBlurFn,
|
|
getCachedMaskBlur as getCachedMaskBlurFn,
|
|
applyClippedBlur as applyClippedBlurFn
|
|
} from './effects'
|
|
import {
|
|
drawNodeFill as drawNodeFillFn,
|
|
applyFill as applyFillFn,
|
|
applyGradientFill as applyGradientFillFn,
|
|
applyImageFill as applyImageFillFn,
|
|
drawArc as drawArcFn
|
|
} from './fills'
|
|
import { LabelCache } from './label-cache'
|
|
import {
|
|
drawSectionTitles as drawSectionTitlesFn,
|
|
drawComponentLabels as drawComponentLabelsFn
|
|
} from './labels'
|
|
import { drawNodeEditOverlay as drawNodeEditOverlayFn } from './node-edit-overlay'
|
|
import {
|
|
drawHoverHighlight as drawHoverHighlightFn,
|
|
drawEnteredContainer as drawEnteredContainerFn,
|
|
drawSelection as drawSelectionFn,
|
|
drawNodeSelection as drawNodeSelectionFn,
|
|
drawSelectionLabels as drawSelectionLabelsFn,
|
|
drawParentFrameOutlines as drawParentFrameOutlinesFn,
|
|
drawNodeOutline as drawNodeOutlineFn,
|
|
drawGroupBounds as drawGroupBoundsFn,
|
|
getRotatedCorners as getRotatedCornersFn,
|
|
drawHandle as drawHandleFn,
|
|
drawSnapGuides as drawSnapGuidesFn,
|
|
drawMarquee as drawMarqueeFn,
|
|
drawFlashes as drawFlashesFn,
|
|
drawLayoutInsertIndicator as drawLayoutInsertIndicatorFn,
|
|
drawTextEditOverlay as drawTextEditOverlayFn
|
|
} from './overlays'
|
|
import {
|
|
drawPenOverlay as drawPenOverlayFn,
|
|
drawRemoteCursors as drawRemoteCursorsFn
|
|
} from './pen-overlay'
|
|
import { drawRulers as drawRulersFn } from './rulers'
|
|
import {
|
|
renderNode as renderNodeFn,
|
|
renderSection as renderSectionFn,
|
|
renderComponentSet as renderComponentSetFn,
|
|
renderShape as renderShapeFn,
|
|
renderShapeUncached as renderShapeUncachedFn,
|
|
renderEffects as renderEffectsFn,
|
|
renderText as renderTextFn
|
|
} from './scene'
|
|
import {
|
|
makeNodeShapePath as makeNodeShapePathFn,
|
|
makePolygonPath as makePolygonPathFn,
|
|
makeRRect as makeRRectFn,
|
|
makeRRectWithSpread as makeRRectWithSpreadFn,
|
|
makeRRectWithOffset as makeRRectWithOffsetFn,
|
|
clipNodeShape as clipNodeShapeFn,
|
|
getVectorPaths as getVectorPathsFn,
|
|
getFillGeometry as getFillGeometryFn,
|
|
getStrokeGeometry as getStrokeGeometryFn
|
|
} from './shapes'
|
|
import {
|
|
drawNodeStroke as drawNodeStrokeFn,
|
|
drawStrokeWithAlign as drawStrokeWithAlignFn,
|
|
drawRRectStrokeWithAlign as drawRRectStrokeWithAlignFn,
|
|
drawIndividualSideStrokes as drawIndividualSideStrokesFn,
|
|
strokeNodeShape as strokeNodeShapeFn
|
|
} from './strokes'
|
|
import {
|
|
measureTextNode as measureTextNodeFn,
|
|
isNodeFontLoaded as isNodeFontLoadedFn,
|
|
buildTextPicture as buildTextPictureFn,
|
|
buildParagraph as buildParagraphFn
|
|
} from './text'
|
|
|
|
import type { EditorState } from '../editor/types'
|
|
import type {
|
|
SceneNode,
|
|
SceneGraph,
|
|
Fill,
|
|
Stroke,
|
|
VectorVertex,
|
|
VectorRegion
|
|
} from '../scene-graph'
|
|
import type { SnapGuide } from '../snap'
|
|
import type { TextEditor } from '../text-editor'
|
|
import type { Color, Rect, Vector } from '../types'
|
|
import type { NodeEditOverlayState } from './node-edit-overlay'
|
|
import type {
|
|
Image as CKImage,
|
|
Path,
|
|
CanvasKit,
|
|
Surface,
|
|
Canvas,
|
|
Paint,
|
|
Font,
|
|
FontMgr,
|
|
TypefaceFontProvider,
|
|
SkPicture,
|
|
ImageFilter,
|
|
MaskFilter,
|
|
Paragraph
|
|
} from 'canvaskit-wasm'
|
|
|
|
export interface RenderOverlays {
|
|
hoveredNodeId?: string | null
|
|
enteredContainerId?: string | null
|
|
editingTextId?: string | null
|
|
textEditor?: TextEditor | null
|
|
marquee?: Rect | null
|
|
snapGuides?: SnapGuide[]
|
|
rotationPreview?: { nodeId: string; angle: number } | null
|
|
dropTargetId?: string | null
|
|
layoutInsertIndicator?: {
|
|
x: number
|
|
y: number
|
|
length: number
|
|
direction: 'HORIZONTAL' | 'VERTICAL'
|
|
} | null
|
|
penState?: {
|
|
vertices: Vector[]
|
|
segments: Array<{
|
|
start: number
|
|
end: number
|
|
tangentStart: Vector
|
|
tangentEnd: Vector
|
|
}>
|
|
dragTangent: Vector | null
|
|
oppositeDragTangent?: Vector | null
|
|
closingToFirst: boolean
|
|
pendingClose?: boolean
|
|
cursorX?: number
|
|
cursorY?: number
|
|
} | null
|
|
nodeEditState?: {
|
|
nodeId: string
|
|
vertices: VectorVertex[]
|
|
segments: Array<{
|
|
start: number
|
|
end: number
|
|
tangentStart: Vector
|
|
tangentEnd: Vector
|
|
}>
|
|
regions: VectorRegion[]
|
|
selectedVertexIndices: Set<number>
|
|
/** Set of selected handles as "segIdx:tangentField" strings */
|
|
selectedHandles?: Set<string>
|
|
hoveredHandleInfo?: { segmentIndex: number; tangentField: 'tangentStart' | 'tangentEnd' } | null
|
|
} | null
|
|
remoteCursors?: Array<{
|
|
name: string
|
|
color: Color
|
|
x: number
|
|
y: number
|
|
selection?: string[]
|
|
}>
|
|
}
|
|
|
|
export class SkiaRenderer {
|
|
ck: CanvasKit
|
|
surface: Surface
|
|
fillPaint: Paint
|
|
strokePaint: Paint
|
|
selectionPaint: Paint
|
|
parentOutlinePaint: Paint
|
|
snapPaint: Paint
|
|
auxFill: Paint
|
|
auxStroke: Paint
|
|
opacityPaint: Paint
|
|
effectLayerPaint: Paint
|
|
imageFilterCache = new Map<string, ImageFilter | null>()
|
|
maskFilterCache = new Map<number, MaskFilter | null>()
|
|
_tmpColor = new Float32Array(4)
|
|
_tmpRect = new Float32Array(4)
|
|
textFont: Font | null = null
|
|
labelFont: Font | null = null
|
|
sizeFont: Font | null = null
|
|
sectionTitleFont: Font | null = null
|
|
componentLabelFont: Font | null = null
|
|
fontMgr: FontMgr | null = null
|
|
fontProvider: TypefaceFontProvider | null = null
|
|
fontsLoaded = false
|
|
imageCache = new Map<string, CKImage>()
|
|
vectorPathCache = new Map<string, Path[]>()
|
|
fillGeometryCache = new Map<string, Path[]>()
|
|
strokeGeometryCache = new Map<string, Path[]>()
|
|
scenePicture: SkPicture | null = null
|
|
scenePictureVersion = -1
|
|
scenePicturePageId: string | null = null
|
|
nodePictureCache = new Map<string, SkPicture | null>()
|
|
readonly labelCache = new LabelCache()
|
|
readonly profiler: RenderProfiler
|
|
|
|
rulerBgPaint: Paint
|
|
rulerTickPaint: Paint
|
|
rulerTextPaint: Paint
|
|
rulerHlPaint: Paint
|
|
rulerBadgePaint: Paint
|
|
rulerLabelPaint: Paint
|
|
penPathPaint: Paint
|
|
penLiveStrokePaint: Paint
|
|
penHandlePaint: Paint
|
|
penVertexFill: Paint
|
|
penVertexStroke: Paint
|
|
|
|
panX = 0
|
|
panY = 0
|
|
zoom = 1
|
|
dpr = 1
|
|
viewportWidth = 0
|
|
viewportHeight = 0
|
|
showRulers = true
|
|
pageColor = CANVAS_BG_COLOR
|
|
pageId: string | null = null
|
|
|
|
worldViewport = { x: 0, y: 0, w: 0, h: 0 }
|
|
_nodeCount = 0
|
|
_culledCount = 0
|
|
_flashes: Array<{ nodeId: string; startTime: number }> = []
|
|
_flashPaint: Paint | null = null
|
|
_aiActiveNodes: Set<string> = new Set()
|
|
_aiDoneFlashes: Array<{ nodeId: string; startTime: number }> = []
|
|
|
|
readonly DEFAULT_FONT_SIZE = DEFAULT_FONT_SIZE
|
|
readonly COMPONENT_SET_BORDER_WIDTH = COMPONENT_SET_BORDER_WIDTH
|
|
readonly COMPONENT_SET_DASH = COMPONENT_SET_DASH
|
|
readonly COMPONENT_SET_DASH_GAP = COMPONENT_SET_DASH_GAP
|
|
|
|
color4f(r: number, g: number, b: number, a: number): Float32Array {
|
|
const c = this._tmpColor
|
|
c[0] = r
|
|
c[1] = g
|
|
c[2] = b
|
|
c[3] = a
|
|
return c
|
|
}
|
|
|
|
ltrb(l: number, t: number, r: number, b: number): Float32Array {
|
|
const rc = this._tmpRect
|
|
rc[0] = l
|
|
rc[1] = t
|
|
rc[2] = r
|
|
rc[3] = b
|
|
return rc
|
|
}
|
|
|
|
selColor(alpha = 1) {
|
|
return this.ck.Color4f(SELECTION_COLOR.r, SELECTION_COLOR.g, SELECTION_COLOR.b, alpha)
|
|
}
|
|
|
|
compColor(alpha = 1) {
|
|
return this.ck.Color4f(COMPONENT_COLOR.r, COMPONENT_COLOR.g, COMPONENT_COLOR.b, alpha)
|
|
}
|
|
|
|
isComponentType(type: string): boolean {
|
|
return type === 'COMPONENT' || type === 'COMPONENT_SET' || type === 'INSTANCE'
|
|
}
|
|
|
|
isRectangularType(type: string): boolean {
|
|
return (
|
|
type === 'FRAME' ||
|
|
type === 'RECTANGLE' ||
|
|
type === 'ROUNDED_RECTANGLE' ||
|
|
type === 'COMPONENT' ||
|
|
type === 'INSTANCE' ||
|
|
type === 'SECTION' ||
|
|
type === 'GROUP'
|
|
)
|
|
}
|
|
|
|
effectOverflow(node: SceneNode): number {
|
|
let expand = 0
|
|
for (const e of node.effects) {
|
|
if (!e.visible) continue
|
|
const blur = e.radius
|
|
const spread = e.spread
|
|
const ox = Math.abs(e.offset.x)
|
|
const oy = Math.abs(e.offset.y)
|
|
expand = Math.max(expand, blur + spread + ox, blur + spread + oy)
|
|
}
|
|
return expand
|
|
}
|
|
|
|
constructor(ck: CanvasKit, surface: Surface, gl?: WebGL2RenderingContext | null) {
|
|
this.ck = ck
|
|
this.surface = surface
|
|
this.profiler = new RenderProfiler(ck, gl ?? null)
|
|
|
|
this.fillPaint = new ck.Paint()
|
|
this.fillPaint.setStyle(ck.PaintStyle.Fill)
|
|
this.fillPaint.setAntiAlias(true)
|
|
|
|
this.strokePaint = new ck.Paint()
|
|
this.strokePaint.setStyle(ck.PaintStyle.Stroke)
|
|
this.strokePaint.setAntiAlias(true)
|
|
|
|
this.selectionPaint = new ck.Paint()
|
|
this.selectionPaint.setStyle(ck.PaintStyle.Stroke)
|
|
this.selectionPaint.setStrokeWidth(1)
|
|
this.selectionPaint.setColor(this.selColor())
|
|
this.selectionPaint.setAntiAlias(true)
|
|
|
|
this.parentOutlinePaint = new ck.Paint()
|
|
this.parentOutlinePaint.setStyle(ck.PaintStyle.Stroke)
|
|
this.parentOutlinePaint.setStrokeWidth(1)
|
|
this.parentOutlinePaint.setColor(this.selColor(PARENT_OUTLINE_ALPHA))
|
|
this.parentOutlinePaint.setAntiAlias(true)
|
|
this.parentOutlinePaint.setPathEffect(
|
|
ck.PathEffect.MakeDash([PARENT_OUTLINE_DASH, PARENT_OUTLINE_DASH], 0)
|
|
)
|
|
|
|
this.snapPaint = new ck.Paint()
|
|
this.snapPaint.setStyle(ck.PaintStyle.Stroke)
|
|
this.snapPaint.setStrokeWidth(1)
|
|
this.snapPaint.setColor(this.ck.Color4f(SNAP_COLOR.r, SNAP_COLOR.g, SNAP_COLOR.b, 1))
|
|
this.snapPaint.setAntiAlias(true)
|
|
|
|
this.auxFill = new ck.Paint()
|
|
this.auxFill.setStyle(ck.PaintStyle.Fill)
|
|
this.auxFill.setAntiAlias(true)
|
|
|
|
this.auxStroke = new ck.Paint()
|
|
this.auxStroke.setStyle(ck.PaintStyle.Stroke)
|
|
this.auxStroke.setAntiAlias(true)
|
|
|
|
this.opacityPaint = new ck.Paint()
|
|
this.effectLayerPaint = new ck.Paint()
|
|
|
|
this.textFont = new ck.Font(null, DEFAULT_FONT_SIZE)
|
|
|
|
const bg = RULER_BG_COLOR
|
|
this.rulerBgPaint = new ck.Paint()
|
|
this.rulerBgPaint.setColor(ck.Color4f(bg.r, bg.g, bg.b, 1))
|
|
|
|
this.rulerTickPaint = new ck.Paint()
|
|
this.rulerTickPaint.setColor(
|
|
ck.Color4f(RULER_TICK_COLOR.r, RULER_TICK_COLOR.g, RULER_TICK_COLOR.b, 1)
|
|
)
|
|
this.rulerTickPaint.setStrokeWidth(1)
|
|
this.rulerTickPaint.setAntiAlias(true)
|
|
|
|
const tc = RULER_TEXT_COLOR
|
|
this.rulerTextPaint = new ck.Paint()
|
|
this.rulerTextPaint.setColor(ck.Color4f(tc.r, tc.g, tc.b, 1))
|
|
this.rulerTextPaint.setAntiAlias(true)
|
|
|
|
this.rulerHlPaint = new ck.Paint()
|
|
this.rulerHlPaint.setAntiAlias(true)
|
|
|
|
this.rulerBadgePaint = new ck.Paint()
|
|
this.rulerBadgePaint.setAntiAlias(true)
|
|
|
|
this.rulerLabelPaint = new ck.Paint()
|
|
this.rulerLabelPaint.setColor(ck.Color4f(1, 1, 1, 1))
|
|
this.rulerLabelPaint.setAntiAlias(true)
|
|
|
|
// Technical outline: 1px grey (same as node-edit techStroke)
|
|
this.penPathPaint = new ck.Paint()
|
|
this.penPathPaint.setStyle(ck.PaintStyle.Stroke)
|
|
this.penPathPaint.setStrokeWidth(1)
|
|
this.penPathPaint.setColor(ck.Color4f(0.698, 0.698, 0.698, 1))
|
|
this.penPathPaint.setAntiAlias(true)
|
|
|
|
// Live stroke preview: 2px black (actual object style)
|
|
this.penLiveStrokePaint = new ck.Paint()
|
|
this.penLiveStrokePaint.setStyle(ck.PaintStyle.Stroke)
|
|
this.penLiveStrokePaint.setStrokeWidth(PEN_PATH_STROKE_WIDTH)
|
|
this.penLiveStrokePaint.setColor(ck.Color4f(0, 0, 0, 1))
|
|
this.penLiveStrokePaint.setAntiAlias(true)
|
|
|
|
this.penHandlePaint = new ck.Paint()
|
|
this.penHandlePaint.setStyle(ck.PaintStyle.Stroke)
|
|
this.penHandlePaint.setStrokeWidth(1)
|
|
this.penHandlePaint.setColor(this.selColor(PARENT_OUTLINE_ALPHA))
|
|
this.penHandlePaint.setAntiAlias(true)
|
|
|
|
this.penVertexFill = new ck.Paint()
|
|
this.penVertexFill.setStyle(ck.PaintStyle.Fill)
|
|
this.penVertexFill.setColor(ck.WHITE)
|
|
this.penVertexFill.setAntiAlias(true)
|
|
|
|
// Vertex circle outline: 1px blue
|
|
this.penVertexStroke = new ck.Paint()
|
|
this.penVertexStroke.setStyle(ck.PaintStyle.Stroke)
|
|
this.penVertexStroke.setStrokeWidth(1)
|
|
this.penVertexStroke.setColor(this.selColor())
|
|
this.penVertexStroke.setAntiAlias(true)
|
|
}
|
|
|
|
getFontProvider(): TypefaceFontProvider | null {
|
|
return this.fontProvider
|
|
}
|
|
|
|
async loadFonts(): Promise<void> {
|
|
this.fontProvider = this.ck.TypefaceFontProvider.Make()
|
|
|
|
const { initFontService, loadFont, ensureArabicFallback, ensureCJKFallback } =
|
|
await import('../fonts')
|
|
initFontService(this.ck, this.fontProvider)
|
|
|
|
const fontData = await loadFont(DEFAULT_FONT_FAMILY, 'Regular')
|
|
if (fontData) {
|
|
const typeface = this.ck.Typeface.MakeFreeTypeFaceFromData(fontData)
|
|
if (typeface) {
|
|
this.textFont?.delete()
|
|
this.labelFont?.delete()
|
|
this.sizeFont?.delete()
|
|
this.sectionTitleFont?.delete()
|
|
this.componentLabelFont?.delete()
|
|
this.textFont = new this.ck.Font(typeface, DEFAULT_FONT_SIZE)
|
|
this.labelFont = new this.ck.Font(typeface, LABEL_FONT_SIZE)
|
|
this.sizeFont = new this.ck.Font(typeface, SIZE_FONT_SIZE)
|
|
this.sectionTitleFont = new this.ck.Font(typeface, SECTION_TITLE_FONT_SIZE)
|
|
this.componentLabelFont = new this.ck.Font(typeface, COMPONENT_LABEL_FONT_SIZE)
|
|
this.profiler.setTypeface(typeface)
|
|
}
|
|
this.fontMgr = this.ck.FontMgr.FromData(fontData) ?? null
|
|
}
|
|
|
|
this.fontsLoaded = true
|
|
this.invalidateAllPictures()
|
|
|
|
void ensureCJKFallback().then((families) => {
|
|
if (families.length > 0) this.invalidateAllPictures()
|
|
})
|
|
void ensureArabicFallback().then((families) => {
|
|
if (families.length > 0) this.invalidateAllPictures()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Load document fonts and set up text measurement for layout.
|
|
* Call after `loadFonts()` and before rendering a document headlessly.
|
|
* Collects all font family+weight pairs used by `nodeIds`, loads them,
|
|
* wires up the text measurer for Yoga layout, and recomputes layout.
|
|
*/
|
|
async prepareForExport(
|
|
graph: SceneGraph,
|
|
pageId: string,
|
|
nodeIds: string[]
|
|
): Promise<() => void> {
|
|
const { collectFontKeys, loadFont } = await import('../fonts')
|
|
const { getTextMeasurer, setTextMeasurer, computeAllLayouts } = await import('../layout')
|
|
|
|
const previousTextMeasurer = getTextMeasurer()
|
|
setTextMeasurer((node, maxWidth) => this.measureTextNode(node, maxWidth))
|
|
|
|
const fontKeys = collectFontKeys(graph, nodeIds)
|
|
await Promise.all(fontKeys.map(([family, style]) => loadFont(family, style)))
|
|
|
|
computeAllLayouts(graph, pageId)
|
|
|
|
return () => setTextMeasurer(previousTextMeasurer)
|
|
}
|
|
|
|
replaceSurface(surface: Surface): void {
|
|
this.surface.delete()
|
|
this.surface = surface
|
|
this.invalidateScenePicture()
|
|
}
|
|
|
|
invalidateScenePicture(): void {
|
|
this.scenePicture?.delete()
|
|
this.scenePicture = null
|
|
this.scenePictureVersion = -1
|
|
}
|
|
|
|
invalidateAllPictures(): void {
|
|
this.invalidateScenePicture()
|
|
for (const pic of this.nodePictureCache.values()) pic?.delete()
|
|
this.nodePictureCache.clear()
|
|
}
|
|
|
|
invalidateNodePicture(nodeId: string): void {
|
|
const pic = this.nodePictureCache.get(nodeId)
|
|
if (pic) {
|
|
pic.delete()
|
|
this.nodePictureCache.delete(nodeId)
|
|
}
|
|
}
|
|
|
|
flashNode(nodeId: string): void {
|
|
this._flashes.push({ nodeId, startTime: performance.now() })
|
|
}
|
|
|
|
aiMarkActive(nodeIds: string[]): void {
|
|
for (const id of nodeIds) this._aiActiveNodes.add(id)
|
|
}
|
|
|
|
aiMarkDone(nodeIds: string[]): void {
|
|
const now = performance.now()
|
|
for (const id of nodeIds) {
|
|
if (this._aiActiveNodes.delete(id)) {
|
|
this._aiDoneFlashes.push({ nodeId: id, startTime: now })
|
|
}
|
|
}
|
|
}
|
|
|
|
aiFlashDone(nodeIds: string[]): void {
|
|
const now = performance.now()
|
|
for (const id of nodeIds) {
|
|
this._aiDoneFlashes.push({ nodeId: id, startTime: now })
|
|
}
|
|
}
|
|
|
|
aiClearActive(): void {
|
|
this._aiActiveNodes.clear()
|
|
}
|
|
|
|
aiClearAll(): void {
|
|
this._aiActiveNodes.clear()
|
|
this._aiDoneFlashes = []
|
|
}
|
|
|
|
get hasActiveFlashes(): boolean {
|
|
return (
|
|
this._flashes.length > 0 || this._aiActiveNodes.size > 0 || this._aiDoneFlashes.length > 0
|
|
)
|
|
}
|
|
|
|
hitTestSectionTitle(graph: SceneGraph, canvasX: number, canvasY: number): SceneNode | null {
|
|
if (!this.sectionTitleFont) return null
|
|
|
|
const pageNode = graph.getNode(this.pageId ?? graph.rootId)
|
|
if (!pageNode) return null
|
|
|
|
const font = this.sectionTitleFont
|
|
let result: SceneNode | null = null
|
|
|
|
const check = (parentId: string, ox: number, oy: number, insideSection: boolean) => {
|
|
const parent = graph.getNode(parentId)
|
|
if (!parent) return
|
|
for (let i = parent.childIds.length - 1; i >= 0; i--) {
|
|
if (result) return
|
|
const childId = parent.childIds[i]
|
|
const child = graph.getNode(childId)
|
|
if (!child || !child.visible) continue
|
|
const ax = ox + child.x
|
|
const ay = oy + child.y
|
|
if (child.type === 'SECTION') {
|
|
const glyphIds = font.getGlyphIDs(child.name)
|
|
const widths = font.getGlyphWidths(glyphIds)
|
|
let textW = 0
|
|
for (const w of widths) textW += w
|
|
|
|
const pillW =
|
|
Math.min(textW + SECTION_TITLE_PADDING_X * 2, child.width * this.zoom) / this.zoom
|
|
const pillH = SECTION_TITLE_HEIGHT / this.zoom
|
|
const gap = SECTION_TITLE_GAP / this.zoom
|
|
const localX = canvasX - ax
|
|
const localY = canvasY - ay
|
|
|
|
let hitX = localX
|
|
let hitY = localY
|
|
if (child.rotation !== 0) {
|
|
const rad = (-child.rotation * Math.PI) / 180
|
|
const cos = Math.cos(rad)
|
|
const sin = Math.sin(rad)
|
|
hitX = localX * cos - localY * sin
|
|
hitY = localX * sin + localY * cos
|
|
}
|
|
|
|
const pillX = 0
|
|
const pillY = insideSection ? gap : -pillH - gap
|
|
|
|
if (hitX >= pillX && hitX <= pillX + pillW && hitY >= pillY && hitY <= pillY + pillH) {
|
|
result = child
|
|
return
|
|
}
|
|
|
|
check(childId, ax, ay, true)
|
|
} else if (child.childIds.length > 0) {
|
|
check(childId, ax, ay, insideSection)
|
|
}
|
|
}
|
|
}
|
|
|
|
check(pageNode.id, 0, 0, false)
|
|
return result
|
|
}
|
|
|
|
hitTestComponentLabel(graph: SceneGraph, canvasX: number, canvasY: number): SceneNode | null {
|
|
if (!this.componentLabelFont) return null
|
|
|
|
const pageNode = graph.getNode(this.pageId ?? graph.rootId)
|
|
if (!pageNode) return null
|
|
|
|
const font = this.componentLabelFont
|
|
const LABEL_TYPES = new Set(['COMPONENT', 'COMPONENT_SET'])
|
|
let result: SceneNode | null = null
|
|
|
|
const check = (parentId: string, ox: number, oy: number) => {
|
|
const parent = graph.getNode(parentId)
|
|
if (!parent) return
|
|
for (let i = parent.childIds.length - 1; i >= 0; i--) {
|
|
if (result) return
|
|
const childId = parent.childIds[i]
|
|
const child = graph.getNode(childId)
|
|
if (!child || !child.visible) continue
|
|
const ax = ox + child.x
|
|
const ay = oy + child.y
|
|
if (LABEL_TYPES.has(child.type)) {
|
|
const glyphIds = font.getGlyphIDs(child.name)
|
|
const widths = font.getGlyphWidths(glyphIds)
|
|
let textW = 0
|
|
for (const w of widths) textW += w
|
|
const labelW = (COMPONENT_LABEL_ICON_SIZE + COMPONENT_LABEL_ICON_GAP + textW) / this.zoom
|
|
const labelH = COMPONENT_LABEL_FONT_SIZE / this.zoom
|
|
const gap = COMPONENT_LABEL_GAP / this.zoom
|
|
|
|
const isInsideSet = parent.type === 'COMPONENT_SET'
|
|
const labelX = ax
|
|
let labelY: number
|
|
if (isInsideSet) {
|
|
labelY = ay + gap
|
|
} else {
|
|
labelY = ay - labelH - gap
|
|
}
|
|
|
|
if (
|
|
canvasX >= labelX &&
|
|
canvasX <= labelX + labelW &&
|
|
canvasY >= labelY &&
|
|
canvasY <= labelY + labelH
|
|
) {
|
|
result = child
|
|
return
|
|
}
|
|
}
|
|
if (child.childIds.length > 0) {
|
|
check(childId, ax, ay)
|
|
}
|
|
}
|
|
}
|
|
|
|
check(pageNode.id, 0, 0)
|
|
return result
|
|
}
|
|
|
|
hitTestFrameTitle(
|
|
graph: SceneGraph,
|
|
canvasX: number,
|
|
canvasY: number,
|
|
selectedIds: Set<string>
|
|
): SceneNode | null {
|
|
if (!this.labelFont || selectedIds.size !== 1) return null
|
|
|
|
const id = [...selectedIds][0]
|
|
const node = graph.getNode(id)
|
|
if (node?.type !== 'FRAME') return null
|
|
|
|
const parent = node.parentId ? graph.getNode(node.parentId) : null
|
|
const isTopLevel = !parent || parent.type === 'CANVAS' || parent.type === 'SECTION'
|
|
if (!isTopLevel) return null
|
|
|
|
const abs = graph.getAbsolutePosition(id)
|
|
const font = this.labelFont
|
|
const glyphIds = font.getGlyphIDs(node.name)
|
|
const widths = font.getGlyphWidths(glyphIds)
|
|
let textW = 0
|
|
for (const w of widths) textW += w
|
|
|
|
const labelW = textW / this.zoom
|
|
const labelH = LABEL_FONT_SIZE / this.zoom
|
|
const localX = canvasX - abs.x
|
|
const localY = canvasY - abs.y
|
|
|
|
let hitX = localX
|
|
let hitY = localY
|
|
if (node.rotation !== 0) {
|
|
const rad = (-node.rotation * Math.PI) / 180
|
|
const cos = Math.cos(rad)
|
|
const sin = Math.sin(rad)
|
|
hitX = localX * cos - localY * sin
|
|
hitY = localX * sin + localY * cos
|
|
}
|
|
|
|
const labelX = 0
|
|
const labelY = -LABEL_OFFSET_Y / this.zoom - labelH
|
|
|
|
if (hitX >= labelX && hitX <= labelX + labelW && hitY >= labelY && hitY <= labelY + labelH) {
|
|
return node
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
renderSceneToCanvas(canvas: Canvas, graph: SceneGraph, pageId: string): void {
|
|
const prevViewport = this.worldViewport
|
|
this.worldViewport = { x: -1e9, y: -1e9, w: 2e9, h: 2e9 }
|
|
const pageNode = graph.getNode(pageId)
|
|
if (pageNode) {
|
|
for (const childId of pageNode.childIds) {
|
|
this.renderNode(canvas, graph, childId, {})
|
|
}
|
|
}
|
|
this.worldViewport = prevViewport
|
|
}
|
|
|
|
renderFromEditorState(
|
|
state: EditorState,
|
|
graph: SceneGraph,
|
|
textEditor: unknown,
|
|
viewportWidth: number,
|
|
viewportHeight: number,
|
|
showRulers = true
|
|
): void {
|
|
const extendedState = state as EditorState & {
|
|
nodeEditState?: RenderOverlays['nodeEditState']
|
|
}
|
|
this.dpr = IS_BROWSER ? window.devicePixelRatio || 1 : 1
|
|
this.panX = state.panX
|
|
this.panY = state.panY
|
|
this.zoom = state.zoom
|
|
this.viewportWidth = viewportWidth
|
|
this.viewportHeight = viewportHeight
|
|
this.showRulers = showRulers
|
|
this.pageColor = state.pageColor
|
|
this.pageId = state.currentPageId
|
|
this.render(
|
|
graph,
|
|
state.selectedIds,
|
|
{
|
|
hoveredNodeId: state.hoveredNodeId,
|
|
enteredContainerId: state.enteredContainerId,
|
|
editingTextId: state.editingTextId,
|
|
textEditor: textEditor as RenderOverlays['textEditor'],
|
|
marquee: state.marquee,
|
|
snapGuides: state.snapGuides,
|
|
rotationPreview: state.rotationPreview,
|
|
dropTargetId: state.dropTargetId,
|
|
layoutInsertIndicator: state.layoutInsertIndicator,
|
|
penState: state.penState
|
|
? ({
|
|
...state.penState,
|
|
cursorX: state.penCursorX ?? undefined,
|
|
cursorY: state.penCursorY ?? undefined
|
|
} as RenderOverlays['penState'])
|
|
: null,
|
|
nodeEditState: extendedState.nodeEditState ?? null,
|
|
remoteCursors: state.remoteCursors
|
|
},
|
|
state.sceneVersion
|
|
)
|
|
}
|
|
|
|
render(
|
|
graph: SceneGraph,
|
|
selectedIds: Set<string>,
|
|
overlays: RenderOverlays = {},
|
|
sceneVersion = -1
|
|
): void {
|
|
const p = this.profiler
|
|
p.beginFrame()
|
|
|
|
graph.clearAbsPosCache()
|
|
|
|
const canvas = this.surface.getCanvas()
|
|
canvas.clear(this.ck.Color4f(this.pageColor.r, this.pageColor.g, this.pageColor.b, 1))
|
|
|
|
this.worldViewport = {
|
|
x: -this.panX / this.zoom,
|
|
y: -this.panY / this.zoom,
|
|
w: this.viewportWidth / this.zoom,
|
|
h: this.viewportHeight / this.zoom
|
|
}
|
|
|
|
const hasVolatileOverlays =
|
|
overlays.dropTargetId != null ||
|
|
overlays.rotationPreview != null ||
|
|
overlays.editingTextId != null ||
|
|
overlays.nodeEditState != null
|
|
|
|
const canUsePicture =
|
|
!hasVolatileOverlays &&
|
|
this.scenePicture &&
|
|
sceneVersion === this.scenePictureVersion &&
|
|
this.pageId === this.scenePicturePageId
|
|
|
|
p.setCacheHit(!!canUsePicture)
|
|
|
|
canvas.save()
|
|
canvas.scale(this.dpr, this.dpr)
|
|
canvas.translate(this.panX, this.panY)
|
|
canvas.scale(this.zoom, this.zoom)
|
|
|
|
p.beginPhase('render:scene')
|
|
if (canUsePicture) {
|
|
p.beginPhase('render:drawPicture')
|
|
if (this.scenePicture) canvas.drawPicture(this.scenePicture)
|
|
p.endPhase('render:drawPicture')
|
|
} else if (hasVolatileOverlays) {
|
|
this._nodeCount = 0
|
|
this._culledCount = 0
|
|
p.beginPhase('render:volatile')
|
|
const pageNode = graph.getNode(this.pageId ?? graph.rootId)
|
|
if (pageNode) {
|
|
for (const childId of pageNode.childIds) {
|
|
this.renderNode(canvas, graph, childId, overlays, 0, 0)
|
|
}
|
|
}
|
|
p.endPhase('render:volatile')
|
|
} else {
|
|
this._nodeCount = 0
|
|
this._culledCount = 0
|
|
p.beginPhase('render:recordPicture')
|
|
this.recordScenePicture(canvas, graph, sceneVersion)
|
|
p.endPhase('render:recordPicture')
|
|
}
|
|
p.endPhase('render:scene')
|
|
|
|
canvas.restore()
|
|
|
|
canvas.save()
|
|
canvas.scale(this.dpr, this.dpr)
|
|
this.labelCache.update(graph, this.pageId, sceneVersion)
|
|
p.beginPhase('render:sectionTitles')
|
|
this.drawSectionTitles(canvas, graph)
|
|
p.endPhase('render:sectionTitles')
|
|
p.beginPhase('render:componentLabels')
|
|
this.drawComponentLabels(canvas, graph)
|
|
p.endPhase('render:componentLabels')
|
|
canvas.restore()
|
|
|
|
canvas.save()
|
|
canvas.scale(this.dpr, this.dpr)
|
|
|
|
this.drawHoverHighlight(
|
|
canvas,
|
|
graph,
|
|
overlays.hoveredNodeId === overlays.nodeEditState?.nodeId ? null : overlays.hoveredNodeId
|
|
)
|
|
this.drawEnteredContainer(canvas, graph, overlays.enteredContainerId)
|
|
p.beginPhase('render:selection')
|
|
this.drawSelection(canvas, graph, selectedIds, overlays)
|
|
p.endPhase('render:selection')
|
|
this.drawFlashes(canvas, graph)
|
|
this.drawSnapGuides(canvas, overlays.snapGuides)
|
|
this.drawMarquee(canvas, overlays.marquee)
|
|
this.drawLayoutInsertIndicator(canvas, overlays.layoutInsertIndicator)
|
|
this.drawNodeEditOverlay(canvas, graph, overlays.nodeEditState)
|
|
this.drawPenOverlay(canvas, overlays.penState)
|
|
this.drawRemoteCursors(canvas, graph, overlays.remoteCursors)
|
|
p.beginPhase('render:rulers')
|
|
if (this.showRulers) this.drawRulers(canvas, graph, selectedIds)
|
|
p.endPhase('render:rulers')
|
|
|
|
p.drawHUD(canvas, this.showRulers)
|
|
|
|
canvas.restore()
|
|
|
|
p.beginPhase('render:flush')
|
|
this.surface.flush()
|
|
p.endPhase('render:flush')
|
|
|
|
p.setNodeCounts(this._nodeCount, this._culledCount)
|
|
p.endFrame()
|
|
}
|
|
|
|
private recordScenePicture(canvas: Canvas, graph: SceneGraph, sceneVersion: number): void {
|
|
this.scenePicture?.delete()
|
|
const prevViewport = this.worldViewport
|
|
this.worldViewport = { x: -1e6, y: -1e6, w: 2e6, h: 2e6 }
|
|
const recorder = new this.ck.PictureRecorder()
|
|
const pageNode = graph.getNode(this.pageId ?? graph.rootId)
|
|
const sceneNodes = pageNode
|
|
? pageNode.childIds
|
|
.map((childId) => graph.getNode(childId))
|
|
.filter((node): node is SceneNode => node != null)
|
|
: []
|
|
const sceneBounds =
|
|
sceneNodes.length > 0
|
|
? computeVisualBounds(sceneNodes, (id) => graph.getAbsolutePosition(id))
|
|
: { x: 0, y: 0, width: 1, height: 1 }
|
|
const padding = 1024
|
|
const bounds = this.ck.LTRBRect(
|
|
sceneBounds.x - padding,
|
|
sceneBounds.y - padding,
|
|
sceneBounds.x + sceneBounds.width + padding,
|
|
sceneBounds.y + sceneBounds.height + padding
|
|
)
|
|
const recCanvas = recorder.beginRecording(bounds)
|
|
if (pageNode) {
|
|
for (const childId of pageNode.childIds) {
|
|
this.renderNode(recCanvas, graph, childId, {}, 0, 0)
|
|
}
|
|
}
|
|
this.scenePicture = recorder.finishRecordingAsPicture()
|
|
recorder.delete()
|
|
this.worldViewport = prevViewport
|
|
this.scenePictureVersion = sceneVersion
|
|
this.scenePicturePageId = this.pageId
|
|
canvas.drawPicture(this.scenePicture)
|
|
}
|
|
|
|
invalidateVectorPath(nodeId: string): void {
|
|
const old = this.vectorPathCache.get(nodeId)
|
|
if (old) {
|
|
for (const p of old) p.delete()
|
|
this.vectorPathCache.delete(nodeId)
|
|
}
|
|
for (const cache of [this.fillGeometryCache, this.strokeGeometryCache]) {
|
|
const oldGeom = cache.get(nodeId)
|
|
if (oldGeom) {
|
|
for (const p of oldGeom) p.delete()
|
|
cache.delete(nodeId)
|
|
}
|
|
}
|
|
}
|
|
|
|
measureTextNode(node: SceneNode, maxWidth?: number): { width: number; height: number } | null {
|
|
return measureTextNodeFn(this, node, maxWidth)
|
|
}
|
|
|
|
isNodeFontLoaded(node: SceneNode): boolean {
|
|
return isNodeFontLoadedFn(this, node)
|
|
}
|
|
|
|
buildTextPicture(node: SceneNode): Uint8Array | null {
|
|
return buildTextPictureFn(this, node)
|
|
}
|
|
|
|
buildParagraph(
|
|
node: SceneNode,
|
|
color?: Float32Array,
|
|
opts?: { halfLeading?: boolean }
|
|
): Paragraph {
|
|
return buildParagraphFn(this, node, color, opts)
|
|
}
|
|
|
|
resolveFillColorInfo(
|
|
fill: Fill,
|
|
fillIndex: number,
|
|
node: SceneNode,
|
|
graph: SceneGraph
|
|
): ResolvedRenderColor {
|
|
const varId = node.boundVariables[`fills/${fillIndex}/color`]
|
|
if (varId) {
|
|
const resolved = graph.resolveColorVariable(varId)
|
|
if (resolved) {
|
|
return {
|
|
color: resolved,
|
|
cssColor: '',
|
|
sourceSpace: 'srgb',
|
|
targetSpace: graph.documentColorSpace,
|
|
clipped: false
|
|
}
|
|
}
|
|
}
|
|
return resolveNodeFillColor(fill, fillIndex, node, {
|
|
documentColorSpace: graph.documentColorSpace
|
|
})
|
|
}
|
|
|
|
resolveFillColor(fill: Fill, fillIndex: number, node: SceneNode, graph: SceneGraph): Color {
|
|
return this.resolveFillColorInfo(fill, fillIndex, node, graph).color
|
|
}
|
|
|
|
resolveStrokeColorInfo(
|
|
stroke: Stroke,
|
|
strokeIndex: number,
|
|
node: SceneNode,
|
|
graph: SceneGraph
|
|
): ResolvedRenderColor {
|
|
const varId = node.boundVariables[`strokes/${strokeIndex}/color`]
|
|
if (varId) {
|
|
const resolved = graph.resolveColorVariable(varId)
|
|
if (resolved) {
|
|
return {
|
|
color: resolved,
|
|
cssColor: '',
|
|
sourceSpace: 'srgb',
|
|
targetSpace: graph.documentColorSpace,
|
|
clipped: false
|
|
}
|
|
}
|
|
}
|
|
return resolveNodeStrokeColor(stroke, strokeIndex, node, {
|
|
documentColorSpace: graph.documentColorSpace
|
|
})
|
|
}
|
|
|
|
resolveStrokeColor(
|
|
stroke: Stroke,
|
|
strokeIndex: number,
|
|
node: SceneNode,
|
|
graph: SceneGraph
|
|
): Color {
|
|
return this.resolveStrokeColorInfo(stroke, strokeIndex, node, graph).color
|
|
}
|
|
|
|
screenToCanvas(sx: number, sy: number): Vector {
|
|
return {
|
|
x: (sx - this.panX) / this.zoom,
|
|
y: (sy - this.panY) / this.zoom
|
|
}
|
|
}
|
|
|
|
destroyed = false
|
|
|
|
destroy(): void {
|
|
if (this.destroyed) return
|
|
this.destroyed = true
|
|
|
|
for (const img of this.imageCache.values()) img.delete()
|
|
this.imageCache.clear()
|
|
for (const cache of [this.vectorPathCache, this.fillGeometryCache, this.strokeGeometryCache]) {
|
|
for (const paths of cache.values()) {
|
|
for (const p of paths) p.delete()
|
|
}
|
|
cache.clear()
|
|
}
|
|
this.fillPaint.delete()
|
|
this.strokePaint.delete()
|
|
this.selectionPaint.delete()
|
|
this.snapPaint.delete()
|
|
this.auxFill.delete()
|
|
this.auxStroke.delete()
|
|
this.opacityPaint.delete()
|
|
this.textFont?.delete()
|
|
this.labelFont?.delete()
|
|
this.sizeFont?.delete()
|
|
this.fontMgr?.delete()
|
|
this.fontProvider?.delete()
|
|
this.rulerBgPaint.delete()
|
|
this.rulerTickPaint.delete()
|
|
this.rulerTextPaint.delete()
|
|
this.rulerHlPaint.delete()
|
|
this.rulerBadgePaint.delete()
|
|
this.rulerLabelPaint.delete()
|
|
this.penPathPaint.delete()
|
|
this.penLiveStrokePaint.delete()
|
|
this.penHandlePaint.delete()
|
|
this.penVertexFill.delete()
|
|
this.penVertexStroke.delete()
|
|
this.effectLayerPaint.delete()
|
|
for (const filter of this.imageFilterCache.values()) filter?.delete()
|
|
this.imageFilterCache.clear()
|
|
for (const filter of this.maskFilterCache.values()) filter?.delete()
|
|
this.maskFilterCache.clear()
|
|
for (const pic of this.nodePictureCache.values()) pic?.delete()
|
|
this.nodePictureCache.clear()
|
|
this.scenePicture?.delete()
|
|
this._flashPaint?.delete()
|
|
this.profiler.destroy()
|
|
this.surface.delete()
|
|
}
|
|
|
|
// --- Delegation methods ---
|
|
|
|
private drawHoverHighlight(
|
|
canvas: Canvas,
|
|
graph: SceneGraph,
|
|
hoveredNodeId?: string | null
|
|
): void {
|
|
drawHoverHighlightFn(this, canvas, graph, hoveredNodeId)
|
|
}
|
|
|
|
private drawEnteredContainer(
|
|
canvas: Canvas,
|
|
graph: SceneGraph,
|
|
enteredContainerId?: string | null
|
|
): void {
|
|
drawEnteredContainerFn(this, canvas, graph, enteredContainerId)
|
|
}
|
|
|
|
private drawSelection(
|
|
canvas: Canvas,
|
|
graph: SceneGraph,
|
|
selectedIds: Set<string>,
|
|
overlays: RenderOverlays
|
|
): void {
|
|
drawSelectionFn(this, canvas, graph, selectedIds, overlays)
|
|
}
|
|
|
|
drawNodeSelection(canvas: Canvas, node: SceneNode, rotation: number, graph: SceneGraph): void {
|
|
drawNodeSelectionFn(this, canvas, node, rotation, graph)
|
|
}
|
|
|
|
drawSelectionLabels(
|
|
canvas: Canvas,
|
|
graph: SceneGraph,
|
|
selectedIds: Set<string>,
|
|
overlays?: RenderOverlays
|
|
): void {
|
|
drawSelectionLabelsFn(this, canvas, graph, selectedIds, overlays)
|
|
}
|
|
|
|
drawParentFrameOutlines(canvas: Canvas, graph: SceneGraph, selectedIds: Set<string>): void {
|
|
drawParentFrameOutlinesFn(this, canvas, graph, selectedIds)
|
|
}
|
|
|
|
drawNodeOutline(canvas: Canvas, node: SceneNode, rotation: number, graph: SceneGraph): void {
|
|
drawNodeOutlineFn(this, canvas, node, rotation, graph)
|
|
}
|
|
|
|
drawGroupBounds(canvas: Canvas, nodes: SceneNode[], graph: SceneGraph): void {
|
|
drawGroupBoundsFn(this, canvas, nodes, graph)
|
|
}
|
|
|
|
getRotatedCorners(n: SceneNode, abs: Vector): Vector[] {
|
|
return getRotatedCornersFn(this, n, abs)
|
|
}
|
|
|
|
drawHandle(canvas: Canvas, x: number, y: number): void {
|
|
drawHandleFn(this, canvas, x, y)
|
|
}
|
|
|
|
private drawSnapGuides(canvas: Canvas, guides?: SnapGuide[]): void {
|
|
drawSnapGuidesFn(this, canvas, guides)
|
|
}
|
|
|
|
private drawMarquee(canvas: Canvas, marquee?: Rect | null): void {
|
|
drawMarqueeFn(this, canvas, marquee)
|
|
}
|
|
|
|
private drawFlashes(canvas: Canvas, graph: SceneGraph): void {
|
|
drawFlashesFn(this, canvas, graph)
|
|
drawAiOverlaysFn(this, canvas, graph)
|
|
}
|
|
|
|
private drawLayoutInsertIndicator(
|
|
canvas: Canvas,
|
|
indicator?: RenderOverlays['layoutInsertIndicator']
|
|
): void {
|
|
drawLayoutInsertIndicatorFn(this, canvas, indicator)
|
|
}
|
|
|
|
drawTextEditOverlay(canvas: Canvas, node: SceneNode, editor: TextEditor): void {
|
|
drawTextEditOverlayFn(this, canvas, node, editor)
|
|
}
|
|
|
|
private drawNodeEditOverlay(
|
|
canvas: Canvas,
|
|
graph: SceneGraph,
|
|
editState: RenderOverlays['nodeEditState']
|
|
): void {
|
|
drawNodeEditOverlayFn(this, canvas, graph, editState as NodeEditOverlayState | null)
|
|
}
|
|
|
|
private drawPenOverlay(canvas: Canvas, penState: RenderOverlays['penState']): void {
|
|
drawPenOverlayFn(this, canvas, penState)
|
|
}
|
|
|
|
private drawRemoteCursors(
|
|
canvas: Canvas,
|
|
graph: SceneGraph,
|
|
cursors?: RenderOverlays['remoteCursors']
|
|
): void {
|
|
drawRemoteCursorsFn(this, canvas, graph, cursors)
|
|
}
|
|
|
|
private drawRulers(canvas: Canvas, graph: SceneGraph, selectedIds: Set<string>): void {
|
|
drawRulersFn(this, canvas, graph, selectedIds)
|
|
}
|
|
|
|
private drawSectionTitles(canvas: Canvas, graph: SceneGraph): void {
|
|
drawSectionTitlesFn(this, canvas, graph)
|
|
}
|
|
|
|
private drawComponentLabels(canvas: Canvas, graph: SceneGraph): void {
|
|
drawComponentLabelsFn(this, canvas, graph)
|
|
}
|
|
|
|
renderNode(
|
|
canvas: Canvas,
|
|
graph: SceneGraph,
|
|
nodeId: string,
|
|
overlays: RenderOverlays,
|
|
parentAbsX = 0,
|
|
parentAbsY = 0
|
|
): void {
|
|
renderNodeFn(this, canvas, graph, nodeId, overlays, parentAbsX, parentAbsY)
|
|
}
|
|
|
|
renderSection(canvas: Canvas, node: SceneNode, graph: SceneGraph): void {
|
|
renderSectionFn(this, canvas, node, graph)
|
|
}
|
|
|
|
renderComponentSet(canvas: Canvas, node: SceneNode, graph: SceneGraph): void {
|
|
renderComponentSetFn(this, canvas, node, graph)
|
|
}
|
|
|
|
renderShape(canvas: Canvas, node: SceneNode, graph: SceneGraph): void {
|
|
renderShapeFn(this, canvas, node, graph)
|
|
}
|
|
|
|
renderShapeUncached(canvas: Canvas, node: SceneNode, graph: SceneGraph): void {
|
|
renderShapeUncachedFn(this, canvas, node, graph)
|
|
}
|
|
|
|
renderEffects(
|
|
canvas: Canvas,
|
|
node: SceneNode,
|
|
rect: Float32Array,
|
|
hasRadius: boolean,
|
|
pass: 'behind' | 'front',
|
|
shadowShapeChild?: SceneNode | null
|
|
): void {
|
|
renderEffectsFn(this, canvas, node, rect, hasRadius, pass, shadowShapeChild)
|
|
}
|
|
|
|
renderText(canvas: Canvas, node: SceneNode): void {
|
|
renderTextFn(this, canvas, node)
|
|
}
|
|
|
|
drawNodeFill(canvas: Canvas, node: SceneNode, rect: Float32Array, hasRadius: boolean): void {
|
|
drawNodeFillFn(this, canvas, node, rect, hasRadius)
|
|
}
|
|
|
|
applyFill(fill: Fill, node: SceneNode, graph: SceneGraph, fillIndex = 0): boolean {
|
|
return applyFillFn(this, fill, node, graph, fillIndex)
|
|
}
|
|
|
|
applyGradientFill(fill: Fill, node: SceneNode, graph: SceneGraph): void {
|
|
applyGradientFillFn(this, fill, node, graph)
|
|
}
|
|
|
|
applyImageFill(fill: Fill, node: SceneNode, graph: SceneGraph): boolean {
|
|
return applyImageFillFn(this, fill, node, graph)
|
|
}
|
|
|
|
drawArc(canvas: Canvas, node: SceneNode, paint: Paint): void {
|
|
drawArcFn(this, canvas, node, paint)
|
|
}
|
|
|
|
drawNodeStroke(canvas: Canvas, node: SceneNode, rect: Float32Array, hasRadius: boolean): void {
|
|
drawNodeStrokeFn(this, canvas, node, rect, hasRadius)
|
|
}
|
|
|
|
drawStrokeWithAlign(
|
|
canvas: Canvas,
|
|
node: SceneNode,
|
|
rect: Float32Array,
|
|
hasRadius: boolean,
|
|
align: 'INSIDE' | 'CENTER' | 'OUTSIDE'
|
|
): void {
|
|
drawStrokeWithAlignFn(this, canvas, node, rect, hasRadius, align)
|
|
}
|
|
|
|
drawRRectStrokeWithAlign(
|
|
canvas: Canvas,
|
|
rrect: Float32Array,
|
|
node: SceneNode,
|
|
stroke: Stroke
|
|
): void {
|
|
drawRRectStrokeWithAlignFn(this, canvas, rrect, node, stroke)
|
|
}
|
|
|
|
drawIndividualSideStrokes(
|
|
canvas: Canvas,
|
|
node: SceneNode,
|
|
align: 'INSIDE' | 'CENTER' | 'OUTSIDE'
|
|
): void {
|
|
drawIndividualSideStrokesFn(this, canvas, node, align)
|
|
}
|
|
|
|
strokeNodeShape(canvas: Canvas, node: SceneNode, paint: Paint): void {
|
|
strokeNodeShapeFn(this, canvas, node, paint)
|
|
}
|
|
|
|
makeNodeShapePath(node: SceneNode, rect: Float32Array, hasRadius: boolean): Path {
|
|
return makeNodeShapePathFn(this, node, rect, hasRadius)
|
|
}
|
|
|
|
makePolygonPath(node: SceneNode): Path {
|
|
return makePolygonPathFn(this, node)
|
|
}
|
|
|
|
makeRRect(node: SceneNode): Float32Array {
|
|
return makeRRectFn(this, node)
|
|
}
|
|
|
|
makeRRectWithSpread(node: SceneNode, spread: number): Float32Array {
|
|
return makeRRectWithSpreadFn(this, node, spread)
|
|
}
|
|
|
|
makeRRectWithOffset(node: SceneNode, ox: number, oy: number, spread: number): Float32Array {
|
|
return makeRRectWithOffsetFn(this, node, ox, oy, spread)
|
|
}
|
|
|
|
clipNodeShape(canvas: Canvas, node: SceneNode, rect: Float32Array, hasRadius: boolean): void {
|
|
clipNodeShapeFn(this, canvas, node, rect, hasRadius)
|
|
}
|
|
|
|
getVectorPaths(node: SceneNode): Path[] | null {
|
|
return getVectorPathsFn(this, node)
|
|
}
|
|
|
|
getFillGeometry(node: SceneNode): Path[] | null {
|
|
return getFillGeometryFn(this, node)
|
|
}
|
|
|
|
getStrokeGeometry(node: SceneNode): Path[] | null {
|
|
return getStrokeGeometryFn(this, node)
|
|
}
|
|
|
|
getCachedDropShadow(dx: number, dy: number, sigma: number, color: Float32Array): ImageFilter {
|
|
return getCachedDropShadowFn(this, dx, dy, sigma, color)
|
|
}
|
|
|
|
getCachedBlur(sigma: number): ImageFilter {
|
|
return getCachedBlurFn(this, sigma)
|
|
}
|
|
|
|
getCachedDecalBlur(sigma: number): ImageFilter {
|
|
return getCachedDecalBlurFn(this, sigma)
|
|
}
|
|
|
|
getCachedMaskBlur(sigma: number): MaskFilter {
|
|
return getCachedMaskBlurFn(this, sigma)
|
|
}
|
|
|
|
applyClippedBlur(
|
|
canvas: Canvas,
|
|
node: SceneNode,
|
|
rect: Float32Array,
|
|
hasRadius: boolean,
|
|
sigma: number
|
|
): void {
|
|
applyClippedBlurFn(this, canvas, node, rect, hasRadius, sigma)
|
|
}
|
|
}
|