fix(vector): validate VectorNetwork input in create_vector
create_vector now validates the path JSON before accepting it: checks vertices have numeric coordinates and segment indices are in range. Returns a clear error message instead of silently creating a malformed node that crashes on save. The scene graph also normalizes vectorNetwork on updateNode as a safety net for other code paths.
This commit is contained in:
parent
3727fa19d0
commit
ab1ff69f99
|
|
@ -27,6 +27,7 @@ export {
|
|||
generateId,
|
||||
cloneVectorNetwork,
|
||||
normalizeVectorNetwork,
|
||||
validateVectorNetwork,
|
||||
type SceneNode,
|
||||
type NodeType,
|
||||
type Fill,
|
||||
|
|
|
|||
|
|
@ -78,6 +78,42 @@ export function cloneVectorNetwork(vn: VectorNetwork): VectorNetwork {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a VectorNetwork structure, returning an array of error messages.
|
||||
* Empty array means the network is valid.
|
||||
*/
|
||||
export function validateVectorNetwork(vn: VectorNetwork): string[] {
|
||||
const errors: string[] = []
|
||||
if (!Array.isArray(vn.vertices)) {
|
||||
errors.push('vertices must be an array')
|
||||
return errors
|
||||
}
|
||||
if (!Array.isArray(vn.segments)) {
|
||||
errors.push('segments must be an array')
|
||||
return errors
|
||||
}
|
||||
if (!Array.isArray(vn.regions)) errors.push('regions must be an array')
|
||||
const vertexCount = vn.vertices.length
|
||||
for (let i = 0; i < vn.vertices.length; i++) {
|
||||
const v = vn.vertices[i]
|
||||
if (typeof v.x !== 'number' || typeof v.y !== 'number') {
|
||||
errors.push(`vertex[${i}]: x and y must be numbers`)
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < vn.segments.length; i++) {
|
||||
const s = vn.segments[i]
|
||||
if (typeof s.start !== 'number' || typeof s.end !== 'number') {
|
||||
errors.push(`segment[${i}]: start and end must be numbers`)
|
||||
} else {
|
||||
if (s.start < 0 || s.start >= vertexCount)
|
||||
errors.push(`segment[${i}]: start index ${s.start} out of range`)
|
||||
if (s.end < 0 || s.end >= vertexCount)
|
||||
errors.push(`segment[${i}]: end index ${s.end} out of range`)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure every segment has tangentStart/tangentEnd.
|
||||
* Missing tangents default to {x:0, y:0} (straight line segments).
|
||||
|
|
@ -860,6 +896,9 @@ export class SceneGraph {
|
|||
) {
|
||||
node.textPicture = null
|
||||
}
|
||||
if (changes.vectorNetwork) {
|
||||
changes = { ...changes, vectorNetwork: normalizeVectorNetwork(changes.vectorNetwork) }
|
||||
}
|
||||
Object.assign(node, changes)
|
||||
this.emitter.emit('node:updated', id, changes)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { parseColor } from '../color'
|
||||
import { fetchIcons, searchIconsBatch } from '../icons'
|
||||
import { createIconFromPaths } from '../icons/render'
|
||||
import { normalizeVectorNetwork } from '../scene-graph'
|
||||
import { normalizeVectorNetwork, validateVectorNetwork } from '../scene-graph'
|
||||
import { defineTool, nodeSummary } from './schema'
|
||||
|
||||
import type { FigmaNodeProxy } from '../figma-api'
|
||||
|
|
@ -170,8 +170,15 @@ export const createVector = defineTool({
|
|||
node.y = args.y
|
||||
if (args.name) node.name = args.name
|
||||
if (args.path) {
|
||||
const network = normalizeVectorNetwork(JSON.parse(args.path) as VectorNetwork)
|
||||
figma.graph.updateNode(node.id, { vectorNetwork: network })
|
||||
let parsed: VectorNetwork
|
||||
try {
|
||||
parsed = JSON.parse(args.path) as VectorNetwork
|
||||
} catch {
|
||||
return { error: 'Invalid JSON in path parameter' }
|
||||
}
|
||||
const errors = validateVectorNetwork(parsed)
|
||||
if (errors.length > 0) return { error: `Invalid VectorNetwork: ${errors.join('; ')}` }
|
||||
figma.graph.updateNode(node.id, { vectorNetwork: normalizeVectorNetwork(parsed) })
|
||||
}
|
||||
if (args.fill) {
|
||||
node.fills = [{ type: 'SOLID', color: parseColor(args.fill), opacity: 1, visible: true }]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
decodeVectorNetworkBlob,
|
||||
computeVectorBounds,
|
||||
normalizeVectorNetwork,
|
||||
validateVectorNetwork,
|
||||
type VectorNetwork,
|
||||
} from '@open-pencil/core'
|
||||
|
||||
|
|
@ -191,6 +192,53 @@ describe('normalizeVectorNetwork', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('validateVectorNetwork', () => {
|
||||
test('valid network returns no errors', () => {
|
||||
const network: VectorNetwork = {
|
||||
vertices: [{ x: 0, y: 0 }, { x: 10, y: 10 }],
|
||||
segments: [{ start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }],
|
||||
regions: []
|
||||
}
|
||||
expect(validateVectorNetwork(network)).toEqual([])
|
||||
})
|
||||
|
||||
test('segments without tangents are valid (normalize handles them)', () => {
|
||||
const network = {
|
||||
vertices: [{ x: 0, y: 0 }, { x: 10, y: 10 }],
|
||||
segments: [{ start: 0, end: 1 }],
|
||||
regions: []
|
||||
} as unknown as VectorNetwork
|
||||
expect(validateVectorNetwork(network)).toEqual([])
|
||||
})
|
||||
|
||||
test('rejects segment with out-of-range start index', () => {
|
||||
const network = {
|
||||
vertices: [{ x: 0, y: 0 }],
|
||||
segments: [{ start: 0, end: 5 }],
|
||||
regions: []
|
||||
} as unknown as VectorNetwork
|
||||
const errors = validateVectorNetwork(network)
|
||||
expect(errors.length).toBe(1)
|
||||
expect(errors[0]).toContain('end index 5 out of range')
|
||||
})
|
||||
|
||||
test('rejects missing vertices array', () => {
|
||||
const network = { segments: [], regions: [] } as unknown as VectorNetwork
|
||||
const errors = validateVectorNetwork(network)
|
||||
expect(errors[0]).toContain('vertices must be an array')
|
||||
})
|
||||
|
||||
test('rejects vertex with non-number coordinates', () => {
|
||||
const network = {
|
||||
vertices: [{ x: 'a', y: 0 }],
|
||||
segments: [],
|
||||
regions: []
|
||||
} as unknown as VectorNetwork
|
||||
const errors = validateVectorNetwork(network)
|
||||
expect(errors[0]).toContain('x and y must be numbers')
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeVectorBounds', () => {
|
||||
test('empty network', () => {
|
||||
expect(computeVectorBounds({ vertices: [], segments: [], regions: [] })).toEqual({
|
||||
|
|
|
|||
Loading…
Reference in a new issue