import type { Vec2 } from '../math'; import { createCardShapes } from '../cards'; import { EditorShapeRecord, type EditorShapeRecord as Shape } from '../editor-model'; import { registry } from './registry'; import type { Stencil, StencilCategory } from './types'; const PLACEHOLDER_PAGE_ID = 'placeholder_page'; type StencilDefinition = Omit & { spawn: (at: Vec2) => Shape[] }; function rect(at: Vec2, width: number, height: number, radius = 0, fill = '#ffffff', stroke = '#1f2937') { return EditorShapeRecord.createRect(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: width, h: height, fill, stroke, radius }); } function ellipse(at: Vec2, width: number, height: number, fill = '#ffffff', stroke = '#1f2937') { return EditorShapeRecord.createEllipse(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: width, h: height, fill, stroke }); } function line(at: Vec2, x: number, y: number, width: number, height: number, stroke = '#1f2937') { return EditorShapeRecord.createLine(PLACEHOLDER_PAGE_ID, at.x + x, at.y + y, { a: { x: 0, y: 0 }, b: { x: width, y: height }, stroke, width: 2 }); } function text(at: Vec2, x: number, y: number, value: string, width: number, fontSize = 16) { return EditorShapeRecord.createText(PLACEHOLDER_PAGE_ID, at.x + x, at.y + y, { text: value, fontSize, fontFamily: 'sans-serif', color: '#111827', w: width }); } function reference(at: Vec2, referenceType: 'url' | 'file' | 'page', value: string, label: string) { return EditorShapeRecord.createReference(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: 280, h: 72, referenceType, value, label }); } function stencil( id: string, name: string, category: StencilCategory, tags: string[], preview: string, spawn: (at: Vec2) => Shape[] ): StencilDefinition { return { id, name, category, tags, preview: { kind: 'svg', data: preview }, spawn }; } const BUILTIN_STENCILS: readonly StencilDefinition[] = [ stencil( 'flowchart:process', 'Process', 'Flowchart', ['rect', 'box', 'action'], '', (at) => [rect(at, 120, 80)] ), stencil( 'flowchart:decision', 'Decision', 'Flowchart', ['diamond', 'if', 'branch'], '', (at) => { const shape = rect(at, 80, 80); shape.rot = Math.PI / 4; return [shape]; } ), stencil( 'flowchart:terminator', 'Terminator', 'Flowchart', ['start', 'end', 'pill'], '', (at) => [rect(at, 120, 60, 30)] ), stencil( 'flowchart:input-output', 'Input / Output', 'Flowchart', ['data', 'parallelogram', 'input', 'output'], '', (at) => [ line(at, 16, 0, 104, 0), line(at, 120, 0, -16, 72), line(at, 104, 72, -104, 0), line(at, 0, 72, 16, -72) ] ), stencil( 'flowchart:document', 'Document', 'Flowchart', ['paper', 'report', 'file'], '', (at) => [ rect(at, 120, 76), line(at, 0, 62, 30, 8), line(at, 30, 70, 30, -8), line(at, 60, 62, 30, 8), line(at, 90, 70, 30, -8) ] ), stencil( 'flowchart:database', 'Database', 'Flowchart', ['storage', 'cylinder', 'data'], '', (at) => [ rect({ x: at.x, y: at.y + 10 }, 100, 90, 0), ellipse(at, 100, 20), ellipse({ x: at.x, y: at.y + 80 }, 100, 20) ] ), stencil( 'flowchart:connector', 'Connector', 'Flowchart', ['link', 'continuation', 'circle'], '', (at) => [ellipse(at, 56, 56)] ), stencil( 'content:url-reference', 'URL Reference', 'Content', ['url', 'link', 'reference'], 'URL reference', (at) => [reference(at, 'url', 'https://example.com', 'Open URL')] ), stencil( 'content:file-reference', 'File Reference', 'Content', ['file', 'path', 'reference'], 'File reference', (at) => [reference(at, 'file', 'path/to/file', 'Open file')] ), stencil( 'ui:card', 'Card', 'UI', ['container', 'panel'], '', (at) => createCardShapes(PLACEHOLDER_PAGE_ID, at.x, at.y, { title: 'Card title', body: 'Add context, notes, or a link.' }) ), stencil( 'ui:button', 'Button', 'UI', ['control', 'cta', 'action'], 'Button', (at) => [rect(at, 140, 44, 6, '#2563eb', '#1d4ed8'), text(at, 38, 12, 'Button', 80, 16)] ), stencil( 'ui:text-input', 'Text Input', 'UI', ['field', 'form', 'textbox'], 'Placeholder', (at) => [rect(at, 240, 44, 4), text(at, 12, 12, 'Placeholder', 210, 15)] ), stencil( 'ui:browser-window', 'Browser Window', 'UI', ['web', 'frame', 'chrome'], '', (at) => [ rect(at, 360, 240, 6), line(at, 0, 36, 360, 0), ellipse({ x: at.x + 12, y: at.y + 13 }, 10, 10, '#ef4444'), ellipse({ x: at.x + 30, y: at.y + 13 }, 10, 10, '#f59e0b') ] ), stencil( 'ui:mobile-frame', 'Mobile Frame', 'UI', ['phone', 'screen', 'device'], '', (at) => [rect(at, 180, 360, 18), line(at, 65, 18, 50, 0)] ), stencil( 'ui:modal', 'Modal Dialog', 'UI', ['dialog', 'overlay', 'popup'], '', (at) => [rect(at, 320, 200, 8), line(at, 0, 48, 320, 0), text(at, 18, 15, 'Dialog title', 220, 18)] ), stencil( 'diagrams:service', 'Service', 'Diagrams', ['developer', 'architecture', 'microservice'], 'Service', (at) => [rect(at, 180, 100, 8, '#eff6ff', '#2563eb'), text(at, 48, 38, 'Service', 100, 17)] ), stencil( 'diagrams:database', 'Data Store', 'Diagrams', ['developer', 'database', 'storage'], '', (at) => [ rect({ x: at.x, y: at.y + 10 }, 110, 90, 0, '#f0fdf4', '#16a34a'), ellipse(at, 110, 20, '#f0fdf4', '#16a34a'), ellipse({ x: at.x, y: at.y + 80 }, 110, 20, '#f0fdf4', '#16a34a') ] ), stencil( 'diagrams:queue', 'Message Queue', 'Diagrams', ['developer', 'broker', 'events'], '', (at) => [ rect(at, 180, 100, 6, '#fff7ed', '#ea580c'), line(at, 18, 28, 144, 0, '#ea580c'), line(at, 18, 50, 144, 0, '#ea580c'), line(at, 18, 72, 144, 0, '#ea580c') ] ), stencil( 'diagrams:api-endpoint', 'API Endpoint', 'Diagrams', ['developer', 'http', 'route', 'api'], 'GET /resource', (at) => [rect(at, 240, 52, 6, '#f8fafc', '#475569'), text(at, 14, 16, 'GET /resource', 210, 15)] ), stencil( 'diagrams:component', 'Component', 'Diagrams', ['developer', 'module', 'package'], '', (at) => [ rect({ x: at.x + 18, y: at.y }, 162, 110, 4), rect({ x: at.x, y: at.y + 22 }, 38, 22, 2), rect({ x: at.x, y: at.y + 66 }, 38, 22, 2), text(at, 56, 44, 'Component', 105, 16) ] ), stencil( 'etc:stickynote', 'Sticky Note', 'Etc', ['note', 'memo', 'yellow'], '', (at) => [rect(at, 200, 200, 0, '#fff740', 'transparent')] ) ]; /** Stable IDs for the curated stencil library, grouped by category in declaration order. */ export const BUILTIN_STENCIL_IDS = BUILTIN_STENCILS.map(({ id }) => id) as readonly string[]; /** Registers the curated built-in stencil library once per registry instance. */ export function registerBuiltinStencils(): void { for (const definition of BUILTIN_STENCILS) { if (!registry.get(definition.id)) registry.register(definition); } }