ANCHORSMITH — AI AGENT AUTOMATION SPEC ====================================== This document describes how an automated agent drives the Anchorsmith vector editor programmatically. It is generated from the source in src/lib/agent/. Everything below is plain text so an agent can parse it directly. A machine copy of this file is served at /tutorial/agent.txt (Content-Type: text/plain). COORDINATE CONTRACT ------------------- - Document space is Y-UP: +Y points upward. Screen rendering flips Y; you never deal with screen space through this API. - Rotation angles are RADIANS, counter-clockwise positive. - Image and text (x, y) are the BOTTOM-LEFT origin of the element. - The "scale" command sets an ABSOLUTE target size (width/height), not a multiplier. - Colors are RGBA: r,g,b are 0-255 integers, a is 0-1. ENTRY POINT ----------- executeAgentAction(request) => AgentActionResult (src/lib/agent/action-api.ts) request = { doc: VectorDocument // the live document, mutated in place commit: (label, mutate) => void // host-supplied undo wrapper; it runs your mutator command: AgentCommand // the action to perform (see COMMANDS) actionLog: AgentActionLog? // OPTIONAL. Pass your own persistent log to diff across calls; // if omitted a throwaway log is used and cross-call diffs are lost } AgentActionResult is a discriminated union on "ok": success: { ok: true, actionId, changedIds[], createdIds[], deletedIds[], warnings[], diff } failure: { ok: false, actionId: '', changedIds: [], ..., warnings[], error } error = { code: string, message: string, details?: unknown } actionId on success = "agent-action-" (monotonic). diff on a success result describes ONLY that one action. ERROR CODES ----------- layer_not_found no editable layer matched (bad layerId, or every layer locked) not_found no elements matched the requested ids invalid_node node target invalid (bad indices, or role can't be edited) invalid_raster traceImage got bad dimensions or a too-small rgba buffer no_result trace / outlineStroke / constraint produced no geometry invalid_selection boolean got <2 shapes, or a constraint selection can't form the kind unsolvable_constraint the constraint would over-constrain the sketch commit_failed the host commit callback threw (details carries the message) Non-fatal problems come back in warnings[] (e.g. "Ignored missing ids: ..."). LAYER RESOLUTION (all create commands) -------------------------------------- If a command takes an optional layerId: given -> that layer (or layer_not_found); omitted -> the first UNLOCKED layer, else layers[0]. Omitted ids default to a random UUID. COMMANDS (AgentCommand, discriminant field is "kind") ===================================================== -- Creation / drawing -- createRect { kind, x, y, width, height, id?, name?, layerId? } x,y = bottom-left corner (Y-up). createEllipse { kind, cx, cy, rx, ry, id?, name?, layerId? } centre + radii; built from 4 cubic beziers. createLine { kind, x1, y1, x2, y2, id?, name?, layerId? } open contour. createPath { kind, contours: VectorContour[], id?, name?, layerId? } createText { kind, x, y, text, fontSize?=24, id?, name?, layerId? } x,y = baseline origin. importImageDataUri { kind, href, width, height, x?=0, y?=0, mimeType?='image/png', id?, name?, layerId? } importReferenceUnderlay { kind, href, width, height, x?=0, y?=0, opacity?=0.35, id?(layer), imageId?, name?='Reference Underlay', naturalWidth?, naturalHeight?, mimeType? } Creates a LOCKED low-opacity layer at the BOTTOM of the stack. traceImage { kind, rgba, imgW, imgH, rect:{x,y,width,height}, maskMode:'ink'|'foreground'|'auto'|'color', traceStyle?='fill'|'centerline'|'brush', tolerance?=32, inkThreshold?=60, foregroundThreshold?=32, contiguous?=false, smoothness?=1.0, seeds?:Point[], minComponent?=40, minPathLength?=4, denoiseEpsilon?, fitTolerance?, cornerAngleThreshold?, circleResidualRatio?, circleResidualMax?, handleClampRatio?, id?, name?, layerId? } rgba = 4 bytes/px, row-major, top-left origin. rect = target doc rect (mapping applies the Y-up flip). Validates imgW>0 && imgH>0 && rgba.length >= imgW*imgH*4 else invalid_raster. -- Deletion / transform -- deleteElements { kind, ids: string[] } move { kind, ids, dx, dy } translate by (dx,dy). scale { kind, ids, width, height } ABSOLUTE target size; text sets fontSize=height. rotate { kind, ids, radians } add rotation (CCW). -- Style (shapes only) -- setFill { kind, ids, fill: VectorFillStyle } replaces fill wholesale. setStroke { kind, ids, stroke: Partial } shallow-merged. setGradient { kind, ids, paint: VectorPaint } enables fill + sets paint. setGroupGradient { kind, groupId, paint: linear|radial VectorPaint, color?, layerId? } Sets one document-space shared fill on the explicit group; all member paths reference the same exported gradient and render as one continuous fill. v1 is fill-only; use solid shape fills or clear the group fill to exit. outlineStroke { kind, id, resultId? } stroke -> filled outline; deletes source. -- Boolean / layout (Pro features) -- boolean { kind, op:'union'|'subtract'|'intersect'|'exclude', ids(>=2), resultId? } ids[0] is the subject; operands are deleted, resultId created. align { kind, ids, align:'left'|'centerX'|'right'|'top'|'centerY'|'bottom' } Y-up: top=maxY, bottom=minY. Aligns to the selection bounding box. distribute { kind, ids(>=3), axis:'horizontal'|'vertical', mode?='gaps'|'centers' } -- Node (path) editing (shapes only) -- addNode { kind, shapeId, contourIndex, segmentIndex, t } t in 0..1 along segment. moveNode { kind, shapeId, node: NodeSelectionDetail, x, y, asymmetric? } setNodeSmooth { kind, shapeId, node, smooth: boolean } deleteNode { kind, shapeId, node } anchors only; refuses below min segments (closed:3, open:1). -- CAD constraints (sketch layer) -- addConstraint { kind, constraintKind, selection: SelectionState[], id? } constraintKind = 'coincident'|'horizontal'|'vertical'|'parallel'|'perpendicular'| 'fixed-distance'|'equal-length'|'angle'|'collinear' selection rules: >=2 points -> coincident/horizontal/vertical/fixed-distance; exactly 1 line -> horizontal/vertical; >=2 lines -> parallel/perpendicular/ equal-length/angle/collinear. Solves the whole sketch; rejects invalid_selection or unsolvable_constraint. addDimension { kind, selection: SelectionState[], id? } a shown fixed-distance constraint (annotation). PERCEPTION (read-only observation) (src/lib/agent/perception.ts) ================================== getAgentDocumentSnapshot(doc, options?) => { json, // serializeDocument(doc) svg, // exportSvg(doc, options) summary: { documentId, name, width, height, artboardIds[], layerIds[], shapeIds[], textIds[], imageIds[] } } getAgentBounds(doc, ids[]) => Record getAgentSelection(selection[]) => defensive clone of the selection array getAgentDiff(log, sinceActionId?) => aggregated { createdIds, changedIds, deletedIds } (needs a persistent AgentActionLog across calls) getAgentPointDistance(doc, { a: GeometryRef, b: GeometryRef }) => { dx, dy, distance } | null prepareAgentVisualPng(doc, options?) => { svg, scale, width, height, targetWidth, targetHeight } renderAgentPng(doc, options?) => Promise // Blob in a DOM, Buffer headless Raster (on { rgba, width, height, rect }): sampleImageAt(source, point) => { pixelX, pixelY, color } | null edgePointsNear(source, point, radius, options?) => foreground edge points centerlineInRegion(source, rect, options?) => skeleton (distance-transform) points KEY TYPES ========= Point { x, y } // Y-up doc coords RGBA { r,g,b: 0-255, a: 0-1 } VectorPaint { kind:'solid' } | { kind:'linear', p0:Point, p1:Point, stops:GradientStop[] } | { kind:'radial', center:Point, radius, focalOffset?, stops:GradientStop[] } GradientStop { offset: 0-1, color: RGBA } VectorFillStyle { enabled: boolean, color: RGBA, paint: VectorPaint } VectorStrokeStyle{ enabled, color: RGBA, paint?, width, cap:'butt'|'round'|'square', join:'miter'|'round'|'bevel', miterLimit, dashes:number[], dashOffset } VectorSegment { kind:'line', end } | { kind:'quadratic', control, end } | { kind:'cubic', control0, control1, end } VectorContour { startAnchor: Point, closed: boolean, segments: VectorSegment[], strict? } SelectionState { layerIndex, shapeIndex, elementKind?:'shape'|'text'|'image'|'dimension', dimensionId?, constraintId?, nodeDetail? } NodeSelectionDetail { contourIndex, pointRole:'start-anchor'|'end-anchor'|'control', segmentIndex, handleIndex? } NOTES ===== - The API never mutates directly: it hands the host (label, mutate) via request.commit and the host wires undo. Diffs across actions need a persistent AgentActionLog you pass every call. - Group/ungroup, layer reorder, set-active-artboard, guides, units and export are NOT agent commands — export is a perception concern (renderAgentPng / getAgentDocumentSnapshot.svg). - Boolean ops and outlineStroke are Pro-gated in the app; the action API itself does not enforce gating (the host does).