openpencil/tests/engine/svg-path-parse.test.ts
Anton A S 733321fbb5 Add step budget warnings and Continue button for AI chat
- Track per-run step count, warn AI when 5 steps remain via _warning
  field injected into tool results
- Show 'Continue' button in chat when step limit (50) is exhausted
- Add step budget section to system prompt so AI plans ahead
- Reset run step counter on each new agent call via prepareCall
2026-03-13 10:18:20 +03:00

109 lines
3.7 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { parseSVGPath, vectorNetworkToSVGPaths } from '@open-pencil/core'
describe('parseSVGPath', () => {
test('simple rectangle M L L L Z', () => {
const vn = parseSVGPath('M0 0 L10 0 L10 10 L0 10 Z')
expect(vn.vertices.length).toBe(4)
expect(vn.segments.length).toBe(4)
expect(vn.regions.length).toBe(1)
expect(vn.regions[0].windingRule).toBe('NONZERO')
expect(vn.regions[0].loops.length).toBe(1)
expect(vn.regions[0].loops[0].length).toBe(4)
})
test('H and V commands', () => {
const vn = parseSVGPath('M0 0 H10 V10 H0 Z')
expect(vn.vertices.length).toBe(4)
expect(vn.segments.length).toBe(4)
})
test('relative commands (m l h v z)', () => {
const vn = parseSVGPath('m0 0 l10 0 l0 10 l-10 0 z')
expect(vn.vertices.length).toBe(4)
expect(vn.segments.length).toBe(4)
expect(vn.regions[0].loops[0].length).toBe(4)
})
test('cubic bezier C command', () => {
const vn = parseSVGPath('M0 0 C10 0 10 10 0 10')
expect(vn.vertices.length).toBe(2)
expect(vn.segments.length).toBe(1)
const seg = vn.segments[0]
expect(seg.tangentStart.x).toBeCloseTo(10)
expect(seg.tangentStart.y).toBeCloseTo(0)
expect(seg.tangentEnd.x).toBeCloseTo(10)
expect(seg.tangentEnd.y).toBeCloseTo(0)
})
test('smooth cubic S command → expanded to C', () => {
const vn = parseSVGPath('M0 0 C5 0 10 5 10 10 S20 20 20 10')
expect(vn.segments.length).toBe(2)
})
test('quadratic Q command → converted to cubic', () => {
const vn = parseSVGPath('M0 0 Q10 10 20 0')
expect(vn.segments.length).toBe(1)
const seg = vn.segments[0]
expect(seg.tangentStart.x).not.toBe(0)
expect(seg.tangentEnd.x).not.toBe(0)
})
test('arc A command → converted to cubics', () => {
const vn = parseSVGPath('M10 0 A10 10 0 0 1 0 10')
expect(vn.segments.length).toBeGreaterThanOrEqual(1)
expect(vn.vertices.length).toBeGreaterThanOrEqual(2)
})
test('evenodd winding rule', () => {
const vn = parseSVGPath('M0 0 L10 0 L10 10 Z', 'EVENODD')
expect(vn.regions[0].windingRule).toBe('EVENODD')
})
test('multiple subpaths → multiple loops in one region', () => {
const vn = parseSVGPath('M0 0 L10 0 L10 10 Z M20 20 L30 20 L30 30 Z')
expect(vn.regions.length).toBe(1)
expect(vn.regions[0].loops.length).toBe(2)
})
test('open subpath → no region', () => {
const vn = parseSVGPath('M0 0 L10 0 L10 10')
expect(vn.regions.length).toBe(0)
expect(vn.segments.length).toBe(2)
})
test('straight segments have zero tangents', () => {
const vn = parseSVGPath('M0 0 L10 10')
const seg = vn.segments[0]
expect(seg.tangentStart.x).toBe(0)
expect(seg.tangentStart.y).toBe(0)
expect(seg.tangentEnd.x).toBe(0)
expect(seg.tangentEnd.y).toBe(0)
})
test('real icon: mdi:home path', () => {
const d = 'M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8z'
const vn = parseSVGPath(d)
expect(vn.vertices.length).toBeGreaterThan(3)
expect(vn.segments.length).toBeGreaterThan(3)
expect(vn.regions.length).toBe(1)
})
test('real icon: lucide heart path (arcs + curves)', () => {
const d = 'M2 9.5a5.5 5.5 0 0 1 9.591-3.676a.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5'
const vn = parseSVGPath(d)
expect(vn.vertices.length).toBeGreaterThan(5)
expect(vn.segments.length).toBeGreaterThan(5)
})
test('round-trip: parse → export produces valid SVG paths', () => {
const d = 'M0 0 L100 0 L100 100 L0 100 Z'
const vn = parseSVGPath(d)
const exported = vectorNetworkToSVGPaths(vn)
expect(exported.length).toBe(1)
expect(exported[0]).toContain('M')
expect(exported[0]).toContain('Z')
})
})