diff --git a/packages/core/src/canvas/renderer/pipeline.ts b/packages/core/src/canvas/renderer/pipeline.ts index 9611894ad..1bf43c78e 100644 --- a/packages/core/src/canvas/renderer/pipeline.ts +++ b/packages/core/src/canvas/renderer/pipeline.ts @@ -38,9 +38,6 @@ export function renderFromEditorState( dpr = 1, layer: RenderLayer = 'full' ): void { - const extendedState = state as EditorState & { - nodeEditState?: RenderOverlays['nodeEditState'] - } r.dpr = dpr r.panX = state.panX r.panY = state.panY @@ -72,7 +69,7 @@ export function renderFromEditorState( cursorY: state.penCursorY ?? undefined } as RenderOverlays['penState']) : null, - nodeEditState: extendedState.nodeEditState ?? null, + nodeEditState: state.nodeEditState ?? null, remoteCursors: state.remoteCursors }, state.sceneVersion, diff --git a/packages/core/src/editor/shapes/pen.ts b/packages/core/src/editor/shapes/pen.ts index 6e97b006d..8a513b1e6 100644 --- a/packages/core/src/editor/shapes/pen.ts +++ b/packages/core/src/editor/shapes/pen.ts @@ -1,5 +1,5 @@ import type { EditorContext } from '#core/editor/types' -import type { Fill, SceneNode, VectorNetwork, VectorRegion, VectorSegment } from '#core/scene-graph' +import type { SceneNode, VectorNetwork, VectorRegion, VectorSegment } from '#core/scene-graph' import type { Vector } from '#core/types' import { computeVectorBounds } from '#core/vector' @@ -189,13 +189,9 @@ export function createPenActions(ctx: EditorContext, createShape: CreateShape) { regions: network.regions } - const penStyle = ps as typeof ps & { - resumedFills?: Fill[] - resumedStrokes?: SceneNode['strokes'] - } - const fills = penStyle.resumedFills ? penStyle.resumedFills.map((f) => ({ ...f })) : [] - const strokes = penStyle.resumedStrokes - ? penStyle.resumedStrokes.map((s) => ({ ...s })) + const fills = ps.resumedFills ? ps.resumedFills.map((f) => ({ ...f })) : [] + const strokes = ps.resumedStrokes + ? ps.resumedStrokes.map((s) => ({ ...s })) : [{ ...PEN_DEFAULT_STROKE }] const nodeId = createShape('VECTOR', bounds.x, bounds.y, bounds.width, bounds.height) diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index 070e95891..35285a7aa 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -1,6 +1,7 @@ import type { CanvasKit } from 'canvaskit-wasm' import type { RulerTheme, SkiaRenderer } from '#core/canvas/renderer' +import type { RenderOverlays } from '#core/canvas/renderer/types' import type { SceneGraph, SceneNode, VectorSegment, VectorVertex } from '#core/scene-graph' import type { SnapGuide } from '#core/scene-graph/snap' import type { UndoManager } from '#core/scene-graph/undo' @@ -45,6 +46,9 @@ export interface EditorState { oppositeDragTangent: Vector | null pendingClose?: boolean closingToFirst: boolean + resumingNodeId?: string + resumedFills?: SceneNode['fills'] + resumedStrokes?: SceneNode['strokes'] } | null penCursorX: number | null penCursorY: number | null @@ -65,6 +69,9 @@ export interface EditorState { sceneVersion: number loading: boolean enteredContainerId: string | null + nodeEditState?: RenderOverlays['nodeEditState'] | null + cursorCanvasX?: number | null + cursorCanvasY?: number | null } export interface EditorEvents { diff --git a/packages/core/src/lint/linter.ts b/packages/core/src/lint/linter.ts index 5395cdc12..989162d04 100644 --- a/packages/core/src/lint/linter.ts +++ b/packages/core/src/lint/linter.ts @@ -17,7 +17,7 @@ export class Linter { private rules = new Map() private ruleConfigs = new Map }>() private messages: LintMessage[] = [] - private nodes = new Map() + private nodes = new Map() constructor(options: { config?: LintConfig; preset?: string; rules?: string[] } = {}) { let baseConfig: Record< @@ -61,8 +61,8 @@ export class Linter { const raw = graph.getNode(id) if (!raw) return const node = this.toLintNode(raw) - ;(node as LintNode & { parent?: LintNode }).parent = parent - this.nodes.set(id, node as LintNode & { parent?: LintNode }) + node.parent = parent + this.nodes.set(id, node) for (const childId of raw.childIds) this.capture(graph, childId, node) } @@ -125,7 +125,7 @@ export class Linter { message, nodeId: node.id, nodeName: node.name, - nodePath: getNodePath(this.nodes.get(node.id) as LintNode & { parent?: LintNode }), + nodePath: getNodePath(this.nodes.get(node.id) ?? node), suggest }) }, diff --git a/packages/core/src/lint/rules/no-hardcoded-colors.ts b/packages/core/src/lint/rules/no-hardcoded-colors.ts index f419fa1bd..ba3dcedb3 100644 --- a/packages/core/src/lint/rules/no-hardcoded-colors.ts +++ b/packages/core/src/lint/rules/no-hardcoded-colors.ts @@ -19,7 +19,14 @@ export default defineRule({ 'INSTANCE' ], check(node, context) { - const checkPaints = (paints: typeof node.fills, field: 'fills' | 'strokes') => { + const checkPaints = ( + paints: ReadonlyArray<{ + visible: boolean + color?: { r: number; g: number; b: number } + type?: string + }>, + field: 'fills' | 'strokes' + ) => { for (const paint of paints) { if (paint.type !== 'SOLID' || !paint.visible || !paint.color || node.boundVariables[field]) continue @@ -31,6 +38,6 @@ export default defineRule({ } } checkPaints(node.fills, 'fills') - checkPaints(node.strokes as typeof node.fills, 'strokes') + checkPaints(node.strokes, 'strokes') } }) diff --git a/packages/core/src/lint/types.ts b/packages/core/src/lint/types.ts index e99512b29..2d3a2f5d6 100644 --- a/packages/core/src/lint/types.ts +++ b/packages/core/src/lint/types.ts @@ -78,6 +78,7 @@ export interface LintNode { visible: boolean radius: number }> + parent?: LintNode } export interface RuleContext { diff --git a/packages/core/src/lint/utils.ts b/packages/core/src/lint/utils.ts index 053e1c0d1..4be5a5b64 100644 --- a/packages/core/src/lint/utils.ts +++ b/packages/core/src/lint/utils.ts @@ -10,15 +10,17 @@ export function isMultipleOf(value: number, base: number, tolerance = 0.01): boo return remainder < tolerance || base - remainder < tolerance } -export function getNodePath(node: { +interface LintPathNode { name: string - parent?: { name: string; parent?: unknown } -}): string[] { + parent?: LintPathNode +} + +export function getNodePath(node: LintPathNode): string[] { const path: string[] = [] - let current: typeof node | undefined = node + let current: LintPathNode | undefined = node while (current) { path.unshift(current.name) - current = current.parent as typeof node | undefined + current = current.parent } return path } diff --git a/packages/vue/src/canvas/node-edit-input/bend.ts b/packages/vue/src/canvas/node-edit-input/bend.ts index b42fc4c86..190f1b9ed 100644 --- a/packages/vue/src/canvas/node-edit-input/bend.ts +++ b/packages/vue/src/canvas/node-edit-input/bend.ts @@ -28,8 +28,7 @@ export type CanvasNodeEditMethods = Partial<{ }> export function getCanvasNodeEditState(editor: Editor): CanvasNodeEditState | null | undefined { - return (editor.state as Editor['state'] & { nodeEditState?: CanvasNodeEditState | null }) - .nodeEditState + return editor.state.nodeEditState as CanvasNodeEditState | null | undefined } export function resolveBendTargetHandle( diff --git a/packages/vue/src/canvas/pen-input/use.ts b/packages/vue/src/canvas/pen-input/use.ts index 644c109af..3d11900c7 100644 --- a/packages/vue/src/canvas/pen-input/use.ts +++ b/packages/vue/src/canvas/pen-input/use.ts @@ -7,8 +7,6 @@ import { createPenDrag, handlePenDragMove } from '#vue/canvas/pen-input/drag' import { handlePenNodeEditDown } from '#vue/shared/input/node-edit' import type { DragState } from '#vue/shared/input/types' -type NodeEditState = NonNullable & { nodeEditState?: object | null } - type SetDrag = (drag: DragState) => void export function startPenInput( @@ -22,7 +20,7 @@ export function startPenInput( editor.state.penCursorX = null editor.state.penCursorY = null - const nodeEditState = (editor.state as NodeEditState).nodeEditState + const nodeEditState = editor.state.nodeEditState if (nodeEditState) { handlePenNodeEditDown(e, cx, cy, editor) return true diff --git a/packages/vue/src/canvas/surface/gl-surface.ts b/packages/vue/src/canvas/surface/gl-surface.ts index b878bdadf..904542f69 100644 --- a/packages/vue/src/canvas/surface/gl-surface.ts +++ b/packages/vue/src/canvas/surface/gl-surface.ts @@ -12,10 +12,9 @@ export function sizeCanvas(canvas: HTMLCanvasElement, editor: Editor) { const dpr = window.devicePixelRatio || 1 canvas.width = canvas.clientWidth * dpr canvas.height = canvas.clientHeight * dpr - const maybeSizedEditor = editor as Editor & { - setViewportSize?: (width: number, height: number) => void + if ('setViewportSize' in editor && typeof editor.setViewportSize === 'function') { + editor.setViewportSize(canvas.clientWidth, canvas.clientHeight) } - maybeSizedEditor.setViewportSize?.(canvas.clientWidth, canvas.clientHeight) } export function makeGLSurface( diff --git a/packages/vue/src/shared/input/node-edit/hit-test.ts b/packages/vue/src/shared/input/node-edit/hit-test.ts index 416ee9732..2363f10ff 100644 --- a/packages/vue/src/shared/input/node-edit/hit-test.ts +++ b/packages/vue/src/shared/input/node-edit/hit-test.ts @@ -14,10 +14,7 @@ export type NodeEditState = { } export function getNodeEditState(editor: Editor): NodeEditState | null { - return ( - (editor.state as Editor['state'] & { nodeEditState?: NodeEditState | null }).nodeEditState ?? - null - ) + return (editor.state.nodeEditState as NodeEditState | null) ?? null } export const NODE_HIT_THRESHOLD = 8 diff --git a/src/app/document/autosave/create.ts b/src/app/document/autosave/create.ts index bcb422d07..9a43da051 100644 --- a/src/app/document/autosave/create.ts +++ b/src/app/document/autosave/create.ts @@ -1,5 +1,4 @@ -import { useDebounceFn } from '@vueuse/core' -import { watch } from 'vue' +import { watchDebounced } from '@vueuse/core' import type { EditorState } from '@open-pencil/core/editor' @@ -18,29 +17,20 @@ export function createAutosave({ hasWritableSource, saveCurrentDocument }: AutosaveOptions) { - const debouncedAutosave = useDebounceFn(async () => { - if (state.sceneVersion === getSavedVersion()) return - if (!state.autosaveEnabled) return - try { - await saveCurrentDocument() - } catch (e) { - console.warn('Autosave failed:', e) - } - }, 3000) - - watch( + const stop = watchDebounced( () => state.sceneVersion, - (version) => { + async (version) => { if (version === getSavedVersion()) return if (!state.autosaveEnabled) return if (!hasWritableSource()) return - void debouncedAutosave() - } + try { + await saveCurrentDocument() + } catch (e) { + console.warn('Autosave failed:', e) + } + }, + { debounce: 3000 } ) - function disposeAutosave() { - ;(debouncedAutosave as typeof debouncedAutosave & { cancel?: () => void }).cancel?.() - } - - return { disposeAutosave } + return { disposeAutosave: stop } } diff --git a/src/app/editor/pen/resume.ts b/src/app/editor/pen/resume.ts index 32dae20cc..6e15b93b3 100644 --- a/src/app/editor/pen/resume.ts +++ b/src/app/editor/pen/resume.ts @@ -1,15 +1,8 @@ import type { EditorState } from '@open-pencil/core/editor' -import type { Fill, SceneNode, VectorSegment, VectorVertex } from '@open-pencil/core/scene-graph' +import type { SceneNode, VectorSegment, VectorVertex } from '@open-pencil/core/scene-graph' -type ResumablePenState = NonNullable & { - resumingNodeId?: string - resumedFills?: Fill[] - resumedStrokes?: SceneNode['strokes'] -} - -export type PenState = Omit & { - penState: ResumablePenState | null -} +export type PenState = EditorState +type PenStateInit = NonNullable export function absoluteVertices(node: SceneNode, vertices: VectorVertex[]): VectorVertex[] { return vertices.map((v) => ({ @@ -31,7 +24,7 @@ export function createResumedPenState( node: SceneNode, vertices: VectorVertex[], segments: VectorSegment[] -): ResumablePenState { +): PenStateInit { return { vertices, segments, diff --git a/src/app/editor/session/types.ts b/src/app/editor/session/types.ts index 75dce8cfd..956273f17 100644 --- a/src/app/editor/session/types.ts +++ b/src/app/editor/session/types.ts @@ -1,6 +1,4 @@ import { createDefaultEditorState, type EditorState } from '@open-pencil/core/editor' -import type { Fill, SceneNode, VectorSegment, VectorVertex } from '@open-pencil/core/scene-graph' -import type { Vector } from '@open-pencil/core/types' import type { NodeEditState } from '@/app/editor/vector-edit/types' @@ -23,18 +21,7 @@ export function createInitialAppEditorState(pageId: string): AppEditorState { } } -export type AppEditorState = Omit & { - penState: { - vertices: VectorVertex[] - segments: VectorSegment[] - dragTangent: Vector | null - oppositeDragTangent: Vector | null - closingToFirst: boolean - pendingClose?: boolean - resumingNodeId?: string - resumedFills?: Fill[] - resumedStrokes?: SceneNode['strokes'] - } | null +export type AppEditorState = EditorState & { showUI: boolean showRulers: boolean showRemoteCursors: boolean