Add custom JS lint rules, replace inline types with named imports
Three oxlint JS plugin rules in lint/plugin.js:
- no-inline-named-types: configurable shape→name map catches inline
{ x: number; y: number } etc. and suggests Vector, Color, GUID,
Rect, Matrix from @open-pencil/core
- no-structuredclone-scene-arrays: flags structuredClone on
fills/strokes/effects — use typed copy helpers instead
- no-math-random: bans Math.random() in favor of crypto
Fixed 40 violations across 13 files: replaced inline type literals
with named imports, switched figma-api.ts structuredClone calls to
copyFills/copyStrokes/copyEffects.
This commit is contained in:
parent
4969447669
commit
cd614ef8f7
132
lint/plugin.js
Normal file
132
lint/plugin.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
const noInlineNamedTypes = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow inline type literals that duplicate a named type',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const typesOption = context.options[0]
|
||||
if (!typesOption || typeof typesOption !== 'object') return {}
|
||||
|
||||
const shapeToName = new Map()
|
||||
for (const [name, shape] of Object.entries(typesOption)) {
|
||||
shapeToName.set(shape, name)
|
||||
}
|
||||
|
||||
return {
|
||||
TSTypeLiteral(node) {
|
||||
const props = node.members?.filter(
|
||||
(m) => m.type === 'TSPropertySignature' && m.key?.type === 'Identifier',
|
||||
)
|
||||
if (!props || props.length < 2) return
|
||||
|
||||
if (props.some((m) => m.optional)) return
|
||||
|
||||
const shape = props
|
||||
.map((m) => {
|
||||
const typeNode = m.typeAnnotation?.typeAnnotation
|
||||
let typeName = 'unknown'
|
||||
if (typeNode) {
|
||||
switch (typeNode.type) {
|
||||
case 'TSNumberKeyword': typeName = 'number'; break
|
||||
case 'TSStringKeyword': typeName = 'string'; break
|
||||
case 'TSBooleanKeyword': typeName = 'boolean'; break
|
||||
}
|
||||
}
|
||||
return `${m.key.name}:${typeName}`
|
||||
})
|
||||
.sort()
|
||||
.join(',')
|
||||
|
||||
const namedType = shapeToName.get(shape)
|
||||
if (namedType) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Use '${namedType}' instead of inline type literal. Import from '@open-pencil/core'.`,
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const noStructuredCloneSceneArrays = {
|
||||
meta: {
|
||||
docs: {
|
||||
description:
|
||||
'Disallow structuredClone on fills/strokes/effects — use typed copy helpers from copy.ts',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Property names that should use typed copy helpers',
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const props = new Set(context.options[0] ?? [
|
||||
'fills', 'strokes', 'effects', 'styleRuns', 'fillGeometry', 'strokeGeometry',
|
||||
])
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (node.callee?.type !== 'Identifier' || node.callee.name !== 'structuredClone') return
|
||||
if (node.arguments?.length !== 1) return
|
||||
const arg = node.arguments[0]
|
||||
if (arg.type === 'MemberExpression' && arg.property?.type === 'Identifier') {
|
||||
if (props.has(arg.property.name)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Use the typed copy helper instead of structuredClone for '${arg.property.name}'. Import from '@open-pencil/core'.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const noMathRandom = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow Math.random() — use crypto.getRandomValues() instead',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee?.type === 'MemberExpression' &&
|
||||
node.callee.object?.type === 'Identifier' &&
|
||||
node.callee.object.name === 'Math' &&
|
||||
node.callee.property?.type === 'Identifier' &&
|
||||
node.callee.property.name === 'random'
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Use crypto.getRandomValues() instead of Math.random().',
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
meta: { name: 'open-pencil' },
|
||||
rules: {
|
||||
'no-inline-named-types': noInlineNamedTypes,
|
||||
'no-structuredclone-scene-arrays': noStructuredCloneSceneArrays,
|
||||
'no-math-random': noMathRandom,
|
||||
},
|
||||
}
|
||||
|
||||
export default plugin
|
||||
13
oxlint.json
13
oxlint.json
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "import", "unicorn", "vue"],
|
||||
"jsPlugins": ["./lint/plugin.js"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2024": true
|
||||
|
|
@ -55,7 +56,17 @@
|
|||
"vue/no-multiple-slot-args": "error",
|
||||
"vue/define-emits-declaration": "error",
|
||||
"vue/define-props-destructuring": "error",
|
||||
"vue/require-typed-ref": "error"
|
||||
"vue/require-typed-ref": "error",
|
||||
|
||||
"open-pencil/no-inline-named-types": ["error", {
|
||||
"Vector": "x:number,y:number",
|
||||
"Color": "a:number,b:number,g:number,r:number",
|
||||
"GUID": "localID:number,sessionID:number",
|
||||
"Rect": "height:number,width:number,x:number,y:number",
|
||||
"Matrix": "m00:number,m01:number,m02:number,m10:number,m11:number,m12:number"
|
||||
}],
|
||||
"open-pencil/no-structuredclone-scene-arrays": "error",
|
||||
"open-pencil/no-math-random": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { NodeChange } from './kiwi/codec'
|
|||
import type { SkiaRenderer } from './renderer'
|
||||
import type { SceneGraph, VariableValue } from './scene-graph'
|
||||
import type { CanvasKit } from 'canvaskit-wasm'
|
||||
import type { GUID } from './types'
|
||||
|
||||
const THUMBNAIL_1X1 = Uint8Array.from(
|
||||
atob(
|
||||
|
|
@ -81,9 +82,9 @@ export async function exportFigFile(
|
|||
|
||||
const blobs: Uint8Array[] = []
|
||||
const pages = graph.getPages(true)
|
||||
const nodeIdToGuid = new Map<string, { sessionID: number; localID: number }>()
|
||||
const nodeIdToGuid = new Map<string, GUID>()
|
||||
const fontDigestMap = await buildFontDigestMap(graph)
|
||||
let internalCanvasGuid: { sessionID: number; localID: number } | null = null
|
||||
let internalCanvasGuid: GUID | null = null
|
||||
|
||||
for (let p = 0; p < pages.length; p++) {
|
||||
const page = pages[p]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type {
|
|||
VariableValue
|
||||
} from './scene-graph'
|
||||
import type { Rect, Vector } from './types'
|
||||
import { copyFills, copyStrokes, copyEffects } from './copy'
|
||||
|
||||
const MIXED = Symbol('mixed')
|
||||
|
||||
|
|
@ -159,7 +160,7 @@ class FigmaNodeProxy {
|
|||
// --- Visual ---
|
||||
|
||||
get fills(): readonly Fill[] {
|
||||
return Object.freeze(structuredClone(this._raw().fills))
|
||||
return Object.freeze(copyFills(this._raw().fills))
|
||||
}
|
||||
|
||||
set fills(v: readonly Fill[]) {
|
||||
|
|
@ -167,7 +168,7 @@ class FigmaNodeProxy {
|
|||
}
|
||||
|
||||
get strokes(): readonly Stroke[] {
|
||||
return Object.freeze(structuredClone(this._raw().strokes))
|
||||
return Object.freeze(copyStrokes(this._raw().strokes))
|
||||
}
|
||||
|
||||
set strokes(v: readonly Stroke[]) {
|
||||
|
|
@ -175,7 +176,7 @@ class FigmaNodeProxy {
|
|||
}
|
||||
|
||||
get effects(): readonly Effect[] {
|
||||
return Object.freeze(structuredClone(this._raw().effects))
|
||||
return Object.freeze(copyEffects(this._raw().effects))
|
||||
}
|
||||
|
||||
set effects(v: readonly Effect[]) {
|
||||
|
|
@ -304,7 +305,7 @@ class FigmaNodeProxy {
|
|||
set strokeWeight(v: number) {
|
||||
const n = this._raw()
|
||||
if (n.strokes.length > 0) {
|
||||
const strokes = structuredClone(n.strokes)
|
||||
const strokes = copyStrokes(n.strokes)
|
||||
strokes[0].weight = v
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokes })
|
||||
}
|
||||
|
|
@ -318,7 +319,7 @@ class FigmaNodeProxy {
|
|||
set strokeAlign(v: string) {
|
||||
const n = this._raw()
|
||||
if (n.strokes.length > 0) {
|
||||
const strokes = structuredClone(n.strokes)
|
||||
const strokes = copyStrokes(n.strokes)
|
||||
strokes[0].align = v as Stroke['align']
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokes })
|
||||
}
|
||||
|
|
@ -1119,9 +1120,9 @@ export class FigmaAPI {
|
|||
height: raw.height,
|
||||
x: raw.x,
|
||||
y: raw.y,
|
||||
fills: structuredClone(raw.fills),
|
||||
strokes: structuredClone(raw.strokes),
|
||||
effects: structuredClone(raw.effects),
|
||||
fills: copyFills(raw.fills),
|
||||
strokes: copyStrokes(raw.strokes),
|
||||
effects: copyEffects(raw.effects),
|
||||
cornerRadius: raw.cornerRadius,
|
||||
topLeftRadius: raw.topLeftRadius,
|
||||
topRightRadius: raw.topRightRadius,
|
||||
|
|
@ -1239,7 +1240,7 @@ export class FigmaAPI {
|
|||
y: first.y,
|
||||
width: first.width,
|
||||
height: first.height,
|
||||
fills: structuredClone(first.fills)
|
||||
fills: copyFills(first.fills)
|
||||
})
|
||||
for (const id of nodeIds) {
|
||||
this.graph.deleteNode(id)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { stringToGuid, VARIABLE_BINDING_FIELDS } from './kiwi/kiwi-convert'
|
|||
|
||||
import type { NodeChange, Paint, VariableConsumptionEntry } from './kiwi/codec'
|
||||
import type { SceneGraph, SceneNode, CharacterStyleOverride } from './scene-graph'
|
||||
import type { GUID } from './types'
|
||||
|
||||
const fontDigestCache = new Map<string, Uint8Array>()
|
||||
|
||||
|
|
@ -248,12 +249,12 @@ function exportTextData(node: SceneNode): NodeChange['textData'] {
|
|||
|
||||
export function sceneNodeToKiwi(
|
||||
node: SceneNode,
|
||||
parentGuid: { sessionID: number; localID: number },
|
||||
parentGuid: GUID,
|
||||
childIndex: number,
|
||||
localIdCounter: { value: number },
|
||||
graph: SceneGraph,
|
||||
blobs: Uint8Array[],
|
||||
nodeIdToGuid?: Map<string, { sessionID: number; localID: number }>,
|
||||
nodeIdToGuid?: Map<string, GUID>,
|
||||
fontDigestMap?: Map<string, Uint8Array>
|
||||
): KiwiNodeChange[] {
|
||||
const localID = localIdCounter.value++
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ export interface Paint {
|
|||
visible?: boolean
|
||||
blendMode?: string
|
||||
stops?: { color: Color; position: number }[]
|
||||
transform?: { m00: number; m01: number; m02: number; m10: number; m11: number; m12: number }
|
||||
transform?: Matrix
|
||||
image?: { hash: string }
|
||||
imageScaleMode?: string
|
||||
colorVariableBinding?: VariableBinding
|
||||
|
|
@ -296,7 +296,7 @@ export interface NodeChange {
|
|||
styleOverrideTable?: NodeChange[]
|
||||
}
|
||||
derivedTextData?: {
|
||||
layoutSize?: { x: number; y: number }
|
||||
layoutSize?: Vector
|
||||
fontMetaData?: Array<{
|
||||
key: { family: string; style: string; postscript?: string }
|
||||
fontLineHeight: number
|
||||
|
|
@ -504,7 +504,7 @@ export function encodePaintWithVariableBinding(
|
|||
* Parse a variable ID string (e.g., "VariableID:38448:122296")
|
||||
* Returns sessionID and localID
|
||||
*/
|
||||
export function parseVariableId(variableId: string): { sessionID: number; localID: number } | null {
|
||||
export function parseVariableId(variableId: string): GUID | null {
|
||||
const match = variableId.match(/VariableID:(\d+):(\d+)/)
|
||||
if (!match) return null
|
||||
return {
|
||||
|
|
@ -567,7 +567,7 @@ export function encodeNodeChangeWithVariables(nodeChange: NodeChange): Uint8Arra
|
|||
function injectVariableBinding(
|
||||
hex: string,
|
||||
marker: string,
|
||||
binding: { variableID: { sessionID: number; localID: number } }
|
||||
binding: { variableID: GUID }
|
||||
): string {
|
||||
const markerIdx = hex.indexOf(marker)
|
||||
if (markerIdx === -1) return hex
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { SceneGraph, SceneNode, GeometryPath } from '../scene-graph'
|
|||
import { guidToString, convertOverrideToProps, resolveGeometryPaths } from './kiwi-convert'
|
||||
import { copyFills, copyStrokes, copyEffects, copyStyleRuns, copyGeometryPaths } from '../copy'
|
||||
import type { GUID } from './codec'
|
||||
import type { Matrix, Vector } from '../types'
|
||||
|
||||
interface SymbolOverride {
|
||||
guidPath?: { guids?: GUID[] }
|
||||
|
|
@ -27,8 +28,8 @@ interface ComponentPropAssignment {
|
|||
|
||||
interface DerivedSymbolOverride {
|
||||
guidPath?: { guids?: GUID[] }
|
||||
size?: { x: number; y: number }
|
||||
transform?: { m00: number; m01: number; m02: number; m10: number; m11: number; m12: number }
|
||||
size?: Vector
|
||||
transform?: Matrix
|
||||
fillGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>
|
||||
strokeGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { normalizeColor } from '../color'
|
|||
import { DEFAULT_FONT_FAMILY, DEFAULT_STROKE_MITER_LIMIT } from '../constants'
|
||||
import { styleToWeight } from '../fonts'
|
||||
import { decodeVectorNetworkBlob } from '../vector'
|
||||
import type { Matrix, Vector } from '../types'
|
||||
|
||||
import type {
|
||||
SceneNode,
|
||||
|
|
@ -79,14 +80,7 @@ function imageHashToString(hash: Record<string, number>): string {
|
|||
return bytes.map((b) => b.toString(16).padStart(2, '0')).join('')
|
||||
}
|
||||
|
||||
function convertGradientTransform(t?: {
|
||||
m00: number
|
||||
m01: number
|
||||
m02: number
|
||||
m10: number
|
||||
m11: number
|
||||
m12: number
|
||||
}): GradientTransform | undefined {
|
||||
function convertGradientTransform(t?: Matrix): GradientTransform | undefined {
|
||||
if (!t) return undefined
|
||||
return { m00: t.m00, m01: t.m01, m02: t.m02, m10: t.m10, m11: t.m11, m12: t.m12 }
|
||||
}
|
||||
|
|
@ -386,7 +380,7 @@ function resolveVectorNetwork(
|
|||
const vectorData = nc.vectorData as
|
||||
| {
|
||||
vectorNetworkBlob?: number
|
||||
normalizedSize?: { x: number; y: number }
|
||||
normalizedSize?: Vector
|
||||
styleOverrideTable?: Array<{ styleID: number; handleMirroring?: string }>
|
||||
}
|
||||
| undefined
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
import type { SceneNode, SceneGraph } from '../scene-graph'
|
||||
import type { SnapGuide } from '../snap'
|
||||
import type { TextEditor } from '../text-editor'
|
||||
import type { Rect } from '../types'
|
||||
import type { Rect, Vector } from '../types'
|
||||
import type { Canvas } from 'canvaskit-wasm'
|
||||
import type { SkiaRenderer } from './renderer'
|
||||
import type { RenderOverlays } from './renderer'
|
||||
|
|
@ -358,8 +358,8 @@ export function drawGroupBounds(
|
|||
export function getRotatedCorners(
|
||||
r: SkiaRenderer,
|
||||
n: SceneNode,
|
||||
abs: { x: number; y: number }
|
||||
): Array<{ x: number; y: number }> {
|
||||
abs: Vector
|
||||
): Vector[] {
|
||||
const cx = (abs.x + n.width / 2) * r.zoom + r.panX
|
||||
const cy = (abs.y + n.height / 2) * r.zoom + r.panY
|
||||
const hw = (n.width / 2) * r.zoom
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ import { RenderProfiler } from '../profiler'
|
|||
import type { SceneNode, SceneGraph, Fill, Stroke } from '../scene-graph'
|
||||
import type { SnapGuide } from '../snap'
|
||||
import type { TextEditor } from '../text-editor'
|
||||
import type { Color } from '../types'
|
||||
import type { Rect } from '../types'
|
||||
import type { Color, Rect, Vector } from '../types'
|
||||
import type { Image as CKImage, Path } from 'canvaskit-wasm'
|
||||
import type {
|
||||
CanvasKit,
|
||||
|
|
@ -128,14 +127,14 @@ export interface RenderOverlays {
|
|||
direction: 'HORIZONTAL' | 'VERTICAL'
|
||||
} | null
|
||||
penState?: {
|
||||
vertices: Array<{ x: number; y: number }>
|
||||
vertices: Vector[]
|
||||
segments: Array<{
|
||||
start: number
|
||||
end: number
|
||||
tangentStart: { x: number; y: number }
|
||||
tangentEnd: { x: number; y: number }
|
||||
tangentStart: Vector
|
||||
tangentEnd: Vector
|
||||
}>
|
||||
dragTangent: { x: number; y: number } | null
|
||||
dragTangent: Vector | null
|
||||
closingToFirst: boolean
|
||||
cursorX?: number
|
||||
cursorY?: number
|
||||
|
|
@ -884,7 +883,7 @@ export class SkiaRenderer {
|
|||
return stroke.color
|
||||
}
|
||||
|
||||
screenToCanvas(sx: number, sy: number): { x: number; y: number } {
|
||||
screenToCanvas(sx: number, sy: number): Vector {
|
||||
return {
|
||||
x: (sx - this.panX) / this.zoom,
|
||||
y: (sy - this.panY) / this.zoom
|
||||
|
|
@ -970,7 +969,7 @@ export class SkiaRenderer {
|
|||
drawGroupBoundsFn(this, canvas, nodes, graph)
|
||||
}
|
||||
|
||||
getRotatedCorners(n: SceneNode, abs: { x: number; y: number }): Array<{ x: number; y: number }> {
|
||||
getRotatedCorners(n: SceneNode, abs: Vector): Vector[] {
|
||||
return getRotatedCornersFn(this, n, abs)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { BLACK, DEFAULT_FONT_FAMILY, DEFAULT_STROKE_MITER_LIMIT } from './consta
|
|||
import { copyEffects, copyFills, copyStrokes, copyStyleRuns } from './copy'
|
||||
|
||||
export type { GUID, Color } from './types'
|
||||
import type { Matrix, Vector } from './types'
|
||||
|
||||
export type HandleMirroring = 'NONE' | 'ANGLE' | 'ANGLE_AND_LENGTH'
|
||||
export type WindingRule = 'NONZERO' | 'EVENODD'
|
||||
|
|
@ -18,8 +19,8 @@ export interface VectorVertex {
|
|||
export interface VectorSegment {
|
||||
start: number
|
||||
end: number
|
||||
tangentStart: { x: number; y: number }
|
||||
tangentEnd: { x: number; y: number }
|
||||
tangentStart: Vector
|
||||
tangentEnd: Vector
|
||||
}
|
||||
|
||||
export interface VectorRegion {
|
||||
|
|
@ -124,7 +125,7 @@ export interface Stroke {
|
|||
export interface Effect {
|
||||
type: 'DROP_SHADOW' | 'INNER_SHADOW' | 'LAYER_BLUR' | 'BACKGROUND_BLUR' | 'FOREGROUND_BLUR'
|
||||
color: Color
|
||||
offset: { x: number; y: number }
|
||||
offset: Vector
|
||||
radius: number
|
||||
spread: number
|
||||
visible: boolean
|
||||
|
|
@ -452,7 +453,7 @@ export class SceneGraph {
|
|||
variableCollections = new Map<string, VariableCollection>()
|
||||
activeMode = new Map<string, string>()
|
||||
rootId: string
|
||||
private absPosCache = new Map<string, { x: number; y: number }>()
|
||||
private absPosCache = new Map<string, Vector>()
|
||||
|
||||
constructor() {
|
||||
const root = createDefaultNode('FRAME', {
|
||||
|
|
@ -659,7 +660,7 @@ export class SceneGraph {
|
|||
this.absPosCache.clear()
|
||||
}
|
||||
|
||||
getAbsolutePosition(id: string): { x: number; y: number } {
|
||||
getAbsolutePosition(id: string): Vector {
|
||||
const cached = this.absPosCache.get(id)
|
||||
if (cached) return cached
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
WindingRule
|
||||
} from './scene-graph'
|
||||
import type { CanvasKit, Path } from 'canvaskit-wasm'
|
||||
import type { Rect } from './types'
|
||||
|
||||
// --- vectorNetworkBlob binary format ---
|
||||
// Header: [numVertices:u32, numSegments:u32, numRegions:u32] (12 bytes)
|
||||
|
|
@ -320,12 +321,7 @@ function buildChains(segments: VectorSegment[], _vertexCount: number): number[][
|
|||
return chains
|
||||
}
|
||||
|
||||
export function computeVectorBounds(network: VectorNetwork): {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
} {
|
||||
export function computeVectorBounds(network: VectorNetwork): Rect {
|
||||
if (network.vertices.length === 0) {
|
||||
return { x: 0, y: 0, width: 0, height: 0 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { computeSelectionBounds, computeSnap } from '@open-pencil/core'
|
||||
|
||||
import type { EditorStore, Tool } from '@/stores/editor'
|
||||
import type { NodeType, Rect, SceneNode } from '@open-pencil/core'
|
||||
import type { NodeType, Rect, SceneNode, Vector } from '@open-pencil/core'
|
||||
|
||||
type HandlePosition = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ interface DragMove {
|
|||
type: 'move'
|
||||
startX: number
|
||||
startY: number
|
||||
originals: Map<string, { x: number; y: number }>
|
||||
originals: Map<string, Vector>
|
||||
duplicated?: boolean
|
||||
autoLayoutParentId?: string
|
||||
brokeFromAutoLayout?: boolean
|
||||
|
|
@ -150,7 +150,7 @@ function getHandlePositions(
|
|||
s: { x: mx, y: y2 },
|
||||
sw: { x: x1, y: y2 },
|
||||
w: { x: x1, y: my }
|
||||
} satisfies Record<HandlePosition, { x: number; y: number }>
|
||||
} satisfies Record<HandlePosition, Vector>
|
||||
}
|
||||
|
||||
function unrotate(
|
||||
|
|
@ -389,7 +389,7 @@ export function useCanvasInput(
|
|||
store.select([hit.id], true)
|
||||
}
|
||||
|
||||
const originals = new Map<string, { x: number; y: number }>()
|
||||
const originals = new Map<string, Vector>()
|
||||
for (const id of store.state.selectedIds) {
|
||||
const n = store.graph.getNode(id)
|
||||
if (n) originals.set(id, { x: n.x, y: n.y })
|
||||
|
|
@ -398,7 +398,7 @@ export function useCanvasInput(
|
|||
// Alt+drag → duplicate
|
||||
if (e.altKey && store.state.selectedIds.size > 0) {
|
||||
const newIds: string[] = []
|
||||
const newOriginals = new Map<string, { x: number; y: number }>()
|
||||
const newOriginals = new Map<string, Vector>()
|
||||
for (const id of store.state.selectedIds) {
|
||||
const src = store.graph.getNode(id)
|
||||
if (!src) continue
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import type {
|
|||
VectorNetwork,
|
||||
VectorRegion,
|
||||
VectorSegment,
|
||||
Vector,
|
||||
VectorVertex
|
||||
} from '@open-pencil/core'
|
||||
import type { CanvasKit } from 'canvaskit-wasm'
|
||||
|
|
@ -175,7 +176,7 @@ export function createEditorStore() {
|
|||
penState: null as {
|
||||
vertices: VectorVertex[]
|
||||
segments: VectorSegment[]
|
||||
dragTangent: { x: number; y: number } | null
|
||||
dragTangent: Vector | null
|
||||
closingToFirst: boolean
|
||||
} | null,
|
||||
penCursorX: null as number | null,
|
||||
|
|
@ -629,10 +630,7 @@ export function createEditorStore() {
|
|||
}
|
||||
}
|
||||
|
||||
function setCanvasKit(
|
||||
ck: CanvasKit,
|
||||
renderer: SkiaRenderer
|
||||
) {
|
||||
function setCanvasKit(ck: CanvasKit, renderer: SkiaRenderer) {
|
||||
_ck = ck
|
||||
_renderer = renderer
|
||||
_textEditor = new TextEditor(ck)
|
||||
|
|
@ -1958,8 +1956,8 @@ export function createEditorStore() {
|
|||
}
|
||||
}
|
||||
|
||||
function commitMove(originals: Map<string, { x: number; y: number }>) {
|
||||
const finals = new Map<string, { x: number; y: number }>()
|
||||
function commitMove(originals: Map<string, Vector>) {
|
||||
const finals = new Map<string, Vector>()
|
||||
for (const [id] of originals) {
|
||||
const n = graph.getNode(id)
|
||||
if (n) finals.set(id, { x: n.x, y: n.y })
|
||||
|
|
|
|||
Loading…
Reference in a new issue