refactor: remove ugly type intersection casts
- Add optional nodeEditState, cursorCanvas, and pen resume fields to core EditorState - Add parent field to LintNode instead of repeated intersection casts - Use typed LintPathNode for node path traversal - Replace autosave useDebounceFn cancel hack with watchDebounced - Simplify AppEditorState to extend EditorState without Omit+intersection - Remove PenState intersection type alias - Use in-operator check for optional setViewportSize
This commit is contained in:
parent
7dce333200
commit
738d199e8a
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export class Linter {
|
|||
private rules = new Map<string, Rule>()
|
||||
private ruleConfigs = new Map<string, { severity: Severity; options?: Record<string, unknown> }>()
|
||||
private messages: LintMessage[] = []
|
||||
private nodes = new Map<string, LintNode & { parent?: LintNode }>()
|
||||
private nodes = new Map<string, LintNode>()
|
||||
|
||||
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
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export interface LintNode {
|
|||
visible: boolean
|
||||
radius: number
|
||||
}>
|
||||
parent?: LintNode
|
||||
}
|
||||
|
||||
export interface RuleContext {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Editor['state']> & { 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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EditorState['penState']> & {
|
||||
resumingNodeId?: string
|
||||
resumedFills?: Fill[]
|
||||
resumedStrokes?: SceneNode['strokes']
|
||||
}
|
||||
|
||||
export type PenState = Omit<EditorState, 'penState'> & {
|
||||
penState: ResumablePenState | null
|
||||
}
|
||||
export type PenState = EditorState
|
||||
type PenStateInit = NonNullable<EditorState['penState']>
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<EditorState, 'penState'> & {
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue