# Vyuh Diagram > API reference and examples for creating, editing, saving, and exporting drawings. Use vyuh_diagram_kit for Flutter or vyuh_diagram_engine for headless Dart. Core values live in vyuh_diagram_types; JSON and SVG have separate codec packages. Start with Getting Started and the DrawingSession API. --- # Agents Give your coding agent these references to create, edit, save, and export drawings using the toolkit. | Resource | Use | | --- | --- | | [llms.txt](/llms.txt) | A compact index for finding the relevant API and examples. | | [llms-full.txt](/llms-full.txt) | The complete consumer reference in one text file. | The references include package setup, API signatures, runnable examples, supported capabilities, validation errors, and persistence contracts. Use the index when your agent can fetch individual pages, or the full reference when supplying context directly. --- # Actions and keyboard bindings A named action lets buttons, shape regions, and keyboard shortcuts share one edit. Configure actions on `DrawingConfiguration`; invoke them through `session.invokeAction('id', elementId: id)`. Register the same actions when reopening a document: callbacks are host behavior, not serialized content. ```dart import 'package:vyuh_diagram_kit/drawing.dart'; final configuration = DrawingConfiguration( actions: [ DrawingAction( id: 'shape.grow', isEnabled: (context) => context.shape != null, execute: (context) { context.resize( width: context.shape!.frame.width + 40, height: context.shape!.frame.height + 20, ); }, ), ], keyBindings: const [ DrawingKeyBinding( action: 'shape.grow', key: LogicalKeyboardKey.keyG, command: true, shift: true, scope: DrawingActionScope.selection, ), ], ); ``` Use `session.canInvokeAction('shape.grow', elementId: id)` for button availability and `session.invokeAction('shape.grow', elementId: id)` on activation. Availability is checked again at invocation; unknown actions, missing targets, and locked targets are denied. Observe session changes to refresh a control's availability. ## One edit, one Undo The context exposes the current selection, target `elementId`, optional `slotId`, and an immutable document. `context.shape` is the targeted shape when applicable. The document reflects changes already staged by this action. Use `updateValues(patch)` for declared data, `resize(width: ..., height: ...)` for dimensions, and `updateShape(...)` for the latest staged shape. Advanced edits use `replace`, `insert`, `remove` and `select`. Edits commit together as one undo step. Exceptions or denial discard them. Contexts expire after the callback; availability predicates cannot edit. Actions are synchronous. Fetch external data before invoking an action; do not retain a context across an `await`. Do not mutate the session separately from the callback: use the context so the edit remains atomic. Publication automatically resolves and renders affected shapes. Rows and columns reflow within updated dimensions; they do not implicitly resize every container. The shape's declared `textSizing` policy can grow a text body automatically, or the action can explicitly replace its dimensions. No refresh method is needed. ## Keyboard ownership Bindings have three scopes: | Scope | Eligible input owner | | --- | --- | | `canvas` | Canvas with an empty selection | | `selection` | Canvas with an object selection | | `text` | The active diagram text editor | Use `shapeType` to restrict a binding to one shape type and `slotId` to restrict a text binding to one named slot. A slot restriction requires `scope: text`. More specific matching bindings win: slot and shape, then slot, then shape, then the unrestricted scope. Duplicate scope/chord declarations are rejected. `command` means Meta on macOS and Control elsewhere. Modifier matching is exact. Bindings receive initial key-down events, not repeats. A matched binding consumes its chord even when its action is disabled; an unmatched key retains the normal editor behavior. Escape, active IME composition, pointer gestures, and focused embedded controls retain their existing input ownership. Selection and canvas bindings never run in diagram text editing. The playground offers Command+Enter on a selected UML class as an action example. It appends an ordinary text paragraph and selects it; this is free-form text, not a structured UML property. Tab retains normal editor behavior and does not add members. ## Buttons and interactive regions Any Flutter control can invoke a named action. The playground Count button uses `controls.increment`; Command+Shift+= invokes it on a selected controls card. Embedded controls continue to own their own focused keystrokes. For a painted shape region, set its `ShapeRegionLayerDefinition.onInteract` to the action ID. A named action runs on pointer release inside the same region; cancel or release outside does not invoke it. Regions without a matching action continue to use `DrawingHooks.onShapeRegionInteraction`. No repeatable-content grammar or runtime composition insertion is introduced by this API. The action decides how to change the existing canonical data. --- # The drawing API The API reference now has separate pages for each object and capability. [Open the API reference](/agent-docs/api/index.md) - [Session](/agent-docs/api/session.md) - [Configuration](/agent-docs/api/configuration.md) - [Shapes and registries](/agent-docs/api/shapes.md) - [Connectors](/agent-docs/api/connectors.md) - [Canvas](/agent-docs/api/canvas.md) - [Navigation](/agent-docs/api/navigation.md) - [Animation](/agent-docs/api/animation.md) - [Embedded widgets](/agent-docs/api/widgets.md) - [Grids](/agent-docs/api/grids.md) - [Text](/agent-docs/api/text.md) - [Undo and redo](/agent-docs/api/history.md) - [Events and hooks](/agent-docs/api/hooks.md) - [Actions and keyboard bindings](/agent-docs/guide/actions.md) - [Complete custom-library recipe](/agent-docs/guide/build-a-library.md) --- # Build your first shape library Follow this path: [first canvas](/agent-docs/guide/getting-started.md) → this library recipe → [composition rules](/agent-docs/guide/shape-composition.md) → [actions](/agent-docs/guide/actions.md). Use the [common tasks](/agent-docs/guide/common-tasks.md) page for command outcomes and persistence; open the API reference when you need individual options. This complete example uses only `drawing.dart`. Paste it into `lib/main.dart` after installing the kit. The shape has a fixed-height title and a flexible body. The button invokes a named action; the action updates declared data and width together in one Undo step. Editing body text uses the normal text editor. ```dart import 'package:flutter/material.dart'; import 'package:vyuh_diagram_kit/drawing.dart'; final reviewLibrary = ShapeLibrary( id: 'review', name: 'Review shapes', shapes: [ ShapeDefinition( type: 'review.card', displayName: 'Review', defaultSize: const DiagramSize(240, 140), valueFields: { 'revision': const NumberValueField(defaultValue: 0, minimum: 0, integer: true), }, composition: ShapeColumn([ ShapeSized(extent: 36, child: const ShapeText('title', text: 'Review', singleLine: true)), const ShapeText('body', text: 'Describe the review'), ]), ), ], ); DrawingSession createReviewSession() => DrawingSession( shapeRegistry: ShapeRegistry.fromLibraries([ ShapeLibraries.standard, reviewLibrary, ]), configuration: DrawingConfiguration( actions: [ DrawingAction( id: 'review.revise', isEnabled: (context) => context.shape?.type == 'review.card', execute: (context) { context.updateValues({ 'revision': (context.shape!.values['revision'] as num) + 1, }); context.resize(width: context.shape!.frame.width + 20); }, ), ], keyBindings: const [ DrawingKeyBinding( action: 'review.revise', key: LogicalKeyboardKey.keyR, command: true, shift: true, scope: DrawingActionScope.selection, shapeType: 'review.card', ), ], ), ); void main() => runApp(const MaterialApp(home: ReviewCanvas())); class ReviewCanvas extends StatefulWidget { const ReviewCanvas({super.key}); @override State createState() => _ReviewCanvasState(); } class _ReviewCanvasState extends State { late final session = createReviewSession(); late final String card; @override void initState() { super.initState(); card = session.createShape( type: 'review.card', worldCenter: const DiagramPoint(300, 200), ); } @override void dispose() { session.dispose(); super.dispose(); } @override Widget build(BuildContext context) => Scaffold( body: Column(children: [ TextButton( onPressed: () => session.invokeAction('review.revise', elementId: card).requireAllowed(), child: const Text('Revise and widen'), ), Expanded(child: DrawingCanvas(session: session)), ]), ); } ``` The declared `revision` value is persisted data; it is not automatically rendered as text. Use [embedded controls](/agent-docs/api/widgets.md) to display domain values, or use `session.text.setPlainText(elementId: id, slotId: 'body', text: value)` for an explicit text edit. Plain-text editing does not require manipulating paragraphs, run offsets or selections. That separate command is its own Undo operation. Keep the same library and actions registered when reopening saved JSON. Libraries define shape types; the registry combines the chosen libraries and validates IDs. Documents store instances and content, not executable callbacks. For a separate package, export your `ShapeLibrary` from its public Dart entry point; depend on layout/types for portable declarations, and keep Flutter controls in the host. ## Check your integration 1. Add a card and edit its title and body independently. 2. Activate Revise and widen; confirm one Undo restores both value and width. 3. Save with `session.document.encode()` and reopen using the same registry. 4. Try a locked target and handle `DiagramChangeDenied` at your UI boundary. 5. Unmount and dispose the session without retaining action contexts. The repository's executable recipe check verifies actions, Undo, persistence and denials. It is not evidence that an unfamiliar developer has completed onboarding. Hosted installation and observed novice onboarding remain separate release checks. --- # Canvas, camera, and grids Configure the camera, grid and minimap without changing document content. Use the full Flutter kit entry point for grids and view extensions: ```dart import 'package:vyuh_diagram_kit/vyuh_diagram_kit.dart'; ``` The examples below assume an existing `DrawingSession session`. ## Move the viewpoint Pan and zoom without rewriting shape coordinates. Fit the document or selection, or animate toward an element after the canvas has mounted: ```dart await session.animateToElement( elementId, duration: const Duration(milliseconds: 500), padding: 48, ); ``` The session also supports animated navigation to points, rectangles, regions, and selections. User navigation interrupts camera animation. Configure an infinite or finite canvas through `session.configuration.canvasPolicy`. Apply changes with `session.setConfiguration(session.configuration.copyWith(canvasPolicy: policy))`. ## Choose a grid Grid visibility and snapping are independent configuration options. Showing a grid does not turn snapping on, and snapping can stay enabled while the grid is hidden. ```dart session.setConfiguration( session.configuration.copyWith( grid: DrawingGridConfiguration( visible: true, snap: true, size: 20, pattern: DiagramGridPattern.dots, ), ), ); ``` Choose `dots`, `lines`, or `layered`. Layered grids combine fine and major lines. Every pattern uses the same grid spacing as snapping. At distant zoom levels, the painter thins visible grid marks while preserving the snapping interval. ## Compose your own pattern Custom patterns use the same dot, line, and cross productions as the defaults. Colors, screen-space mark sizes, and spacing multiples belong to each layer: ```dart final engineeringGrid = DiagramGridPattern( id: 'engineering', label: 'Engineering', layers: [ DiagramDotGridLayer( color: DiagramColor(0xFFBBC9E8), size: 2, ), DiagramLineGridLayer( spacingMultiple: 5, color: DiagramColor(0xFF8296C7), ), ], ); session.setConfiguration( session.configuration.copyWith( grid: session.configuration.grid.copyWith(pattern: engineeringGrid), ), ); ``` Patterns are validated, immutable compositions. A custom pattern changes the painted grid, not the document geometry or snapping rules. Arbitrary custom renderer callbacks are not part of this grid contract. ## Set the minimap detail policy The minimap defaults to shape outlines and fills, without connections. It uses resolved shape geometry and clipping; text, handles and detailed editing chrome are omitted. ```dart final minimap = DiagramMinimapExtension( initiallyExpanded: true, policy: DiagramMinimapPolicy( shapeAppearance: DiagramMinimapShapeAppearance.outlines, showConnections: false, ), ); session.setConfiguration(session.configuration.copyWith( extensions: [...session.configuration.extensions, minimap], )); ``` Use `outlinesAndFills` for colored silhouettes, or opt into `showConnections`. A policy can provide `shapePainterForType` for custom registered types. Thumbnail painters receive resolved geometry and must remain deterministic; replacing the policy refreshes external thumbnail inputs. They affect presentation only. Camera movement reuses a recorded scene picture. Document or policy changes rebuild that picture, so a changed scene still costs work proportional to its shapes. Thousands of shapes are not automatically a constant-cost or 120 fps minimap; measure the representative document and chosen thumbnail policy. ## Particle count and size Count controls the number of particles evenly distributed along a path. It does not shrink their dimensions. Size is the radius for circles and polygons; character particles use a font scale of twice that value. Dash length is eight times size, capped only by total path length, while width sets dash thickness. The configured screen/world scaling policy controls zoom behavior. Dense particles can overlap. --- # Common tasks Start with `vyuh_diagram_kit` in Flutter, or `vyuh_diagram_engine` in a Dart service. Both expose the same content commands. Keep one session for the editing lifetime; widgets observe it rather than owning a second document. | Task | Start here | | --- | --- | | Mount a drawing | `DrawingSession` and `DrawingCanvas(session: session)` | | Create content with registry defaults | `createShape`, `createText` | | Insert an already-authored record | `addElement` | | Connect existing objects | `connect` | | Add a label | `addConnectorLabel(text: ...)` | | Rename text | `session.text.setPlainText` | | Preserve rich formatting while editing | `session.text.replace` | | Save committed content | `session.document.encode()` | | Observe edits for autosave | `addDocumentChangeListener` | | Open saved content | `DrawingSession.fromJson` with the original custom registry | | Undo or redo | `undo().requireAllowed()`, `redo().requireAllowed()` | | Customize shapes and label grammar | `ShapeRegistry` | | Customize canvas appearance and UI | `DrawingConfiguration` | ## Create, connect and rename ```dart final source = session.createShape( type: Shapes.rectangle, worldCenter: const DiagramPoint(100, 100), text: 'Draft', ); final target = session.createText( worldCenter: const DiagramPoint(400, 100), text: 'Review', ); final connector = session.connect(source, target); session.addConnectorLabel(connector, text: 'Ready\nfor review'); session.text.setPlainText(elementId: source, text: 'Proposal').requireAllowed(); ``` `setPlainText` preserves story identity and replaces formatting with ordinary paragraphs. Newline sequences become paragraph boundaries, including a final empty line. Text that looks like a list remains plain text. Rich-text replacement is available when you need to retain marks or list structure. For a connector with several labels, pass the desired label's `story.id` as `storyId`. An ambiguous connector is denied rather than silently editing its first label. The same canonical admission checks enforce single-line regions, locks and host policies. Authored content that violates grammar is rejected; it is not silently truncated. For a story with several text slots, also pass `slotId`. The edit preserves role and slot bindings and leaves all other slots unchanged. Omitting the slot for an ambiguous story is denied. ## Handle command outcomes Use this convention consistently in your host: - Commands returning an identity throw `DiagramChangeDenied` on a policy denial. - Commands returning `DiagramPolicyDecision` let you inspect the result. Call `requireAllowed()` to use the same exception path as identity-returning commands. - Void document edits report denied publication by throwing. Selection and navigation are presentation operations with their own preconditions. - Invalid authored data, unknown identities and invalid lifecycle calls can throw `ArgumentError`, `StateError` or format errors. Do not catch those as ordinary policy denials. ```dart try { session.text.setPlainText(elementId: source, text: 'Approved').requireAllowed(); } on DiagramChangeDenied catch (error) { onDenied(error.decision); // Your host's feedback callback. // Use error.decision.code to choose host messaging or localization. // error.decision.reason is optional human-readable detail. } ``` `DiagramDenialCode` provides stable categories. Host policies created with `DiagramPolicyDecision.deny(...)` have the `policy` code. Use `DiagramPolicyDecision.denied(code, reason: ...)` to supply a category explicitly. Never parse the reason string. A successful individual command is one undoable operation; several statements in a try block are not an atomic batch. ### One error-handling boundary Creation returns the new ID; admitted editing commands return a decision. For an application that handles denials with exceptions, call `requireAllowed()` on decisions and catch `DiagramChangeDenied` once at the initiating UI boundary. Keep IDs as IDs: no result wrapper is needed to create and connect two shapes. Named actions use this same decision contract, including typed denial codes. For several changes that must undo together, register a [named action](/agent-docs/guide/actions.md) and stage edits through its context. Do not call independent session commands inside that callback. Use `context.updateValues` for declared data, `context.resize` for dimensions, and `context.updateShape` for other shape fields. For a single ordinary text edit, use `session.text.setPlainText` with a slot ID; manual text-story replacement is an advanced escape hatch. ## Choose the correct owner | Concern | Owner | Lifetime | | --- | --- | --- | | Shapes, ports, text regions, label block grammar | `ShapeRegistry` and definitions | Establish when creating a session; reuse for saved documents. | | Creation/connection rules and document validation | Session policies | Canonical admission for API and interactive edits. | | Before-change approval | Core `beforeChange`; Flutter `DrawingConfiguration.hooks.beforeChange` | Canonical admission, including history. | | Grid, navigation, UI extensions and widget builders | Flutter `DrawingConfiguration` | Presentation configuration for the session. | | Font metrics | Text-layout engine | Choose before creating a session that will be mounted. | | Autosave | Document-change listener | Subscribe once, remove before host disposal. | | Pointer preview and camera feedback | General/presentation observation | Do not treat transient previews as committed autosave data. | ## Platform capabilities | Capability | Portable session | Flutter session, unmounted | Flutter session, mounted | | --- | --- | --- | --- | | Create/edit/history/JSON | Yes | Yes | Yes | | Text metrics | Explicit backend; default fixed metrics | Flutter shaping | Flutter shaping | | SVG | Prepared image/font resources | Flutter output adapter | Flutter output adapter | | PNG | Requires raster output adapter | Supported by Flutter output | Supported by Flutter output | | Navigate or read viewport | No mounted viewport | Requires attachment | Supported | | Pointer, keyboard and IME | No | No | Supported | Native shaping is available through the separate Pango library; native resources and fonts need their own setup and disposal. Fixed metrics are useful for deterministic headless work, but do not promise font-accurate output. `DrawingSession.fromSession(coreSession)` borrows the existing canonical owner. Configure Flutter text layout on that source before mounting, and dispose the Flutter adapter before its source. It does not create a collaboration participant. ## Progress to custom definitions Follow [shapes](/agent-docs/api/shapes.md), then [connectors](/agent-docs/api/connectors.md), [text](/agent-docs/api/text.md), [embedded widgets](/agent-docs/api/widgets.md) and [ownership](../api/ownership). Add one concern at a time: outline, ports, text regions, declared values, widget slots, then custom tools. Keep content in declared values and stories; a widget builder is a projection and dispatches through its bounded commands. An `enabled` snapshot is presentation feedback, not permission to bypass live admission. ## A small starting surface For a new Flutter host, start with: `import 'package:vyuh_diagram_kit/drawing.dart';` This entry point exposes the everyday canvas, session, style, shape-definition and error contracts. It re-exports the same objects, not another implementation. The existing `vyuh_diagram_kit.dart` import remains supported and provides the full extension surface for custom geometry, tools, effects, text grammars and export adapters. Add it when the task calls for those capabilities. ## Draw lines without coordinate bookkeeping ```dart final line = session.createLine( start: const DiagramPoint(40, 60), end: const DiagramPoint(240, 160), ); session.createPolyline( points: const [ DiagramPoint(300, 60), DiagramPoint(460, 60), DiagramPoint(380, 180), ], closed: true, ); final corner = session.paths.addPoint(line, after: 'node-0'); session.paths.setHandleMode(line, corner, DiagramPathHandleMode.smooth) .requireAllowed(); ``` Points are in world coordinates. The session computes the frame, normalizes nodes, applies parent placement and records one undo step. `createPath` remains available for authored Bézier nodes, tangent offsets and explicit frames. Creation returns an ID or throws on denial; edits return a decision you can inspect or promote to an exception with `requireAllowed()`. ## Declare a custom shape and its inspector A rectangle with editable text needs only `ShapeDefinition(type: 'task')`. Standard defaults supply its outline, padded text region, size and capabilities. Declare only the differences: ```dart final task = ShapeDefinition( type: 'task', displayName: 'Task', valueFields: { 'progress': const NumberValueField(defaultValue: 0, minimum: 0, maximum: 100), 'status': TextValueField(defaultValue: 'Draft', allowedValues: ['Draft', 'Ready']), }, inspector: ShapeInspectorDefinition([ ShapePropertyEditor.customValues, ShapePropertyEditor.fill, ShapePropertyEditor.stroke, ]), ); final taskSession = DrawingSession(shapeRegistry: ShapeRegistry.withStandard([task])); taskSession.createShape( type: 'task', worldCenter: const DiagramPoint(100, 100), text: 'Review proposal', ); ``` Omit `inspector` to derive editors from capabilities, or provide a list to choose their order. Unsupported and duplicate entries reject. Visibility does not change edit permissions. Path and gradient controls use the same declarations. --- # Create custom shapes A custom type is a registry definition, not a new painter or a private drag implementation. Compose the available outline, text, port, and capability grammar; the shared runtime resolves its geometry and handles editing. ## Declare a node ```dart import 'package:vyuh_diagram_kit/vyuh_diagram_kit.dart'; ShapeDefinition reviewNode() => ShapeDefinition( type: 'example.review', outline: const ShapeOutlineDefinition.rectangle(), textBounds: const ShapeBoundsDefinition.inset(16), minimumSize: const DiagramSize(140, 80), defaultSize: const DiagramSize(220, 120), defaultStyle: const ShapeStyle(cornerRadius: 12), capabilities: ShapeCapabilities( rotatable: false, cornerRadiusEditable: true, textSizing: ShapeTextSizing.growHeight, ), connection: ShapeConnectionDefinition.fixedPorts([ PortDefinition.onSide( id: 'input', side: PortSide.left, ), PortDefinition.onSide( id: 'output', side: PortSide.right, ), ]), ); ShapeRegistry reviewRegistry() => ShapeRegistry.withStandard([ reviewNode(), ]); ``` Pass `reviewRegistry()` into the session's `shapeRegistry`. Before or after mounting, create an instance using the registered type: ```dart String insertReview(DrawingSession session) => session.createShape( type: 'example.review', worldCenter: const DiagramPoint(320, 240), ); ``` The creation command uses definition defaults and the canonical text-story factory. The instance participates in shared selection, text editing, history and routing. ## Keep one definition per type `ShapeRegistry.withStandard(custom)` adds unique types. Duplicate definitions are rejected. To intentionally replace a built-in definition, use the separate `overrides` argument with an existing type ID. To build an entirely curated library, construct `ShapeRegistry(definitions)` without standard shapes. ## Attach capabilities `ShapeCapabilities` declares selection, movement, deletion, resize sites, rotation, text editing, appearance editing, text growth and child ownership/clipping. These are definition-level behaviors. Per-instance locks use `session.setElementsLocked(ids, locked)` and inherit through owners; shared admission enforces them across commands. The visible rotation handle is opt-in per definition. Keep rotation enabled while choosing whether to show its stem-and-circle control: ```dart final rotationCapabilities = ShapeCapabilities( rotatable: true, transform: const ShapeTransformBehavior(showRotationHandle: true), ); ``` `showRotationHandle` defaults to `false`. Desktop corner rotation remains available when `rotatable` is true. Setting `rotatable: false` suppresses every rotation control, including the visible handle. Text layers can declare named slots and title/body regions through `ShapeTextLayerDefinition`. Their content stays in the shared rich-text story. A title or body should not introduce a second text editor. ## Embed paragraph content A text region uses the same rich-text story and editor as a text box. Declare its bounds and slot; the kit owns input, selection, layout and undo. Several paragraph blocks can flow through one slot. ```dart final descriptionLayer = ShapeTextLayerDefinition( id: 'description', slotId: 'description', bounds: ShapeBoundsDefinition.inset(16), verticalAlignment: DiagramTextVerticalAlignment.top, emptyPlaceholder: 'Add a description…', ); ``` Add the layer to your definition's `layers`. Multiline content is the default; `singleLine: true` is an explicit restriction for a short title region. Set `ShapeCapabilities(textSizing: ShapeTextSizing.growHeight)` when the shape should grow to accommodate content. Fixed-size regions retain their declared bounds. Text boxes created with a click start in automatic-width mode: Enter adds a new paragraph, while the box grows horizontally as you type. Drag to create a wrapping box, or resize its width afterward. Its height grows to fit the paragraphs. Paragraphs retain canonical runs, marks, list kind, nesting and alignment. Shapes and connector labels share story ownership, layout and editing commands; a host should not add an independent text field with a second copy of the content. Connections can be disabled, outline-bound, fixed-port, default-side-port, or instance-port based. `ShapePortLayout.distribute` positions multiple ports on a side using the same resolver-owned placement contract. ## Keep domain meaning above geometry The names `input` and `output` in this example are stable port IDs. They do **not** automatically enforce workflow direction or payload compatibility. A domain package declares those meanings and supplies a session `connectionPolicy` to enforce them. Custom type IDs are open. Geometry productions are deliberately finite: rectangle, ellipse, regular polygon and authored path. Adding a genuinely new geometry or interaction primitive requires extending its shared grammar and resolution contract, not bypassing it with shape-specific painting. Embed native controls with `ShapeWidgetLayerDefinition` and a `ShapeWidgetRegistry` in the drawing configuration. Their values remain canonical shape content; the host supplies portable fallback layers for unmounted and output projections. See [widgets inside shapes](/agent-docs/api/widgets.md) for the complete declaration, editing and lifecycle API. See [capability status](/agent-docs/reference/capabilities.md) before relying on embedded controls, port labels, or connector-label editing. ## Shared text regions Every text-capable shape uses the same story, block, run, and resolved-line pipeline. Define separate text regions for a card title and body rather than mounting private text editors. A single-line region accepts one paragraph with inline formatting. Enter finishes editing that region. Multiline regions support paragraph blocks, ordered and unordered list items, nested lists, and checklist items. Typing `[] ` or `[ ] ` at the beginning of a multiline paragraph creates a checklist item. The default line-height multiplier is 1.3. Lines reflow within the region width after accounting for list indentation. Empty paragraphs still resolve a full line and caret. The declared text sizing policy controls whether the owner grows or retains its bounds; rendering and editing consume the same line geometry. Checklist markers use resolved geometry for painting and clicking. Clicking a checkbox toggles its canonical block state, preserves text selection, and can be undone. Paragraph alignment applies only to ordinary paragraphs. Bulleted lists, numbered lists, and checklists remain left-aligned in their region, including nested items. Markers reserve their measured width plus a half-em gap before the item text. ## Standalone text Use `TextElement` for text that does not belong to a shape. Configure its regions and interaction behavior through the registry's `text: TextElementDefinition(...)` argument. It shares `FramedElement` transforms and the same rich-text story machinery as cards, but exposes no shape style, ports, image, or child ownership. The reserved `text` type is not a custom shape definition. See [the text API](/agent-docs/api/text.md#standalone-text) for an insertion example. --- --- prev: text: '7. Add a custom review node' link: /guide/flowchart-custom-shapes next: text: '9. Control the canvas and grid' link: /guide/flowchart-canvas-controls --- # 8. Share actions and shortcuts A named action groups an edit so a button and a shortcut perform exactly the same operation. Add this import: ```dart import 'package:flutter/services.dart'; ``` Add `configuration:` to the existing session constructor alongside `shapeRegistry:`: ```dart configuration: DrawingConfiguration( actions: [ DrawingAction( id: 'review.grow', isEnabled: (context) => context.shape?.type == 'tutorial.review', execute: (context) { final shape = context.shape!; context.resize(width: shape.frame.width + 40, height: shape.frame.height + 20); }, ), ], keyBindings: const [ DrawingKeyBinding( action: 'review.grow', key: LogicalKeyboardKey.keyG, command: true, shift: true, scope: DrawingActionScope.selection, ), ], ), ``` Add this method and include `buildGrowButton()` in the reactive toolbar: ```dart Widget buildGrowButton() { final selection = session.selection; final ids = selection is ElementSelection ? selection.elementIds : {}; final id = ids.length == 1 ? ids.single : null; return TextButton( onPressed: id != null && session.canInvokeAction('review.grow', elementId: id) ? () => showDecision(session.invokeAction('review.grow', elementId: id)) : null, child: const Text('Grow review'), ); } ``` Availability is rechecked at invocation. Changes staged through the action context commit together as one Undo entry. An exception or denial discards them. Use the context inside `execute`; do not issue independent session edits or keep the context across an `await`. Here `command` maps to Meta on macOS and Control elsewhere. Selection-scoped bindings belong to object selection, not active text editing or a focused host text field. Choose a chord that does not collide with your application's shortcuts. **Checkpoint:** select a review node and use Grow review, then Command/Ctrl+Shift+G with canvas focus. Both grow it and Undo reverses each operation. Select a diamond: Grow review disables. [Action reference](/agent-docs/guide/actions.md) covers canvas/text scopes and painted regions. --- --- prev: text: '8. Share actions and shortcuts' link: /guide/flowchart-actions next: text: 'Explore the flowchart example' link: /examples/flowchart --- # 9. Control the canvas and grid Finish with view controls. These change presentation and interaction settings without rewriting the saved flowchart. Add a grid toggle to the reactive toolbar: ```dart FilterChip( label: const Text('Grid'), selected: session.canvasConfiguration.grid.visible, onSelected: (visible) => session.setConfiguration( session.canvasConfiguration.copyWith( grid: session.canvasConfiguration.grid.copyWith(visible: visible, snap: visible), ), ), ), ``` This UI deliberately toggles visibility and snapping together. They are independent SDK options. When visible, direct manipulation snaps to displayed grid spacing; when hidden with snapping enabled, it uses the configured base interval. Zooming out can reduce grid density. Add two more toolbar controls: ```dart TextButton( onPressed: () => session.navigateToSelection(), child: const Text('Fit selection'), ), TextButton( onPressed: () => session.navigateToPoint(DiagramPoint.zero, zoom: 1), child: const Text('Reset view'), ), ``` Navigation needs a mounted, laid-out canvas. These callbacks run after mounting. For initial navigation, use a post-frame callback and verify that the State is still mounted and the session attached. For a zoom percentage, register `addPresentationListener`, read `session.cameraState?.zoom`, and update a small host notifier. Remove the same listener during cleanup. Document listeners will not update a zoom label. The default canvas is infinite. Set `canvasPolicy` to `FiniteCanvasPolicy` with world bounds if your editor needs a bounded workspace. Configuration updates preserve document and history; use `copyWith` so installed actions and hooks survive your grid toggle. **Checkpoint:** select nodes and fit the selection, change the grid, pan and zoom. Saving still captures authored world coordinates; camera movement creates no document Undo entry. ## Where to go next You now have a flowchart canvas, a registry, reactive host controls, an inspector, connections, a JSON boundary, a custom node and a shared keyboard action. Your application supplies storage, authorization and domain semantics around those boundaries. Try the [interactive flowchart example](/agent-docs/examples/flowchart.md), then use [Common tasks](/agent-docs/guide/common-tasks.md) for focused recipes or [Build a library](/agent-docs/guide/build-a-library.md) to package your domain shapes. [Navigation](/agent-docs/api/navigation.md) and [Grids](/agent-docs/api/grids.md) cover the remaining view controls. --- --- prev: text: 'Tutorial overview' link: /guide/getting-started next: text: '2. Register and create shapes' link: /guide/flowchart-shapes --- # 1. Set up the canvas Start with an empty Flutter app. By the end of this page you will have a bounded, interactive canvas and a session whose lifetime belongs to your screen. ::: info Package access You need access to the Vyuh package registry at `https://pub.vyuh.tech` with your designated access token to install `vyuh_diagram_kit` and its companion packages. Register your token before fetching dependencies: ```sh dart pub token add https://pub.vyuh.tech ``` Enter your designated token when prompted. Keep it private; do not commit it to your repository or include it in application source. Enterprise licenses are allocated manually after onboarding and payment through Paddle. Our team provisions your company and package access, then sends your designated registry token. A Paddle payment receipt is not a registry token. Once your token is installed, follow the dependency setup below and run `flutter pub get` to download the package. If access is denied, contact the onboarding team to check your allocation. **Don't have a license yet? [Contact us for a license](/#licensing).** ::: ## What you need Use Flutter 3.47 or later with Dart 3.13 or later. Add `vyuh_diagram_kit` to your application: ```yaml dependencies: material_ui: ^1.1.0 vyuh_diagram_kit: hosted: https://pub.vyuh.tech version: ^2.2.0 ``` Run `flutter pub get`. The kit brings in the Flutter renderer, headless engine, types, and codecs. Add a companion package directly only when your application imports it. For a headless Dart service, use `vyuh_diagram_engine` instead. It exposes `DrawingSession` as the headless application entry point. See [package ownership](/architecture/#package-ownership) for the complete package list. This tutorial uses `material_ui` for its `MaterialApp`, `Scaffold` and buttons. The bundled inspector uses that same Material implementation through CDX. Keep this import throughout the tutorial so its controls have the matching theme and Material ancestors. Your own Flutter UI can still use the session independently; a custom inspector needs only the public command and subscription APIs. ## Mount the session Replace `lib/main.dart` with this complete app. All later steps edit this file. Keep the same `DiagramSurface` and its State throughout the tutorial. ```dart import 'package:material_ui/material_ui.dart'; import 'package:vyuh_diagram_kit/drawing.dart'; void main() => runApp(MaterialApp( theme: ThemeData(splashFactory: InkRipple.splashFactory), home: const Scaffold(body: DiagramSurface()), )); class DiagramSurface extends StatefulWidget { const DiagramSurface({super.key}); @override State createState() => _DiagramSurfaceState(); } class _DiagramSurfaceState extends State { late final session = DrawingSession(); @override void initState() { super.initState(); } @override void dispose() { session.dispose(); super.dispose(); } @override Widget build(BuildContext context) => SizedBox.expand( child: DrawingCanvas(session: session), ); } ``` The empty `initState` is where we will seed the flowchart next. Create the session once, outside `build`, and dispose it with its owner. Each session supports one mounted canvas. Child toolbars and panels borrow that session; they do not dispose it. The canvas needs finite layout constraints. `Scaffold.body` provides them here; inside a Column use `Expanded`. Content commands work before mounting, but camera commands need a mounted, laid-out canvas. **Checkpoint:** run `flutter run`. You should see an empty canvas without layout errors. --- --- prev: text: '6. Save and load the drawing' link: /guide/flowchart-persistence next: text: '8. Share actions and shortcuts' link: /guide/flowchart-actions --- # 7. Add a custom review node Now give our flowchart a reusable review node with left and right ports. Add this top-level definition below the app classes: ```dart ShapeDefinition reviewNode() => ShapeDefinition( type: 'tutorial.review', outline: const ShapeOutlineDefinition.rectangle(), textBounds: const ShapeBoundsDefinition.inset(16), minimumSize: const DiagramSize(140, 80), defaultSize: const DiagramSize(220, 120), defaultStyle: const ShapeStyle(cornerRadius: 12), capabilities: ShapeCapabilities( rotatable: false, cornerRadiusEditable: true, textSizing: ShapeTextSizing.growHeight, ), connection: ShapeConnectionDefinition.fixedPorts([ PortDefinition.onSide(id: 'input', side: PortSide.left), PortDefinition.onSide(id: 'output', side: PortSide.right), ]), ); ``` Replace the session initializer: ```dart late final session = DrawingSession( shapeRegistry: ShapeRegistry.withStandard([reviewNode()]), ); ``` In both the initial process creation and `addProcess`, replace `Shapes.rectangle` with `'tutorial.review'`. Hot restart because the session is created once. Existing saved rectangles do not automatically become the new type. Before restarting, also replace the two connection calls in `initState`. A ports-only review node requires an explicit port ID on its endpoint: ```dart session.connect(startId, processId, targetPortId: 'input'); session.connect(processId, decisionId, sourcePortId: 'output'); ``` The other endpoints still bind to the standard shapes' outlines. Omitting a review port is invalid; the SDK does not silently choose an outline attachment for a ports-only shape. The definition owns shape geometry and behavior. The instance owns its ID, position and text. Shared code handles text editing, hit testing, movement, port placement, routing, saving and history. You do not implement a second painter/drag pipeline. ## Capabilities, locks and host permissions | Mechanism | Question it answers | Example | | --- | --- | --- | | Definition capabilities | What edits does this type support? | Review nodes cannot rotate. | | Instance locks | Is this object currently locked? | A finalized node cannot move. | | Host admission | Is this candidate edit permitted now? | The current user has read-only access. | Unlocking a node does not grant a capability its definition lacks. All three can affect the decision returned by an edit. This is why handling `allowed` belongs in custom controls too. Add a `DrawingHooks.beforeChange` callback in configuration when you need host-wide admission; return `DiagramPolicyDecision.deny('This drawing is read-only.')` or `allow()`. It is synchronous and read-only. Server authorization remains your backend's responsibility. Fixed domain invariants belong in `documentValidator`; see [hooks](/agent-docs/api/hooks.md). **Checkpoint:** add a review node, edit its text, connect through its ports and save/load it. It has no rotation interaction. For richer compositions and native controls, continue later with [Build a library](/agent-docs/guide/build-a-library.md). --- --- prev: text: '5. Show properties and connections' link: /guide/flowchart-properties next: text: '7. Add a custom review node' link: /guide/flowchart-custom-shapes --- # 6. Save and load the drawing Save the committed document, not widget state or resolved routes. Add this field and these methods to the State for a storage-free round trip: ```dart String? savedJson; void saveDrawing() { savedJson = session.document.encode(); } void loadDrawing() { final json = savedJson; if (json == null) return; showDecision(session.importJson( json, expectedRevision: session.document.revision, )); } ``` Add Save and Load buttons to `buildToolbar` using these callbacks. This intentionally keeps the saved text in memory; connect the same JSON boundary to your file picker or backend for durable storage. ## Observe completed edits For autosave, register a dedicated listener in `initState`: ```dart session.addDocumentChangeListener(onDocumentChanged); ``` Add its callback and remove it in `dispose`, before disposing the session: ```dart void onDocumentChanged(DiagramDocumentChange change) { final json = change.after.encode(); // Enqueue json in your host's debounced, ordered persistence queue. // Keep file/network work outside this synchronous notification. } // In dispose(): // session.removeDocumentChangeListener(onDocumentChanged); ``` Wire the queue when you choose storage; the callback above only illustrates the subscription. Completed changes include Undo and Redo. A canceled drag produces no completed edit to save. Order asynchronous writes so an older request cannot overwrite a newer revision, and surface storage failures in your application. ## Loading after asynchronous work Capture `session.document.revision` **before** awaiting a file or network read, then pass that captured value as `expectedRevision`. If editing occurred while waiting, the SDK rejects the stale replacement with `DiagramRevisionConflict`. Let the user retry or choose how to reconcile the content. Do not simply substitute the latest revision to bypass this check. The in-memory Load above is synchronous, so capturing at invocation is sufficient. For external input, handle decode/validation errors as well as policy denial and revision conflicts. A new session can open JSON with `DrawingSession.fromJson`. Supply the same custom registry used to create it. Definitions, action callbacks, host widgets and runtime presentation are not serialized into the document. Reinstall them when reopening. **Checkpoint:** Save, move a node, Load. The saved drawing returns; Undo can reverse the accepted replacement. See [persistence](/agent-docs/api/persistence.md) for codecs and migrations. --- --- prev: text: '4. Connect reactive state to your UI' link: /guide/flowchart-reactivity next: text: '6. Save and load the drawing' link: /guide/flowchart-persistence --- # 5. Show properties and connections The built-in inspector is included in Diagram Kit. It uses the same `DrawingSession` as the canvas. You can also build your own properties panel using the custom control pattern below. Keep the toolbar above the canvas. Replace its Expanded canvas child with a bounded row: ```dart Expanded( child: Row(children: [ Expanded(child: DrawingCanvas(session: session)), SizedBox(width: 280, child: DrawingInspector(session: session)), ]), ), ``` The inspector observes the same session and shows properties applicable to the selection. It writes through the same commands, admission and undo history. On a narrow screen, put it in a drawer instead of reserving 280 pixels. ## Connect our flowchart Append these commands to `initState` after creating all three nodes and before subscribing: ```dart session.connect(startId, processId); session.connect(processId, decisionId); ``` Connections retain endpoint identities. Moving a node causes the shared resolver to update the route; the toolbar does not recalculate connector coordinates. Connection mode and port policy come from registered definitions. Port names alone do not enforce workflow direction or business rules. ## Write a custom property control A custom panel follows the same read/command/observe loop. Add this method to the State: ```dart Widget buildSelectionTools() { final selection = session.selection; final ids = selection is ElementSelection ? selection.elementIds : {}; return TextButton( onPressed: ids.length == 1 ? () => showDecision(session.moveElements(ids, const DiagramPoint(20, 0))) : null, child: const Text('Move selected right'), ); } ``` Add `buildSelectionTools()` to the toolbar children. Its existing reactive builder refreshes this control when selection changes. For an appearance field, read `session.document.shapeById(id)` and submit `setShapeStyle`; for registered domain fields use `updateValues`. Do not mutate the returned element. See [shape properties](/agent-docs/api/shapes.md) for those command signatures. Selection can represent text or a connector label as well as objects, so handle the selection variant before treating it as a set of node IDs. A disabled button is a usability hint; admission checks still run at execution. **Checkpoint:** select a node, edit it in the inspector, drag it and undo. Its connections remain attached, and your custom selection button follows the selection. --- --- prev: text: '3. Create the toolbar' link: /guide/flowchart-toolbar next: text: '5. Show properties and connections' link: /guide/flowchart-properties --- # 4. Connect reactive state to your UI The session exposes read-only MobX-observable getters. Documents remain immutable snapshots; a document reference you saved earlier never changes in place. ## From an action to a visible update Canvas gestures and toolbar commands use the same engine transaction pipeline. The engine validates the operation, prepares state and resolved geometry, then publishes the update. The session batches observable invalidations for that publication. The canvas already subscribes internally; your own controls can observe the public session getters. ## Rebuild the toolbar Add `flutter_mobx: ^2.4.0` to your app dependencies, run `flutter pub get`, and import `package:flutter_mobx/flutter_mobx.dart`. Replace `buildToolbar()` in the Column's children with: ```dart Observer( builder: (context) => buildToolbar(), ), ``` Inside `buildToolbar`, use the current history state: ```dart // Undo button: onPressed: session.canUndo ? () => showDecision(session.undo()) : null, // Redo button: onPressed: session.canRedo ? () => showDecision(session.redo()) : null, ``` Reads of `selection`, `activeTool`, `canUndo`, `canRedo`, `isInteracting`, `document`, `scene`, `snapshot`, `configuration`, and `cameraState` inside an Observer are tracked. Only changes to the values that it reads invalidate it. Read those values during the builder's execution, not exclusively in a button's later `onPressed` callback. Nested child widgets need their own Observer when those children perform the reads. There is no manual subscription or revision counter to dispose. Observer manages its own reaction; your screen still owns and disposes the session after unmounting its canvas. Avoid wrapping the whole editor when a small toolbar or property control is sufficient. Kit consumes the engine session observables. ## Choose the right subscription | Your requirement | Subscribe with | Read | | --- | --- | --- | | Toolbar availability, selection or active tool | `Observer` / MobX `reaction` | Tracked session getters | | Save, audit or synchronize completed edits | `addDocumentChangeListener` | `change.after`, `change.elements` | | Zoom label or camera overlay | `addPresentationListener` | `session.cameraState` | ### Subscribe to one element Read `session.elementById(id)` inside an `Observer` or MobX reaction when a control depends on one shape or connector. Unlike reading `session.document`, this does not invalidate the control when a different element changes. Removal returns null; Undo can restore the element and notify the same subscription. For a pure-Dart listener, use `session.watchElement(id)`. Its read-only `value`, `addListener` and `removeListener` follow the `listen` package's `ValueListenable` contract. Dispose the signal when its consumer is done. The engine separates document, scene, selection, tool, history, interaction, camera and settings notifications. A selector is evaluated only when its domain changes and it has listeners. Element comparisons use only actively subscribed IDs on committed document changes; unchanged elements emit no notification. MobX attaches these subscriptions only while a reaction observes them and batches invalidations from one publication. Built-in controls use these narrow subscriptions too. The inspector observes its displayed properties, diagnostics observe committed content, and overlays subscribe separately to geometry, camera, selection or alignment guides. Canvas rendering continues to receive live geometry during a drag. The retained `addListener` API includes interaction previews and provisional text edits. MobX observers invalidate only when a tracked getter changes. Public `document`, `scene` and `snapshot` retain committed content while a gesture is previewing. A live notification therefore does not mean there is a new document to save. Completed change events exclude canceled gestures, selection and camera movement. `DrawingHooks.onChange` observes the same completed events as document listeners. Use a hook for one configuration-owned callback, or independently removable listeners for separate consumers. ## Bring your own state management The existing callback boundary remains available to update a Riverpod provider, BLoC stream or MobX observable projection. Keep document edits as session commands; do not maintain a second mutable diagram in your state store. Subscribe when the consumer attaches and unsubscribe when it detaches. If a widget accepts a replaceable session, move subscriptions in `didUpdateWidget` as well. Observers may read state and update host UI, but cannot synchronously issue another diagram edit during notification. Schedule any follow-up command after delivery and re-read current state then. **Checkpoint:** Undo disables after exhausting history, and Redo updates after canvas edits as well as toolbar clicks. No manual refresh call is needed. --- --- prev: text: '1. Set up the canvas' link: /guide/flowchart-canvas next: text: '3. Create the toolbar' link: /guide/flowchart-toolbar --- # 2. Register and create shapes A registry describes **kinds of shapes**: their geometry, text regions, connection sites and supported edits. A document contains **instances** with stable IDs, positions and content. Register a type once; create as many instances as needed. Replace the kit import with the full public import for the rest of this tutorial: ```dart import 'package:vyuh_diagram_kit/vyuh_diagram_kit.dart'; ``` Replace the session field with an explicitly registered standard library: ```dart late final session = DrawingSession(shapeRegistry: ShapeRegistry.standard()); ``` This is also the default registry. It already registers rectangles, polygons and ellipses, so there is no need to redefine them. A four-sided polygon supplies our decision diamond. Add these fields and replace `initState`: ```dart late final String startId; late final String processId; late final String decisionId; @override void initState() { super.initState(); startId = session.createShape( type: Shapes.ellipse, worldCenter: const DiagramPoint(-240, 0), text: 'Start', ); processId = session.createShape( type: Shapes.rectangle, worldCenter: DiagramPoint.zero, text: 'Review request', ); decisionId = session.createShape( type: Shapes.polygon, polygonSides: 4, worldCenter: const DiagramPoint(260, 0), text: 'Approved?', ); } ``` Positions are world coordinates. Panning and zooming change how you view them, not their saved positions. Creation returns an ID after a successful edit. We retain those IDs to connect the nodes later. **Checkpoint:** move the three shapes and double-click their text. The registry supplies editing behavior without host drag handlers. [Step 7](/agent-docs/guide/flowchart-custom-shapes.md) adds our own registered type. --- --- prev: text: '2. Register and create shapes' link: /guide/flowchart-shapes next: text: '4. Connect reactive state to your UI' link: /guide/flowchart-reactivity --- # 3. Create the toolbar The host owns the toolbar layout; the session owns its edits. Add these methods inside `_DiagramSurfaceState`: ```dart void showDecision(DiagramPolicyDecision decision) { if (!decision.allowed && mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(decision.reason ?? 'This edit is unavailable.'), )); } } void addProcess() { try { session.createShape( type: Shapes.rectangle, worldCenter: const DiagramPoint(0, 180), text: 'New process', ); } on DiagramChangeDenied catch (error) { showDecision(error.decision); } } Widget buildToolbar() => Wrap( spacing: 8, children: [ TextButton(onPressed: addProcess, child: const Text('Add process')), TextButton(onPressed: () => showDecision(session.undo()), child: const Text('Undo')), TextButton(onPressed: () => showDecision(session.redo()), child: const Text('Redo')), ], ); @override Widget build(BuildContext context) => Column( children: [ buildToolbar(), Expanded(child: DrawingCanvas(session: session)), ], ); ``` Replace the old `build` method with this one. New processes start at the same teaching position; move one aside before adding another. ## Why does an edit return “allowed”? An edit is a request. A shape may be locked, a capability may forbid resizing, a target may have disappeared, or your host may reject changes. The SDK checks the request before publishing it. Toolbar clicks, canvas gestures and API calls pass through shared admission and history. Methods such as `undo()`, `moveElements()` and `invokeAction()` return a `DiagramPolicyDecision`. Read `allowed` to handle refusal in your UI, as above. Use `code` for program logic and `reason` for a human-readable explanation. ```dart void moveProcess() { session.moveElements([processId], const DiagramPoint(20, 0)) .requireAllowed(); } ``` `requireAllowed()` checks the **returned result** and throws `DiagramChangeDenied` if denied. It neither asks permission nor forces the edit through. Use it when refusal should propagate as an exception, such as setup code or a test; handle the decision for ordinary UI feedback. Creation helpers return an ID and throw on denial, which is why `addProcess` catches that exception. An allowed result can be a no-op; it does not promise that content changed. Invalid arguments and malformed data can throw separately. **Checkpoint:** add a process, undo it and redo it. Next we will make history buttons reflect availability. --- # Build a flowchart editor Learn Vyuh Diagram by building one Flutter editor, a step at a time. Start with an empty canvas and finish with a toolbar that follows editing state, a properties panel, connected custom nodes, saving/loading and keyboard actions. ## The ideas behind the SDK **One session owns editing.** `DrawingSession` holds the current document, selection and history. Your canvas, toolbar and property editors use that same session. Host UI sends commands and observes results. **Content is immutable; the session is reactive.** After an accepted edit, read a new document snapshot. Live editing notifications, completed document events and camera notifications serve different purposes. Step 4 shows how to subscribe from your own UI without depending on an internal state-management library. **Edits can be refused.** Capabilities, locks and host rules are checked before a change is published. `allowed` tells you the outcome; `requireAllowed()` throws when that outcome is a denial. Neither is an extra approval step. Step 3 introduces both with real toolbar callbacks. **Definitions supply behavior.** Register geometry, text, ports and capabilities once. Instances share the SDK's interaction, routing, undo and persistence machinery. ## Follow the tutorial Each page continues the same `lib/main.dart` from step 1. Code blocks tell you what to add or replace; checkpoints tell you what to try before moving on. | Step | What you build | | --- | --- | | [1. Set up the canvas](/agent-docs/guide/flowchart-canvas.md) | A session and bounded Flutter canvas. | | [2. Register and create shapes](/agent-docs/guide/flowchart-shapes.md) | Start, process and decision nodes. | | [3. Create the toolbar](/agent-docs/guide/flowchart-toolbar.md) | Creation and history buttons; meaningful denial handling. | | [4. Connect reactive state to your UI](/agent-docs/guide/flowchart-reactivity.md) | Subscriptions that keep your own controls current. | | [5. Show properties and connections](/agent-docs/guide/flowchart-properties.md) | An inspector, custom selection control and attached connectors. | | [6. Save and load the drawing](/agent-docs/guide/flowchart-persistence.md) | A JSON round trip and an autosave integration boundary. | | [7. Add a custom review node](/agent-docs/guide/flowchart-custom-shapes.md) | A review node with declared text, ports and capabilities. | | [8. Share actions and shortcuts](/agent-docs/guide/flowchart-actions.md) | One edit shared by a button and a keyboard binding. | | [9. Control the canvas and grid](/agent-docs/guide/flowchart-canvas-controls.md) | Grid, snapping and camera controls. | Package installation and registry access are covered in [step 1](/agent-docs/guide/flowchart-canvas.md#package-access). For commercial onboarding, see [SDK licensing](/agent-docs/guide/sdk-licensing.md). [Start with the canvas →](/agent-docs/guide/flowchart-canvas.md) --- # Package access You need access to the Vyuh package registry at `https://pub.vyuh.tech` with your designated access token to install `vyuh_diagram_kit` and its companion packages. Register your token before fetching dependencies: ```sh dart pub token add https://pub.vyuh.tech ``` Enter your designated token when prompted. Keep it private; do not commit it to your repository or include it in application source. Enterprise licenses are allocated manually after onboarding and payment through Paddle. Our team provisions your company and package access, then sends your designated registry token. A Paddle payment receipt is not a registry token. Once your token is installed, follow the dependency setup below and run `flutter pub get` to download the package. If access is denied, contact the onboarding team to check your allocation. **Don't have a license yet? [Contact us for a license](/#licensing).** ## What you need Use Flutter 3.47 or later with Dart 3.13 or later. Add `vyuh_diagram_kit` to your application: ```yaml dependencies: vyuh_diagram_kit: hosted: https://pub.vyuh.tech version: ^2.2.0 ``` Run `flutter pub get`. The kit brings in the Flutter renderer, headless engine, types, and codecs. Add a companion package directly only when your application imports it. For a headless Dart service, use `vyuh_diagram_engine` instead. It exposes `DrawingSession` as the headless application entry point. See [package ownership](/architecture/#package-ownership) for the complete package list. Continue with [Set up the canvas](/agent-docs/guide/flowchart-canvas.md) to build your first editor. See [SDK licensing](/agent-docs/guide/sdk-licensing.md) for commercial license scope, onboarding and billing management. --- # SDK licensing Vyuh Diagram is available for internal evaluation, development and testing. Production use requires a written commercial license. ## Commercial license benefits | Benefit | What you get | | --- | --- | | Diagram SDK | The complete Diagram package set, with the same features across team plans. | | Commercial use | One named application, including its web, desktop and mobile editions. | | End users | Unlimited end users and diagrams within the licensed application, with no runtime royalties. | | SDK updates | Updates during your active subscription. | | Technical support | Email assistance with installation, documented APIs and SDK bug investigation. | | Onboarding | One integration call to help your team get started. | | Custom integrations | Build your own shapes, widgets and integrations through the public APIs. | Custom development and dedicated support arrangements are quoted separately. Contact [licensing@vyuh.tech](mailto:licensing@vyuh.tech) to discuss a commercial license for your application. Your written agreement defines the developer allocation, license scope and support commitments. Changes to SDK source require written authorization. Separately licensed third-party components retain their own terms. --- # Compose shapes and libraries Build a compound as one registered shape. Its parts share movement, rotation, selection, text editing, history and persistence. Rows and columns arrange the parts inside its frame; they are portable geometry declarations, not Flutter widgets. ```dart import 'package:vyuh_diagram_kit/drawing.dart'; import 'package:vyuh_diagram_shapes_uml/vyuh_diagram_shapes_uml.dart'; final approval = ShapeDefinition( type: 'app.approval', displayName: 'Approval', defaultSize: const DiagramSize(240, 140), composition: ShapePadding(12, child: ShapeColumn([ ShapeSized(extent: 32, child: ShapeRow([ ShapeSized(extent: 32, child: const ShapeSurface( outline: ShapeOutlineDefinition.ellipse(), style: ShapeStyle(fill: DiagramColor(0xFFDBEAFE)), )), const ShapeText('title', text: 'Approval', singleLine: true), ], gap: 8)), const ShapeText('body', text: 'Describe the decision'), ], gap: 8)), ); final library = ShapeLibrary( id: 'app', name: 'My application', shapes: [approval], ); final session = DrawingSession(shapeRegistry: ShapeRegistry.fromLibraries([ ShapeLibraries.standard, library, ])); ``` ## Small layout vocabulary ### Geometry, layout and appearance **Geometry** describes frames, contours and paths. **Layout** computes where content fits within those bounds: rows, columns, stacks, sizing, gaps and padding. **Appearance** describes how that geometry is painted: fills, stroke colors, widths, dash styles and text formatting. An outline's path belongs to geometry because clipping, hit testing and connector attachment also use it. Its fill and stroke belong to appearance. Some appearance properties affect measured bounds: text size changes text metrics, and stroke width can affect visible bounds and connector clearance. Text uses named story slots and the shared paragraph system for formatting. Single-line constraints, wrapping and growth describe text behavior and layout, not just paint. Surfaces and paths carry their appropriate appearance properties. Connections additionally own endpoint bindings, routing and arrowheads; these behaviors remain separate from content layout inside a shape. Parts are nestable and reusable: a padded column may contain a title row, a stacked body and a footer. They compile into ordinary layers, using the existing geometry and text resolvers. A new composed shape does not need its own painter, hit-test implementation or text editor. Use `ShapePath` for normalized path artwork and `ShapeVector` for polyline or polygon artwork within a composition. Call the outer frame and outline **shape geometry**. Call meaningful interior areas **content regions**, such as title, body and footer. **Content layout** arranges those regions and their parts inside the shape. **Diagram layout** arranges whole shapes on the canvas; it is independent of this composition. Region names describe their purpose. They do not require separate title, body or footer classes. For example, a column can contain a fixed-height `ShapeText('title')`, flexible `ShapeText('body')` and fixed-height `ShapeText('footer')`, using `ShapeSized` for the fixed heights. | Part | Behavior | | --- | --- | | `ShapeRow(children, gap: ...)` | Left-to-right; children stretch vertically. | | `ShapeColumn(children, gap: ...)` | Top-to-bottom; children stretch horizontally. | | `ShapeSized(child: ..., extent: 32)` | Fixed size along the containing row or column. | | `ShapeSized(child: ..., flex: 2)` | Twice the remaining space of a default flexible child. | | `ShapePadding(12, child: ...)` | Equal inset on every side. | | `ShapeStack(children)` | Paint parts back-to-front in the same bounds. | | `ShapeSurface(outline: ..., style: ..., child: ...)` | Static rectangle, ellipse, polygon or line surface, with optional content. | | `ShapeText('slot', text: ...)` | Editable named text; horizontal padding defaults to 8, vertical padding to 0. | | `ShapePath(path, style: ...)` | Open or closed normalized Bezier artwork using canonical path geometry. | | `ShapeVector(paths)` | Portable normalized polyline/polygon artwork. | | `ShapeDivider(edge: ...)` | Straight separator attached to a slot edge. | | `ShapeImage()` | The owning shape's image resource. | Unwrapped children have flex 1. Fixed extents and gaps are allocated first; flex children share the remainder. Child minimums constrain resizing, and the default size must meet them. Composition does not infer intrinsic text size, wrap rows or scroll. The outer outline clips the parts; surfaces do not add clipping boundaries. There is no dedicated `ShapeGrid`; nest rows and columns for fixed grid-like arrangements. Live Flutter content currently uses a separately declared `ShapeWidgetLayerDefinition`, rather than a composition part. Inside that slot, Flutter controls use Flutter's own layout widgets. See [widgets inside shapes](/agent-docs/api/widgets.md#compose-a-shape-visually) for how composition, bounds and widget slots relate. A surface has its own declared style. The ordinary shape fill/border inspector edits the outer shape, not every internal surface. Vector artwork follows the owning shape's stroke. Image slots currently share the owner's one image resource. ## Reuse parts Keep any `ShapePart` in a variable, or return it from a function. Use `part.named('sender')` and `part.named('recipient')` when repeating a part with text slots or explicitly named surfaces. This prefixes those identities—for example, `sender.title`—so the two instances retain separate content. Edit a slot using `session.text.setPlainText(elementId: id, slotId: 'title', text: 'Approved').requireAllowed()`. Initial text is applied only when creating an instance. Resizing or rebuilding the registry never resets edited text. Give a `ShapeSurface` an `id` to target its bounds with a port's `targetLayerId`. The existing port resolver applies the compound's transform and keeps attached connectors in sync. Use the full kit import for port declarations and advanced layer/widget APIs. ## Choose libraries Specialist libraries are independent packages: `vyuh_diagram_shapes_uml`, `vyuh_diagram_shapes_flowchart`, `vyuh_diagram_shapes_workflow`, and `vyuh_diagram_shapes_network`. Add only those you need and import their matching Dart entry point. For example, import `package:vyuh_diagram_shapes_uml/vyuh_diagram_shapes_uml.dart` for `umlShapes`. These packages depend only on the engine and types. The workflow shape library does not bring in the separate workflow authoring/runtime package. The default library remains in the engine so the standard registry and existing hosts retain their defaults without a dependency cycle. | Library | Initial definitions | | --- | --- | | `ShapeLibraries.standard` | Existing built-in shapes, paths, images and drawing shapes. | | `umlShapes` | Class, interface, object, enumeration, data type, state, use case, component. | | `flowchartShapes` | Process, decision, terminal, subprocess, preparation, junction, extract. | | `workflowShapes` | Operation, wait, review, start, end, decision, parallel branches, nested workflow. | | `networkShapes` | Server, client, cluster. | These are starter libraries, not exhaustive notation catalogs or execution models. Standalone text and connector grammar remain registry-level productions. Existing `ShapeRegistry.standard()` and `withStandard(...)` remain supported and obtain their built-in definitions from the standard library. Use `ShapeRegistry.fromLibraries([...])` to choose your vocabulary. Select an individual definition with `umlShapes.shape('uml.class')`, or create a subset with `umlShapes.select(['uml.class'], id: 'my-uml', name: 'My UML')`. Libraries retain the original definitions; duplicate type IDs are rejected rather than silently overridden. Save and reopen documents with the same registry. Try [the library playground](/playground/?dataset=libraries). Its representative shapes are assembled using this public API. Compound parts belong to one element; use groups or child-owning shapes for independently selectable document elements. The playground's **Shape libraries** panel sits above Properties. Switch between UML, Flowchart, and Workflow, then click a preview to add a shape or drag it to a canvas position. Previews come from the registered definitions, and additions support Undo. Collapse the panel to leave more room for Properties. Flowchart tiles place their names inside the icons. New flowchart shapes start with centered text, declared using `ShapeText('label', alignment: ParagraphAlignment.center)`. This is an initial paragraph style; subsequent text alignment edits are preserved. ## Resize behavior is part of the grammar For `ParagraphAlignment`, `ShapeTextSizing`, and advanced text grammar options in this section, import `package:vyuh_diagram_kit/vyuh_diagram_kit.dart` instead of the smaller `drawing.dart` entry point. Text uses its declared rectangular region for layout and clipping. It is not masked by the outer contour by default, so slanted or curved edges do not cut through letters. Set `ShapeCapabilities(clipTextToOutline: true)` only when contour masking is intentional. This does not introduce contour-aware wrapping; use an inset text region when labels must remain entirely inside an outline. Rows and columns adapt to the container size. Use fixed `ShapeSized(extent: ...)` headers with a flexible body, or make every child flexible for proportional resizing. With only fixed children, extra space stays at the end. Text has `horizontalPadding` (8 by default), `verticalPadding` (0 by default), and `verticalAlignment` (`top`, `center`, `bottom`). Use top alignment in expandable bodies so existing text stays put while room grows below it. Padding and alignment resolve into the same text region used by paint, caret, selection and SVG export. UML definitions share a 40-unit title and one top-aligned multiline body. `ShapeTextSizing.growHeight` lets the body grow downward as content is added, while the title retains its height. Use `ShapeText('kind', text: 'Review', editable: false)` for a fixed type label. It still uses the shared text layout and export pipeline, but cannot activate an editor. Document admission preserves its declared paragraph, and text commands reject changes to that slot. Adjacent title and body slots remain editable. ## Build your own library Use [named actions and scoped keyboard bindings](/agent-docs/guide/actions.md) to share edits between buttons, interactive regions, and shape-specific text shortcuts. A library is a named collection of definitions. The registry is the combined catalog used to create, resolve, edit and reopen a document. Include a definition only once: duplicate type identifiers are rejected. ```dart final applicationShapes = ShapeLibrary( id: 'application', name: 'Application shapes', shapes: [ umlShapes.shape('uml.class'), ShapeDefinition( type: 'application.service', composition: ShapeColumn([ ShapeSized( extent: 40, child: const ShapeText('title', text: 'Service', singleLine: true), ), const ShapeText('members', text: '+ execute()'), ]), ), ], ); final registry = ShapeRegistry.fromLibraries([ ShapeLibraries.standard, applicationShapes, ]); ``` Import `umlShapes` from `package:vyuh_diagram_shapes_uml/vyuh_diagram_shapes_uml.dart`. Add more definitions to `applicationShapes.shapes` when constructing the library, or combine several libraries in the registry. Libraries and definitions are immutable after construction. Multiline slots accept additional paragraphs through the editor or `session.text.setPlainText(elementId: id, slotId: 'members', text: ...)`. The UML package also exposes `umlShape(type: ..., name: ..., title: ..., body: ...)` to create your own variants with the same title/body layout and connection behavior. Fixed children stay fixed; use a flexible body and `ShapeCapabilities(textSizing: ShapeTextSizing.growHeight)` for content that expands the container. While editing, overflow text and the caret remain visible beyond the text region and owning shape. Single-line titles stay on one baseline. This editing overlay uses the existing shaped paragraphs; leaving edit mode restores normal clipping. UML shapes declare `ShapeConnectionDefinition.outline()`: no visible ports are required. Connector creation and endpoint dragging use the shared outline binding and standard virtual snap points. Choose the connector tool to start a connection; dragging the shape in selection mode continues to move it. --- # Animation `DrawingSession` exposes two independent animation APIs: viewport transitions move the camera, while path effects decorate resolved connectors. Both are transient presentation state. Neither changes route geometry, enters undo history, or becomes part of the serialized document. All command methods below require a mounted `DrawingCanvas`; a detached or disposed session throws `StateError`. ## On this page | Category | What you can do | | --- | --- | | [Animate the view](#viewport-transitions) | Move smoothly to a point, element or selection. | | [Cancel a transition](#cancellation) | Stop or replace a viewport animation. | | [Animate connections](#path-effect-commands) | Start and stop effects along connection paths. | | [Effect options](#diagrampathanimation) | Set speed, size, count and appearance. | | [Custom effects](#custom-path-effects) | Register additional visual effects. | ## Viewport transitions ```dart Future animateToPoint( DiagramPoint point, { double? zoom, Duration duration = const Duration(milliseconds: 450), Curve curve = Curves.easeInOutCubic, }) Future animateToRectangle( DiagramRect bounds, { double padding = 48, Duration duration = const Duration(milliseconds: 450), Curve curve = Curves.easeInOutCubic, }) Future animateToElement( String elementId, { double padding = 48, Duration duration = const Duration(milliseconds: 450), Curve curve = Curves.easeInOutCubic, }) Future animateToRegion( String elementId, String regionId, { double padding = 48, Duration duration = const Duration(milliseconds: 450), Curve curve = Curves.easeInOutCubic, }) Future animateToSelection({ double padding = 48, Duration duration = const Duration(milliseconds: 450), Curve curve = Curves.easeInOutCubic, }) ``` | Target method | Required arguments | Target geometry | | --- | --- | --- | | `animateToPoint` | `point: DiagramPoint`. | World center; optional `zoom` preserves the current scale when omitted. | | `animateToRectangle` | `bounds: DiagramRect`. | Fit world bounds to the viewport. | | `animateToElement` | `elementId: String`. | Fit the element's resolved bounds. | | `animateToRegion` | `elementId: String`, `regionId: String`. | Fit the declared shape-layer region, transformed into world bounds. | | `animateToSelection` | None. | Fit the current resolved selection bounds. | | Optional argument | Type | Default | Behavior | | --- | --- | --- | --- | | `zoom` | `double?` | `null` | Point target only; clamped to camera zoom limits. | | `padding` | `double` | `48` | Bounds targets only; logical pixels on each viewport edge. | | `duration` | `Duration` | `450 ms` | Transition time. Zero or negative durations apply the target immediately. | | `curve` | Flutter `Curve` | `Curves.easeInOutCubic` | Easing for interpolating center and zoom. | The returned future resolves to `true` when the transition finishes, including an unchanged target or an immediate transition. It resolves to `false` when interrupted or when an element, region, or selection cannot be resolved. A missing target returns `false` without replacing an existing animation. Target geometry is captured when the command starts; this is not a continuous follow-element behavior. Zoom limits and finite-canvas policy still apply. When system animations are disabled, viewport transitions apply immediately. See [Navigation](/agent-docs/api/navigation.md) for fitting and region lookup semantics. ```dart final completed = await session.animateToElement( 'approval', padding: 64, duration: const Duration(milliseconds: 600), curve: Curves.easeOutCubic, ); if (!completed) { // The target was absent, or navigation interrupted the transition. } ``` ## Cancellation Animation targets must have finite, valid camera coordinates. An invalid target rejects the returned future with `ArgumentError` before replacing a running transition or changing the camera. Overshooting curves may overshoot position, but zoom stays within camera limits. A throwing or non-finite curve stops at the last valid state and completes with an error. It does not prevent later navigation or cancel a replacement transition. ```dart void stopViewportAnimation() ``` Stops at the current camera position and completes an active transition with `false`. It is safe to call when no transition is running. Starting another valid transition, immediate navigation, and user navigation interrupt the active transition. Unmounting or replacing the mounted navigation owner also cancels it. There is no document transaction to undo. Observe camera changes using [`addPresentationListener`](/agent-docs/api/navigation.md#presentation-listeners). The transition future describes completion; the listener supplies changing camera snapshots. ## Path-effect commands ```dart bool startPathAnimation(DiagramPathAnimation animation) bool stopPathAnimation(String animationId) void clearPathAnimations() Map get pathAnimations DiagramPathEffectRegistry get pathEffects ``` | Member | Arguments | Result and behavior | | --- | --- | --- | | `startPathAnimation` | `animation`: playback definition. | `true` when admitted; `false` for invalid values, an unregistered effect kind, or a missing resolved connector. Reuses an existing animation ID to replace its definition. | | `stopPathAnimation` | `animationId`: playback ID, not connector ID. | `true` if removed; `false` if no playback had that ID. | | `clearPathAnimations` | None. | Removes every active path effect. | | `pathAnimations` | None. | Read-only map keyed by animation ID; empty when detached. Reading after disposal throws. | | `pathEffects` | None. | Registry from `DrawingConfiguration.pathEffects`. | Multiple playback IDs may target the same connector. Removing a connector prunes its effects. Effects follow the connector's current resolved route, including rerouting after shape movement. Stopping playback restores the authored base-path paint. Use presentation listeners to observe explicit start, replacement, stop, and clear commands; they are not a per-frame effect clock. ```dart session.startPathAnimation(const DiagramPathAnimation( id: 'approval-flow', connectorId: 'review-to-approval', particle: DiagramDashParticle(), count: 6, size: 2, strokeWidth: 2, period: Duration(milliseconds: 1800), basePathOpacity: 0.15, )); final current = session.pathAnimations['approval-flow']; if (current != null) { session.startPathAnimation(current.copyWith(count: 10)); } session.stopPathAnimation('approval-flow'); ``` ## `DiagramPathAnimation` Its constructor uses named arguments. `id` and `connectorId` are required; every other property has a default. | Property | Type | Default | Contract | | --- | --- | --- | --- | | `id` | `String` | Required | Nonempty playback identity. | | `connectorId` | `String` | Required | Existing resolved connector ID. | | `kind` | `DiagramPathAnimationKind` | `.particles` | Registry key; standard kinds are `particles` and `pulse`. | | `scaleMode` | `DiagramPathAnimationScaleMode` | `.world` | `world` scales dimensions with zoom; `screen` retains logical-pixel dimensions. | | `period` | `Duration` | `1600 ms` | Positive loop duration. Smaller values move faster. | | `color` | `DiagramColor?` | `null` | Falls back to connector color. | | `endColor` | `DiagramColor?` | `null` | Optional second gradient color across path bounds. Character particles currently do not render this gradient. | | `particle` | `DiagramParticleVisual` | `DiagramCircleParticle()` | Visual used by the particles effect. | | `count` | `int` | `5` | Positive number of evenly distributed particles. | | `size` | `double` | `3` | Positive finite particle dimension, interpreted below. | | `strokeWidth` | `double` | `2` | Positive finite dash or pulse thickness. | | `opacity` | `double` | `0.9` | Effect opacity, from `0` to `1`. | | `phase` | `double` | `0` | Loop offset, from `0` to `1`. | | `reverse` | `bool` | `false` | Reverses progress along the route. | | `basePathOpacity` | `double` | `0.2` | Base-line opacity multiplier, from `0` to `1`; does not modify authored style. | `isValid` performs runtime validation, including in release builds. `hasValidPeriod` reports whether the period is positive. Constructor assertions are not the only admission check. `copyWith(...)` accepts `kind`, `period`, `color`, `endColor`, `clearEndColor`, `scaleMode`, `particle`, `count`, `size`, `strokeWidth`, `opacity`, `reverse`, and `basePathOpacity`. It retains `id`, `connectorId`, and `phase`; construct a new instance to change these. Passing `clearEndColor: true` removes the gradient. Passing `null` for `color` preserves the old color; there is no clear-color flag. ## Particle visuals | Constructor | Arguments | Size interpretation | | --- | --- | --- | | `DiagramCircleParticle()` | None. | Radius = `size`. | | `DiagramDashParticle()` | None. | Length = `size × 8`, limited by total route length; thickness = `strokeWidth`. | | `DiagramPolygonParticle({int sides = 4, double rotation = math.pi / 4})` | `sides`: 3–20; `rotation`: finite radians. | Circumradius = `size`. | | `DiagramCharacterParticle(String character)` | Exactly one nonblank grapheme cluster, including an emoji. | Rendered character font scale = `size × 2`. | Increasing count changes spacing, not particle dimensions. Dense particles can overlap. Dash width and length remain independent. Particles are distributed by resolved path length, not by a raw Bezier parameter. Pulse draws a periodically fading stroke and wider halo over the entire route. It uses `strokeWidth`; it does not use `particle`, `count`, or `size`. The inspector advertises effect-specific properties, although the playback model currently carries a shared set of fields. ## Custom path effects ```dart final effects = DiagramPathEffectRegistry.withStandard([ DiagramPathEffectDefinition( kind: const DiagramPathAnimationKind('my-effect'), label: 'My effect', properties: {DiagramPathEffectProperty.color}, paint: (canvas, context) { final paint = context.createPaint() ..style = PaintingStyle.stroke ..strokeWidth = 2 * context.sizeToWorld; canvas.drawPath(context.path, paint); }, ), ]); final configuration = DrawingConfiguration(pathEffects: effects); ``` Import Flutter painting types for custom callbacks. The example draws a stroke using the effect color and scaling mode. | API | Arguments / return | Contract | | --- | --- | --- | | `DiagramPathEffectRegistry(definitions)` | Iterable of definitions → registry. | Uses exactly the supplied effects. Rejects empty kinds/labels and duplicate kinds with `ArgumentError`. | | `DiagramPathEffectRegistry.withStandard(custom)` | Iterable of definitions → registry. | Adds custom effects to the standard set; duplicate standard kinds are rejected. | | `DiagramPathEffectRegistry.standard` | Registry. | Built-in particles and pulse. | | `definitions` | `Iterable`. | Registered definitions. | | `contains(kind)` | Kind → `bool`. | Checks registration. | | `require(kind)` | Kind → definition. | Throws `StateError` when missing. | ### `DiagramPathEffectDefinition` | Constructor argument | Type | Required / default | | --- | --- | --- | | `kind` | `DiagramPathAnimationKind` | Required registry identity. | | `label` | `String` | Required inspector label. | | `paint` | `void Function(Canvas, DiagramPathEffectContext)` | Required paint callback. | | `properties` | `Set` | Empty by default; retained as an immutable set. | | `editing` | `DiagramPathEffectEditing` | Definition-owned inspector ranges; defaults below. Validated when registered. Does not clamp authored playback values. | `DiagramPathEffectEditing` groups `DiagramPathEffectRange(minimum, maximum, step)` values: | Property | Default range | Units | | --- | --- | --- | | `speed` | `(0.1, 5, 0.1)` | Cycles per second | | `baseOpacity`, `opacity` | `(0, 100, 5)` | Percent | | `size`, `width` | `(1, 24, 1)` | Pixels | | `count` | `(1, 30, 1)` | Particles; integer limits and step | Ranges must be finite and ordered with positive steps. Speed, size, width and count require positive minima; opacity stays within 0–100. Only advertised properties produce controls. Advertised properties are `particle`, `color`, `gradient`, `speed`, `baseOpacity`, `opacity`, `size`, `width`, `count`, `direction`, and `scale`. They select inspector controls; the painter defines their actual effect. ### `DiagramPathEffectContext` | Member | Type | Purpose | | --- | --- | --- | | `connector` | `ResolvedConnector` | Current resolved connector. | | `pathGeometry` | `ResolvedPathGeometry` | Authoritative path sampling. | | `path` | Flutter `Path` | Copy of the resolved paint path; modifying it does not corrupt retained geometry. | | `metric` | Flutter `PathMetric` | Path length and segment extraction. | | `animation` | `DiagramPathAnimation` | Playback settings. | | `progress` | `double` | Loop progress after phase and direction. | | `inverseZoom` | `double` | Inverse current camera scale. | | `sizeToWorld` | `double` | `1` for world scaling; inverse zoom for screen scaling. | | `createPaint([double intensity = 1])` | Flutter `Paint` | Applies color, gradient and opacity. Rejects nonfinite intensity or values outside `0..1` with `ArgumentError`. | Custom effect paint runs in world space against current route geometry. Keep it paint-only; document changes belong in session commands. --- # DrawingCanvas Mount a session inside a bounded Flutter layout. The canvas owns no second document or configuration. For content inside an individual shape, see the [visual shape composition guide](/agent-docs/api/widgets.md#compose-a-shape-visually). ```dart Expanded(child: DrawingCanvas(session: session)) ``` Optional `DrawingInspector(session: session)` uses the same registry and configuration. Host applications compose toolbars and other surrounding UI. A session supports one mounted canvas at a time; unmount before `session.dispose()`. Unmounting cancels an unfinished canvas gesture and restores its content baseline without adding an undo entry. Late pointer events cannot change a subsequent edit. This applies to registered custom tools as well as the default tools. The canvas clips scene paint and its local overlays to the allocated viewport. Shapes, text, connector markers and selection chrome cannot paint over adjacent host widgets. This screen-space clip does not alter document geometry or export bounds; frame/shape content clipping remains a separate world-space rule. Use `DrawingConfiguration.canvasPolicy` to choose `InfiniteCanvasPolicy()` or `FiniteCanvasPolicy(worldBounds)`. Camera navigation never rewrites the authored shape coordinates. ## On this page | Category | What you can do | | --- | --- | | [Canvas limits](#canvas-policies) | Choose finite or infinite navigation. | | [Embedded controls](#arbitrary-flutter-widgets-inside-a-node) | Mount interactive content inside elements. | | [Accessibility](#accessibility) | Expose content and actions to assistive tools. | | [Touch navigation](#touch-navigation) | Configure touch input behavior. | ## View-only diagrams Use `DrawingCanvas(session: session, viewOnly: true)` in Kit for a lightweight selectable view with pan, scroll and zoom. It mounts shared scene painting without editor gesture controllers, text editing, IME or editing extensions. Attribution follows the canvas configuration. The session remains the document authority: host code can still update content. View-only mode restricts user interaction, not programmatic access. Embedded widgets use their painted fallback. See the [view-only example](/agent-docs/examples/view-only.md). ## Canvas policies | API | Contract | | --- | --- | | `InfiniteCanvasPolicy()` | Allows unrestricted camera positioning within the configured zoom range. | | `FiniteCanvasPolicy(DiagramRect worldBounds, {double viewportPadding = 0})` | Constrains navigation to finite, positive-area world bounds. Padding is a finite, nonnegative number of screen pixels. | | `CanvasPolicy.validate()` | Throws `ArgumentError` for invalid runtime values. Drawing configuration and camera controllers call it before installing a policy. Const construction alone does not validate release-mode input. | Replacing a policy re-clamps the existing camera; the mounted editor applies that publication after its build. Rejected policies preserve the previous camera and policy. Policy changes do not rewrite document coordinates or enter undo history. Try the [finite/infinite example](/agent-docs/examples/bounds.md). ## Arbitrary Flutter widgets inside a node Use declared widget slots for sliders, dropdowns, buttons and text fields inside shapes. The slot is part of the shape definition; Flutter builders are registered once in the drawing configuration. | Object | Responsibility | | --- | --- | | `ShapeWidgetLayerDefinition` | Slot bounds, builder key and portable fallback layers. | | `ShapeWidgetRegistry` | Maps builder keys to Flutter controls. | | `ShapeElement.values` | Canonical, validated control values. | | `ShapeWidgetContext` | Reads values and dispatches updates through admission and history. | The canvas applies owner transforms, clipping, visibility and paint ordering to native slots. Controls own interactive gestures; non-interactive slot space participates in shape selection and movement. Host overlays remain useful for canvas chrome, but are not the embedding mechanism for node content. See [Widgets inside shapes](/agent-docs/api/widgets.md) for declarations, builder lifetime, value-edit gestures, focus ownership and export fallbacks. SVG uses the declared fallback rather than serializing an arbitrary Flutter subtree. Dense native-widget scenes still require performance work. Release observations with 10,000 shapes and hundreds of widgets do not yet meet the frame budget at dense overview zoom; this is not an automatic 60/120 FPS guarantee. ## Accessibility When Flutter semantics are enabled, visible canvas objects expose their type and available text and lock state. Selectable objects also expose selection state and an activation action through the same session command path as pointer selection. Setting `selectable: false` keeps the content readable without a selection action or selection hint; it does not hide the content from assistive technology. Semantic bounds follow the camera and intersect the resolved ancestor clip paths. Offscreen and fully clipped objects and connector labels are omitted. Removing a clip makes the content available again, including through undo and redo. Partially clipped content uses the bounding rectangle of the remaining region, since semantic bounds are rectangular. Set `ShapeDefinition.displayName` to give custom shapes a readable kind name in both the inspector and screen-reader labels. The host can localize that name when constructing its registry. The canonical type ID remains unchanged. While editing text, the shared paragraph component exposes the active block as a text field, including its current value and selection. Accessibility text and selection changes use the same canonical editing pipeline as the IME and remain subject to change admission and undo/redo. The mounted text input follows the canonical story selection. Switching to a different story switches the input target; an object selection exits text mode. Undo that restores an editable story selection restores text input, and remounting the canvas retains that selection. If the target becomes non-editable, selection falls back to the object when selectable, or clears otherwise. This is initial support, not a complete screen-reader editing contract. Traversal between offscreen objects and paragraph/list blocks, custom localized object descriptions, richer editing actions, and assistive-technology compatibility remain under development. ## Touch navigation Two fingers pan and zoom the viewport. The second contact cancels any active object preview, exits text editing, and transfers ownership to camera navigation. A combined sample publishes one camera state, respects zoom limits and finite-canvas bounds, and does not add an undo entry. After one finger lifts, the remaining contact cannot become an object drag; release every contact before starting another single-finger interaction. A third contact does not change the active pair until one of that pair ends. Contact cancellation releases ownership just like lifting a finger. --- # Sessions, agents and collaboration ## Sharing a drawing `DrawingCanvas` takes one `DrawingSession`. A session can be mounted in one canvas at a time; commands can also run without mounting that canvas. | Scenario | Behavior | | --- | --- | | User and in-process automation call the same session | Both use the same commands, admission and history. They share selection and the active interaction. Coordinate their work. | | Independent user/agent selections and simultaneous previews | Not provided by borrowed sessions. | | Remote collaborators | No built-in transport, participant presence, conflict reconciliation or participant-scoped undo contract. | | Server-side agent | Use `DrawingSession` from `vyuh_diagram_engine` with an appropriate text backend. Content commands match the Flutter session. | A second facade does not create independent participant state. Coordinate commands through the session; independent collaborative selections and conflict resolution require an explicit collaboration integration. ## Attaching a Flutter canvas Use `DrawingSession.fromSession(source)` from `vyuh_diagram_kit` to attach a Flutter canvas to an existing headless session. Configure a Flutter text layout backend on the source before mounting it. Supported font choices and the default font belong to the shared drawing settings. Attachment retains them. Updating either session's configuration updates the same font choices for every attached facade, and listeners see the updated list. Font configuration changes do not edit document content or add undo entries. Passing a canvas configuration explicitly updates those shared font settings as well as that canvas's presentation options. Dispose the canvas adapter before the source. Disposing the adapter leaves the source document and history available. --- # DrawingConfiguration `DrawingConfiguration` is the immutable behavior, presentation, and host integration configuration for a `DrawingSession`. Defaults are enough for a working canvas. Change only the options you need: ```dart session.setConfiguration(session.configuration.copyWith( grid: session.configuration.grid.copyWith(visible: true, snap: true), )); ``` | What you want to change | Where it belongs | | --- | --- | | Grid, fonts, navigation, extensions, callbacks | `DrawingConfiguration` | | What a shape type supports, including its rotation handle | `ShapeDefinition.capabilities` | | A shape's content, appearance, position, or connections | Session editing commands | Configuration updates preserve document content and undo history. For the first working app, start with [Getting started](/agent-docs/guide/getting-started.md). ## On this page | Category | What you can do | | --- | --- | | [Create configuration](#constructor) | Choose the starting behavior for the drawing. | | [Configuration fields](#properties) | Find appearance, content and integration options by category. | | [Change configuration](#copywith) | Copy immutable settings and apply them to the session. | | [Canvas limits](#canvas-policies) | Choose an infinite canvas or bounded workspace. | | [Visual detail](#diagramlodpolicy) | Configure optional detail reduction. | | [Fonts](#supported-fonts) | Share font choices and defaults across text elements. | | [Images](#drawingimageconfiguration) | Configure image loading and retention. | ## Constructor ```dart DrawingConfiguration({ List extensions = const [], List actions = const [], List keyBindings = const [], List supportedFonts = const [], String? defaultFontFamily, DiagramPathEffectRegistry? pathEffects, ShapeWidgetRegistry widgetRegistry = const ShapeWidgetRegistry.empty(), CanvasPolicy canvasPolicy = const InfiniteCanvasPolicy(), NavigationConfiguration navigation = const NavigationConfiguration(), DrawingGridConfiguration? grid, DrawingImageConfiguration? images, double rotationSnapDegrees = 15, double connectionSnapRadius = 10, DiagramLodPolicy lodPolicy = const DiagramLodPolicy(), Color backgroundColor = const Color(0xFFFAFAFA), DrawingHooks hooks = const DrawingHooks(), DiagramImageSourcePicker imageSourcePicker = pickDiagramImageSource, }) ``` ## Properties All stored properties are final. Nullable constructor inputs for `grid`, `images`, and `pathEffects` become non-null properties. ### Canvas appearance and navigation Choose the background and available drawing area. | Property | Type | Default | Contract | | --- | --- | --- | --- | | `canvasPolicy` | `CanvasPolicy` | `const InfiniteCanvasPolicy()` | Selects infinite or finite world navigation. | | `navigation` | `NavigationConfiguration` | `const NavigationConfiguration()` | [Edge auto-pan padding, speed and keyboard movement](/agent-docs/api/navigation.md#pointer-and-keyboard-navigation). | | `backgroundColor` | `Color` | `Color(0xFFFAFAFA)` | Flutter canvas background color. | | `finiteCanvas` | `bool` getter | Derived | `true` when `canvasPolicy is FiniteCanvasPolicy`. | ### Grids and snapping Control visual guides and movement increments. | Property | Type | Default | Contract | | --- | --- | --- | --- | | `grid` | `DrawingGridConfiguration` | `DrawingGridConfiguration()` | Visibility, snapping, spacing, and pattern; see [grids](/agent-docs/api/grids.md). | | `connectionSnapRadius` | `double` | `10` | Proximity in screen pixels for explicit ports and virtual connection points. Finite and nonnegative; `0` disables magnets. Consistent across zoom levels. | | `rotationSnapDegrees` | `double` | `15` | Finite angle increment, greater than `0` and at most `360`. | ### Content and rendering Configure image resources, embedded controls and visual detail. | Property | Type | Default | Contract | | --- | --- | --- | --- | | `widgetRegistry` | `ShapeWidgetRegistry` | `const ShapeWidgetRegistry.empty()` | Flutter builders for declared shape slots; see [widgets inside shapes](/agent-docs/api/widgets.md). | | `images` | `DrawingImageConfiguration` | `DrawingImageConfiguration.standard` | Image load concurrency, active-load deadlines and offscreen decoded retention; see below. | | `lodPolicy` | `DiagramLodPolicy` | `const DiagramLodPolicy()` | Full visual detail by default; adaptive detail is opt-in. | ### Extensions and application callbacks Add UI extensions, effects and host integration. | Property | Type | Default | Contract | | --- | --- | --- | --- | | `extensions` | `List` | Empty list | Copied into an unmodifiable list; IDs must be nonempty and unique. | | `pathEffects` | `DiagramPathEffectRegistry` | `DiagramPathEffectRegistry.standard` | Registry used for transient connector path effects. | | `hooks` | `DrawingHooks` | `const DrawingHooks()` | Admission, completed-change, region, and connection callbacks; see [hooks](/agent-docs/api/hooks.md). | | `imageSourcePicker` | `DiagramImageSourcePicker` | `pickDiagramImageSource` | Image picker callback supplied at construction. | `DiagramImageSourcePicker` is `Future Function()`; returning `null` cancels image selection. ## `copyWith` ```dart DrawingConfiguration copyWith({ List? extensions, List? actions, List? keyBindings, List? supportedFonts, Object? defaultFontFamily, // Omission preserves; explicit null clears. DiagramPathEffectRegistry? pathEffects, ShapeWidgetRegistry? widgetRegistry, CanvasPolicy? canvasPolicy, NavigationConfiguration? navigation, DrawingGridConfiguration? grid, DrawingImageConfiguration? images, double? rotationSnapDegrees, double? connectionSnapRadius, DiagramLodPolicy? lodPolicy, Color? backgroundColor, DrawingHooks? hooks, DiagramImageSourcePicker? imageSourcePicker, }) ``` Returns a new configuration. Omitted arguments preserve their current values. Explicit `null` also preserves other fields, but `defaultFontFamily: null` clears the inherited family. That parameter accepts a `String` or `null`; internally, an omitted-value sentinel distinguishes omission from clearing. Constructor validation runs again. Supply `imageSourcePicker` to replace the picker through the same configuration update path; omit it to preserve the current callback. | Failure | Result | | --- | --- | | Non-finite rotation increment, `<= 0`, or `> 360` | `ArgumentError` | | Empty/whitespace-only extension ID or duplicate ID | `ArgumentError` | | Negative or non-finite connection snap radius | `ArgumentError` | | Invalid canvas policy, navigation settings, font choices, or blank default font family | `ArgumentError` | | Invalid action IDs or keyboard bindings | `ArgumentError` | ## Canvas policies ```dart const InfiniteCanvasPolicy() const FiniteCanvasPolicy(DiagramRect worldBounds, {double viewportPadding = 0}) ``` | Class / property | Type | Default | Meaning | | --- | --- | --- | --- | | `InfiniteCanvasPolicy` | `CanvasPolicy` | Outer configuration default | No finite world boundary. | | `FiniteCanvasPolicy.worldBounds` | `DiagramRect` | Required positional argument | Finite world boundary. | | `FiniteCanvasPolicy.viewportPadding` | `double` | `0` | Screen-space breathing room around the boundary; must be finite and nonnegative. Configuration validates the complete policy in release mode too. | ## `DiagramLodPolicy` ```dart const DiagramLodPolicy({ bool adaptive = false, double compactBelowPixels = 32, double thumbnailBelowPixels = 10, }) DiagramVisualDetail detailFor({ required double projectedMinimumDimension, required bool interactionCritical, }) ``` | Property | Type | Default | Meaning | | --- | --- | --- | --- | | `adaptive` | `bool` | `false` | Enables projected-size-based visual detail. | | `compactBelowPixels` | `double` | `32` | Compact threshold, in projected pixels. | | `thumbnailBelowPixels` | `double` | `10` | Thumbnail threshold, in projected pixels. | `detailFor` returns `DiagramVisualDetail.full` when adaptation is disabled or the element is interaction-critical. Otherwise it returns `thumbnail` below the thumbnail threshold, `compact` below the compact threshold, and `full` at or above both thresholds. Threshold comparisons are strict `<`. ## Applying configuration ```dart final session = DrawingSession( document: DiagramDocument.empty('my-drawing'), shapeRegistry: ShapeRegistry.standard(), tools: DiagramToolRegistry.standard(), configuration: DrawingConfiguration( grid: DrawingGridConfiguration(visible: true, snap: true, size: 20), extensions: [const DiagramMinimapExtension()], ), ); session.setConfiguration(session.configuration.copyWith( grid: session.configuration.grid.copyWith(size: 24), )); // Mount inside a bounded Flutter layout. DrawingCanvas(session: session); ``` `setConfiguration` preserves document history. The session projects toolbar and inspector setting changes back into `session.configuration`, so hosts can read the current settings from that property. Shape and tool registries are constructor inputs to the session, not configuration fields; there is no live registry replacement method. A Flutter adapter created with `DrawingSession.fromSession` retains its source session's admission hook; see [session lifetime and errors](/agent-docs/api/session.md#lifetime-and-errors). Settings that require text resolution are prepared before replacing the admission hook. If preparation fails, the previous settings, scene and hook remain active. On success, listeners observe the new settings and hook together. Pure Dart hosts also use `session.setConfiguration` for supported fonts and the default font; Flutter presentation settings belong to the Flutter configuration. ## Supported fonts ::: tip Configuration callbacks Queue a later update if a listener needs to change configuration. Calling `setConfiguration` while another configuration update is being published throws `StateError`; the active update remains intact for every listener. ::: `DrawingConfiguration.supportedFonts` supplies the font preview chips used by `DrawingInspector` for every text host: standalone text, shape stories, card regions, and connector labels. ```dart DrawingConfiguration( defaultFontFamily: 'Shantell Sans', supportedFonts: const [ DiagramFontFamily(family: 'IBM Plex Sans', label: 'Sans serif'), DiagramFontFamily(family: 'IBM Plex Serif', label: 'Serif'), DiagramFontFamily(family: 'IBM Plex Mono', label: 'Monospace'), DiagramFontFamily(family: 'Shantell Sans', label: 'Handwritten'), ], ) ``` | Member | Type | Meaning | | --- | --- | --- | | `defaultFontFamily` | `String?` | Shared inherited family for every text host; null uses the backend default. | | `supportedFonts` | `List` | Immutable font choices; defaults to an empty list. | | `DiagramFontFamily.family` | `String` | Exact registered Flutter family name. | | `DiagramFontFamily.label` | `String` | Font name shown in the tooltip and accessibility label. | The host bundles or loads the font files. A descriptor does not download a font. The playground bundles the four families above, including their licenses, and supplies font faces to SVG export. Other hosts supply their own `SvgFont` resources for portable SVG output. The Font inspector previews configured families and updates `TextRun.fontFamily` through undoable edits. It targets the selected story or connector label without changing paragraph structure. Explicit run families override `defaultFontFamily`. For headless export, pass that default to `DiagramResolver.resolve`. Changing the default reflows inherited text without changing the document or history; null restores the backend default. Imported font families remain intact but do not expand the catalog. After loading fonts, invalidate the Flutter text engine’s font cache and refresh layout. ## DrawingImageConfiguration `DrawingConfiguration.images` applies to every canvas image. It is immutable and validated before it reaches a mounted view. ```dart DrawingImageConfiguration({ int maximumConcurrentLoads = 4, Duration loadTimeout = const Duration(seconds: 30), int retentionBytes = 256 * 1024 * 1024, }) ``` | Property | Contract | | --- | --- | | `maximumConcurrentLoads` | Positive active-load limit. Increasing it admits queued work; lowering it lets existing work finish before starting more. | | `loadTimeout` | Positive deadline from admission to the first decoded image; queue time is excluded. Active loads keep their original deadlines after a configuration update. | | `retentionBytes` | Positive soft target for decoded RGBA bytes retained by the drawing view. Offscreen resources and dependent pictures are reclaimed above it. Visible images stay pinned even when they exceed the target. Excludes Flutter global caches, pending decoder/raster allocations and independent image handles. | `copyWith({int? maximumConcurrentLoads, Duration? loadTimeout, int? retentionBytes})` returns a validated configuration; omitted fields retain their values. Invalid limits throw `ArgumentError`. ```dart session.setConfiguration(session.configuration.copyWith( images: session.configuration.images.copyWith( maximumConcurrentLoads: 2, retentionBytes: 128 * 1024 * 1024, ), )); ``` Updates preserve the document, undo history, decoded images and active loads. PNG/SVG export resources use their explicit [output options](/agent-docs/api/output.md) instead of the interactive viewport retention policy. **Shift** snaps line/path creation and dragged endpoints to `rotationSnapDegrees`, independently of grid snapping. Angles use the opposite endpoint or adjacent polyline vertex and preserve pointer radius. Valid port or shape bindings take precedence. Curved connectors keep their router; only endpoint direction is constrained. ## Named actions and keyboard bindings `actions` is a list of `DrawingAction` declarations; `keyBindings` is a list of `DrawingKeyBinding` declarations. Both default to empty and are preserved by `copyWith`. Changing configuration does not replace document content or history. Action IDs must be nonempty and unique. Bindings must reference registered IDs; duplicate scoped chords and Escape bindings are rejected. `slotId` requires text scope. See [actions and keyboard bindings](/agent-docs/guide/actions.md) for complete fields, precedence and input ownership. Callbacks are host configuration, not saved data. --- # Connectors and labels ## On this page | Category | What you can do | | --- | --- | | [Routing](#change-router) | Choose the path between connected elements. | | [Appearance](#change-connector-style) | Set stroke color, width, pattern and arrowheads. | | [Connect elements](#connect-and-label-shapes) | Create connections and add labels. | | [Label capabilities](#connectorlabelingcapability) | Declare allowed label behavior. | | [Label appearance](#style-one-label) | Set the appearance of an individual label. | | [Creation defaults](#creation-defaults) | Configure initial connection appearance. | ## Change connector style ```dart session.setConnectorStyle( connectorId, color: const DiagramColor(0xFF3355FF), strokeWidth: 3, strokeStyle: StrokeStyle.dashed, ).requireAllowed(); ``` The same method works in Flutter and pure Dart. Only supplied fields change. | Argument | Type | Meaning | | --- | --- | --- | | `connectorId` | `String` | Existing connector ID. | | `color` | `DiagramColor?` | Stroke color. | | `strokeWidth` | `double?` | Stroke width in world units. | | `strokeStyle` | `StrokeStyle?` | Solid, dashed or dotted stroke. | | `startArrowhead`, `endArrowhead` | `Arrowhead?` | Replace either terminal marker. | Omitted fields retain their current values. Endpoints, routing and labels are preserved. The method returns an allow/deny decision; a successful change creates one undo step. Missing or locked connectors and host admission can deny the edit. Invalid values fail canonical validation. An unchanged style publishes nothing. The inspector uses this same command. ## Route spacing and corners Use `session.rerouteConnections()` to explicitly find clear paths for elbow connections. Pass `connectorIds` to reroute a subset. The command replaces manual elbow bends and saves all resulting paths in one undoable document edit. If any requested path cannot be found, nothing changes; show the returned decision's `reason` in your application. Moving unrelated equipment does not trigger an obstacle search or change a saved path. Moving an endpoint keeps the connector attached using normal routing; choose **Reroute connections** again to avoid obstacles at its new position. Saved paths are used while their original terminals match. Straight and curved routes are excluded from the default command. Routing uses conservative world bounds, a local search area and a bounded search. It does not bundle links, avoid every crossing, or guarantee a globally shortest route. Group interiors are not obstacles. Owned connections respect every active ancestor clip; disabled clipping imposes no boundary. The planner conservatively checks stroke and corner clearance against resolved outlines, including rotated ancestors. Locked connections, blocked terminals, or no proven route inside the clips reject the whole command. Try [the crowded network](/agent-docs/examples/dense-network.md). ```dart session.setConnectorRouting( connectorId, portSpacing: 8, portExtension: 24, cornerRadius: 12, ).requireAllowed(); ``` `portSpacing` applies to every route. `portExtension` and `cornerRadius` apply only to elbow routes; supplying either for another route denies the entire edit. All values are world-space distances. Omitted values retain their current values. The command preserves endpoints, labels, route kind and appearance, and creates one undo step. Missing or locked connectors are denied. An unchanged update is a no-op. The inspector calls this same command. ## Change router `session.setConnectorRouter(connectorId, router)` returns a `DiagramPolicyDecision`. | Argument | Type | Meaning | | --- | --- | --- | | `connectorId` | `String` | ID of an existing, unlocked connector. | | `router` | `ConnectorRoute` | New route kind, such as `straight`, `orthogonal`, `quadratic`, or `bezier`. | It changes the route kind in one undo unit, retaining endpoints, labels, and appearance. Switching kinds clears control points; undo restores them. Selecting the existing kind is a no-op. Unknown or locked connectors are denied without changing the publication. The inspector uses the same command. ```dart session.setConnectorRouter('connection', ConnectorRoute.bezier).requireAllowed(); ``` ## Connect and label shapes Use `session.connect(sourceId, targetId)` to connect existing elements. The session assigns a fresh connector ID and returns it. Shapes and text boxes use the same connection rules; optional ports select specific anchors. ```dart final connectionId = session.connect( sourceId, targetId, sourcePortId: 'output', targetPortId: 'input', router: ConnectorRoute.bezier, ); session.reconnect( connectionId, atStart: false, endpoint: BoundEndpoint(elementId: anotherTargetId, portId: 'input'), ); final labelId = session.addConnectorLabel( connectionId, text: 'Next step', fraction: 0.5, ); ``` `sourcePortId` and `targetPortId` default to `null`; `router` defaults to `ConnectorRoute.straight`, and `select` defaults to `true`. An accepted connection is one undoable edit. Invalid ports and denied connections are rejected without inserting a connector. Use `createConnector(ConnectorElement(...))` for authored IDs, free endpoints, custom anchors, or initial appearance fields. The example assumes those IDs and ports exist. Omit `portId` for a shape anchor or use `FreeEndpoint` for an unbound endpoint. Connection commands apply the session's connection policy; reconnection retains the other connection properties. Routing supports straight, elbow (`orthogonal`), quadratic and cubic Bezier paths. Dragging an endpoint onto a portless, whole-shape-connectable object creates a point anchor at the drop position. `BoundEndpoint.anchorMode` is `ConnectorAnchorMode.point`, with `normalizedAnchor` measured relative to the owner's frame. The anchor follows movement, resizing and rotation, and survives undo, copying and save/reopen. The visible stroke and arrowhead are clipped to the contour; selecting the connector reveals its actual interior endpoint. Port-only shapes continue to use declared ports. `connectionSnapRadius` is a screen-pixel radius shared by ports and virtual targets; zero disables magnets. Portless shapes offer a center, four side midpoints and four projected corners. Away from magnets, drops preserve the chosen anchor. Port-only shapes require a nearby declared port. Moving the other endpoint preserves the source port. For the same behavior through the API: ```dart session.reconnect( connectionId, atStart: false, endpoint: BoundEndpoint( elementId: targetId, normalizedAnchor: const DiagramPoint(0.23, 0.71), anchorMode: ConnectorAnchorMode.point, ), ); ``` Coordinates range from 0 to 1 in each axis: `(0, 0)` is the frame's top-left and `(1, 1)` is its bottom-right. Point mode cannot be combined with `portId`. Whole-shape bindings default to `ConnectorAnchorMode.outline` and a center anchor. Straight and curved routes aim at the center and clip at the outline; elbows choose a facing axis. Ports and explicit side anchors keep their directions. Handles show the attachment; visible strokes end at the outline. Port bindings use the same owner-contour fitting as whole-shape anchors. A port is a routing anchor, not permission to draw through its owner. Straight, quadratic and Bézier paths stop at the first occupied owner contour, with the complete arrowhead and stroke kept outside disjoint endpoint owners. Elbows add orthogonal route negotiation before that shared fitting step. Overlapping owners retain the existing overlap exception. Labels are attachments owned by the connector. Their fraction ranges from 0 to 1 along resolved path length; they also have offset, size and a shared text story. The label service supports update, selection and removal. The registry's connector labeling capability controls availability and maximum count. ## Arrange labels ```dart session.arrangeLabels().requireAllowed(); session.arrangeLabels(connectorIds: ['connection-id'], spacing: 12).requireAllowed(); ``` `requireAllowed()` throws if the editor rejects the edit, for example because a connector is locked. It does not request permission from a user. To show a message instead, handle the returned result: ```dart final result = session.arrangeLabels(); if (!result.allowed) { // Display result.reason in your application's feedback area. } ``` Find nearby positions using measured label geometry, avoiding leaf shapes and other labels. Omit `connectorIds` to arrange all labels; labels outside a supplied subset stay fixed and remain obstacles. `spacing` is the minimum gap in world units (default 8; finite and nonnegative). Connectors owned by expanded frames, lanes, or groups can arrange their labels. Candidates must fit inside every active ancestor clip, including spacing. Clipping disabled on a container imposes no boundary. Rotated and custom outlines use conservative containment checks; if no candidate can be proven to fit, the whole arrangement is rejected without changing content or history. Bulk label arrangement and rerouting skip connections hidden by collapsed frames. Explicitly targeting a hidden connection asks you to expand its frame first. Visible external connections use the same collapsed-frame endpoint in previews, rerouting and the canvas; their saved child/port bindings stay intact. Valid placements near their path anchors are retained. Detached labels are returned to the path or placed immediately beside it. The bounded search tries positions along the connector instead of moving labels progressively farther away. All changes share one undo entry and are stored as ordinary label fractions and offsets, so save/load, hit testing and output consume the same positions. A repeated call with nothing to move adds no history. The bounded search can fail even when a distant placement exists. On failure nothing changes; the decision explains that more room is needed. Label movement must be enabled, and requested connectors must exist, be unlocked and have no group owner. Finish active edits first. This command does not avoid all connector crossings or solve global label optimization. See the [crowded-label example](/agent-docs/examples/connector-labels.md). ## `ConnectorLabelingCapability` The default connector label is plain text, centered on its path anchor. It grows to fit its longest line and expands vertically for additional paragraphs. Lists are disabled by its paragraph grammar, and it has no resize handles by default. | Property | Default | Contract | | --- | --- | --- | | `maximumLabels` | `2` | One or two attachments on a connector. | | `endInset` | `0.15` | Default distance from each end, as a fraction of the route. | | `defaultFontFamily` | `sans-serif` | Label font, independent of node typography. | | `defaultFontSize` | `14` | Inherited label text size in logical pixels; explicit run sizes take precedence. | | `singleLine` | `false` | Multiline plain paragraphs with inline formatting, without lists. Set to `true` for Enter to exit editing. | | `paragraphGrammar` | `ParagraphGrammar(bullets: false, numberedLists: false, checklists: false)` | Permitted label block types. Combined with the registry-wide grammar for admission, shortcuts and inspector controls. | | `resizable` | `false` | No resize handles or resize commands by default. | | `movable` | `true` | Dragging projects the label onto the visible path, between fractions 0 and 1. | | `textEditable` | `true` | Double-click creates or edits a label. | | `deletable` | `true` | A selected label can be removed independently. | | `defaultSize` | `DiagramSize(120, 32)` | Authored box; plain labels fit their longest line, reserving one character when empty. Height grows for additional lines. | | `minimumSize` | `DiagramSize(24, 16)` | Admission limit for authored label geometry. | Select a connector and press **Enter**, or double-click its path, to create a label when capacity remains. With two-label capacity, defaults place labels near opposite ends: at route fractions `endInset` and `1 - endInset` (`0.15` and `0.85`). With one-label capacity, the default is the midpoint. Explicit API `fraction` values remain supported. At capacity, path activation does nothing; it never switches to an arbitrary existing label. Select a specific label and press **Enter**, or double-click that label, to edit its text. Label selection retains its identity even when another label exists. `maximumLabels` accepts 1 or 2. `endInset` must be finite and in `[0, 0.5)`. `defaultFontFamily` defaults to `sans-serif`, independently of the node font; `defaultFontSize` remains 14. Explicit inline font choices override these defaults. These interactions are built into the connector labeling capability. Locked connectors, disabled labeling, or `textEditable: false` prevent entry. Merely entering an existing label creates no document/history change. Enter adds a paragraph; Escape exits editing. The padded editing outline stays visible while typing. Clearing text removes visible ink and the idle hit box; reactivation can reopen the existing empty attachment. Labels accept plain paragraphs, not lists. Use `session.addConnectorLabel` explicitly for additional labels, up to the configured limit. Hosts can opt into single-line or resizable labels through a custom capability. The default inherited font size is **14 px**, shared by measurement, painting, caret geometry, the inspector and size shortcuts. Explicit `TextRun.fontSize` values are preserved. Set `ConnectorLabelingCapability(defaultFontSize: ...)` to customize the inherited size for a registry. ## Style one label Select a label to show **Connector label** in the inspector. Its font size, text color and inline marks affect that attachment only. The connector stroke and other labels keep their own styles. Plain labels expose text properties; choosing **Badge** also exposes fill, border color/style/width, radius and padding. Single-line labels do not show list or paragraph-alignment controls. Text styling uses the shared `TextRun` fields (`fontSize`, `color`, `fontFamily`, `marks`), not a separate label-only text model. For example: ```dart session.updateConnectorLabel( connectionId, labelId, story: TextStory( id: existingStory.id, blocks: [ for (final block in existingStory.blocks) block.copyWith(runs: [ for (final run in block.runs) run.copyWith(fontSize: 20, color: const DiagramColor(0xff2454d8)), ]), ], ), presentation: const BadgeLabelPresentation( fill: DiagramColor(0xffffffff), stroke: DiagramColor(0xff2454d8), strokeWidth: 2, cornerRadius: 8, padding: 6, ), ); ``` `existingStory` is the selected attachment's current story. Preserve its IDs and other run properties when restyling. A text change and presentation change in the same call publish and undo together. ### `session.updateConnectorLabel` ```dart void updateConnectorLabel( String connectorId, String labelId, { TextStory? story, double? fraction, DiagramPoint? offset, DiagramSize? size, LabelPresentation? presentation, }) ``` | Argument | Contract | | --- | --- | | `connectorId`, `labelId` | Required positional identities. Missing or locked targets reject the operation. | | `story` | Shared paragraph/run content. Requires text editing capability. | | `fraction` | Position along resolved path length, from `0` to `1`; requires movement capability. | | `offset` | World-space displacement from the path anchor; requires movement capability. | | `size` | Authored label size; requires resize capability, disabled by default. | | `presentation` | Plain text or badge surface. Omitted fields preserve their current values. | All named arguments default to `null`, meaning unchanged. The method returns `void`; invalid values, missing capabilities or targets throw. Host admission denial throws `DiagramChangeDenied`. No partial update occurs. ### `LabelPresentation` | Production | Contract | | --- | --- | | `const PlainLabelPresentation()` | Text only. No fill, border, radius or padding properties. | | `const BadgeLabelPresentation(...)` | Rounded backing surface with the fields below. | | Badge property | Type | Default | | --- | --- | --- | | `fill` | `DiagramColor` | `0xffffffff` (white). Use transparent for a border-only badge. | | `stroke` | `DiagramColor` | `0xff94a3b8` (muted slate). | | `strokeWidth` | `double` | `0` (no border). | | `strokeStyle` | `StrokeStyle` | `solid`; also supports `dashed` and `dotted`. | | `cornerRadius` | `double` | `4`. | | `padding` | `double` | `4`. | Dimensions must be finite and nonnegative. Padding must leave a valid text interior. `copyWith` returns a new badge value; equality includes every field. Border geometry is centered on the backing surface. Resolved `visualBounds` include its half-width bleed for hit testing, selection and scene bounds; text layout continues to use the padded interior. A visible badge fill or border reserves its area from path effects so particles cannot paint over it. These styles are saved in JSON version 14 and rendered by the shared canvas/PNG painter. Earlier versions migrate with no border, preserving their appearance. ## Creation defaults `ShapeRegistry(connector: DiagramConnectorDefinition(...))` configures connector creation and labeling for the session. | Property | Type | Default | Behavior | | --- | --- | --- | --- | | `defaultEndOffset` | `DiagramPoint` | `(160, 0)` | Canvas-space displacement from the start when the connector tool is clicked without dragging. Must be finite and nonzero. Grid snapping still applies. Dragging uses the pointer endpoint. | | `labeling` | `ConnectorLabelingCapability?` | Default labeling capability | Configures label creation and editing; `null` disables labels. | ### Inspector editing ranges `DiagramConnectorDefinition.editing` accepts `DiagramConnectorEditing`. These bounds configure the built-in inspector; they do not rewrite authored route values or change the geometry admission rules. | Property | Default | | --- | --- | | `minimumStrokeWidth` / `maximumStrokeWidth` | `1` / `32` | | `minimumArrowSize` / `maximumArrowSize` | `4` / `64` | | `maximumPortSpacing` | `64` | | `maximumPortExtension` | `160` | | `maximumCornerRadius` | `64` | All values must be finite and positive, and each minimum must not exceed its maximum. Registry construction validates the declaration. Spacing, extension, and radius controls retain a zero lower bound. Route capabilities determine which controls are shown; quadratic and Bezier routes do not acquire extension controls simply because an editing maximum exists. `addConnectorLabel(text: ...)` assigns story and paragraph identities. Supply either plain `text` or a structured `story`, never both. Registered label rules still apply: labels accept multiline plain paragraphs, while list content is rejected without publishing a label or changing undo history. --- # Domain diagnostics `DiagramDiagnostics.check` runs explicit checks against an immutable document. Use it to review drafts without preventing incomplete edits. Keep hard admission rules in `documentValidator`. ```dart final report = DiagramDiagnostics.check(session.document, rules: [ (document) => [ for (final shape in document.elements.whereType()) if (shape.type == Shapes.rectangle && shape.parentId == null) DiagramDiagnostic( id: 'department:${shape.id}', message: 'Assign this task to a department.', elementIds: [shape.id], ), ], ]); if (report.isCurrent(session.document) && report.items.isNotEmpty) { session.selectElements(report.items.first.elementIds); } ``` Each finding has a unique ID, message, severity and optional element targets. Targets must exist in the checked document. `hasErrors` ignores informational findings and warnings. Check `isCurrent` before using a result; rerun checks after content changes. Rules must be pure. Checks run only when called, not on pointer movement or camera updates. The headless engine provides reports; the Flutter kit adds a panel: ```dart DrawingDiagnostics(session: session, rules: [reviewDocument]) ``` Mount the panel in a bounded area. **Run checks** evaluates the rules. Selecting a finding selects its target elements. For targets hidden inside collapsed frames, the panel selects the nearest visible containing frame without expanding it or changing undo history. Hidden bundle members select their visible representative; hidden internal connectors use their endpoint frames even without a parent. Edits mark the report stale and disable its selection actions until checks run again. Try [network review](/agent-docs/examples/dense-network.md) for a missing backbone binding or [department ownership](/agent-docs/examples/swimlanes.md) for an unassigned review task. Both demonstrate repair, rechecking, and undo using host-owned rules. --- # Grids and snapping `DrawingGridConfiguration` configures the base grid interval. With the grid visible, direct manipulation snaps to the displayed spacing, including density reduction when zooming out. It never snaps to hidden subdivisions. With the grid hidden, snapping uses the configured base interval. Visibility does not enable snapping by itself. ## On this page | Category | What you can do | | --- | --- | | [Grid settings](#drawinggridconfiguration) | Set visibility, spacing and snapping. | | [Patterns](#diagramgridpattern) | Choose or compose a visual pattern. | | [Layers](#grid-layers) | Describe each pattern layer. | ## `DrawingGridConfiguration` ```dart DrawingGridConfiguration({ bool visible = false, bool snap = false, double size = 20, DiagramGridPattern pattern = DiagramGridPattern.layered, }) DrawingGridConfiguration copyWith({ bool? visible, bool? snap, double? size, DiagramGridPattern? pattern, }) ``` | Property | Type | Default | Contract | | --- | --- | --- | --- | | `visible` | `bool` | `false` | Paint the grid. Does not enable snapping. | | `snap` | `bool` | `false` | Enable grid snapping. Does not require a visible grid. | | `size` | `double` | `20` | World-space interval, normalized on construction and `copyWith`. | | `pattern` | `DiagramGridPattern` | `DiagramGridPattern.layered` | Immutable composition of paint layers. | `copyWith` returns a new configuration; omitted or `null` arguments preserve existing values. All four properties are final. ### Size normalization `DrawingGridConfiguration(size: value)` normalizes the interval automatically; `copyWith(size: value)` applies the same rule. Read `.size` to obtain the normalized interval. No separate settings object or normalization call is needed. ```dart final grid = DrawingGridConfiguration(size: 23); assert(grid.size == 24); ``` | Constraint | Value | | --- | --- | | Minimum interval | `4` | | Maximum interval | `128` | | Interval increment | `4` | Finite inputs are divided by `4`, rounded with Dart's `round()`, multiplied by `4`, and clamped to `[4, 128]`. Non-finite inputs become `20`; negative or zero finite inputs become `4`. Normalization does not throw for those values. ## `DiagramGridPattern` ```dart factory DiagramGridPattern({ required String id, required String label, required Iterable layers, }) ``` | Property | Type | Requirement | | --- | --- | --- | | `id` | `String` | Nonempty after trimming and no leading/trailing whitespace. | | `label` | `String` | Nonempty after trimming. | | `layers` | `List` | Unmodifiable copy containing 1–16 layers. | Patterns compare by `id`, `label`, and ordered layer values, with a matching `hashCode`. | Static value | ID / label | Layers | | --- | --- | --- | | `DiagramGridPattern.dots` | `dots` / `Dots` | One default `DiagramDotGridLayer`. | | `DiagramGridPattern.lines` | `lines` / `Lines` | One default `DiagramLineGridLayer`. | | `DiagramGridPattern.layered` | `layered` / `Layered` | Lines at interval `1`, color `0xB8E5E5E5`; lines at interval `5`, color `0x9ED4D4D4`. | | `DiagramGridPattern.presets` | — | Constant list `[dots, lines, layered]`. | ## Grid layers `DiagramGridLayer` is sealed. Its concrete productions are dots, lines, and crosses: ```dart const DiagramDotGridLayer({ int spacingMultiple = 1, DiagramColor color = const DiagramColor(0xFFCDD5E4), double size = 2, }) const DiagramLineGridLayer({ int spacingMultiple = 1, DiagramColor color = const DiagramColor(0xFFCDD5E4), double size = 1, }) const DiagramCrossGridLayer({ int spacingMultiple = 1, DiagramColor color = const DiagramColor(0xFFCDD5E4), double size = 3, }) ``` | Property | Type | Default | Meaning / accepted range in a pattern | | --- | --- | --- | --- | | `spacingMultiple` | `int` | `1` | Multiplies the shared world interval; `1`–`1024`. | | `color` | `DiagramColor` | `DiagramColor(0xFFCDD5E4)` | ARGB integer from `0` through `0xFFFFFFFF`. | | `size` | `double` | Dot: `2`; line: `1`; cross: `3` | Screen-space dot diameter, line stroke width, or cross arm length. Must be finite, `> 0`, and `<= 32`. | Layer values compare by concrete type and all three properties. Validation occurs when constructing `DiagramGridPattern`, which throws `ArgumentError` for invalid identity, label, layer count, spacing, size, or color. Layer constructors themselves do not perform this pattern validation. ## Example ```dart final pattern = DiagramGridPattern( id: 'cross-and-major-lines', label: 'Crosses and major lines', layers: const [ DiagramCrossGridLayer(size: 3), DiagramLineGridLayer( spacingMultiple: 5, color: DiagramColor(0x99CDD5E4), ), ], ); session.setConfiguration(session.configuration.copyWith( grid: session.configuration.grid.copyWith( visible: true, snap: true, size: 20, pattern: pattern, ), )); ``` See [custom grid patterns](/agent-docs/guide/canvas.md#compose-your-own-pattern) for a canvas example. Rotation snapping uses `DrawingConfiguration.rotationSnapDegrees` rather than the grid configuration. ## Neighbor snap guides Grid snapping and neighbor alignment are independent. Install the optional extension to align moved selections to nearby resolved bounds: ```dart DrawingConfiguration( extensions: [ DiagramSnapGuidesExtension( policy: DiagramSnapGuidePolicy( sides: true, centers: true, maxCandidates: 24, ), tolerance: 6, color: const Color(0xFF5260FF), ), ], ) ``` Set `color` to any Flutter `Color` to match your application; the default is `Color(0xFF5260FF)`. It controls the guide lines without changing snapping behavior. Use `enabled` for an on/off toggle. The snap tolerance is in screen pixels. Candidate shapes must intersect the current viewport. Candidate limits range from 1 to 256 and apply after the spatial query and distance ranking. Guides use resolved bounds, including borders. Selected elements and their containing ancestors are excluded. Neighbor alignment takes priority over the grid on aligned axes; other axes keep grid behavior. Guides appear only during selection moves and clear on completion or cancellation. Try [snap guides](/agent-docs/examples/snap-guides.md) and [policy options](/agent-docs/examples/snap-policy.md). --- # Undo and redo Use `session.undo()` and `session.redo()` for document history. The drawing has one history; text editing, shape manipulation, connector changes and host commands do not maintain separate undo stacks. ## On this page | Category | What you can do | | --- | --- | | [Undo and redo](#session-api) | Read availability and undo or redo an edit. | | [Atomic edits](#atomic-editing) | Combine related changes into one operation. | | [Restored content](#what-undo-restores) | Understand what an undo action changes. | | [Edit grouping](#editing-granularity) | Keep each user action as one history entry. | | [Permissions and events](#permissions-and-events) | Apply admission rules and observe completed history actions. | ## Session API | Member | Type / signature | Behavior | | --- | --- | --- | | `canUndo` | `bool get canUndo` | Whether an undo entry exists. This is availability, not a permission check. | | `canRedo` | `bool get canRedo` | Whether a redo entry exists. This is availability, not a permission check. | | `undo()` | `DiagramPolicyDecision undo()` | Restore the preceding entry's document and selection after admission. | | `redo()` | `DiagramPolicyDecision redo()` | Restore the next entry's document and selection after admission. | | `addListener(listener)` | `void addListener(DiagramStateListener listener)` | Observe live publications and read updated history availability. | | `removeListener(listener)` | `void removeListener(DiagramStateListener listener)` | Remove the same listener instance. | `undo()` and `redo()` take no arguments. With an empty corresponding stack they return an allowed decision without restoring anything. Both first close an active history group. Commit or cancel an active pointer interaction before calling either method programmatically. ### Return value | `DiagramPolicyDecision` member | Meaning | | --- | --- | | `bool allowed` | The operation passed admission. An allowed no-op does not mean content changed. | | `String? reason` | Optional explanation when denied. | | `void requireAllowed()` | Throws `DiagramChangeDenied` if denied; otherwise returns normally. | A denied restoration leaves its history cursor and current publication unchanged. If closing a preceding group is denied, that group is rolled back and undo/redo does not proceed. ```dart final decision = session.undo(); if (!decision.allowed) { // Present decision.reason in the host application's UI. } ``` ### Keeping toolbar state current ```dart void onDrawingChanged() { setState(() { // Rebuild controls that read session.canUndo / session.canRedo. }); } // Register once when the host mounts. session.addListener(onDrawingChanged); // Remove when the host unmounts. session.removeListener(onDrawingChanged); ``` Session listeners take no arguments. Read `session.canUndo` and `session.canRedo` in the callback. Schedule follow-up edits after notification completes. ## Atomic editing Each session command is one validated operation. Batch commands such as `moveElements` and `setShapeStyles` update multiple elements as one undo action. For custom atomic operations, see [transactions and previews](/architecture/transactions). ## What undo restores | State | History behavior | | --- | --- | | Document elements, stories, bindings and order | Restored from the entry's before/after snapshot. | | Selection | Restored with the document and mapped to valid resolved targets. | | Camera, zoom and viewport animation | Current navigation is retained. See [navigation and animation](/agent-docs/api/navigation-and-animation.md). | | Canvas configuration, grids and snapping | Current settings are retained. | | Active tool and default connector router | Current choices are retained. | | Environmental layout refresh | No document history entry. | Restoration resolves geometry before publishing it with the document and selection. Revisions remain monotonic: undo restores prior content, not an old revision number. An edit with unchanged content creates no history entry or completed-change event and preserves redo. This includes empty transactions, same-content replacements and a gesture or group that returns to its starting content. Selection-only changes are transient. An unchanged direct dispatch retains the document revision and scene; already-published previews can have advanced the revision even when the final net edit is empty. A new content-changing transaction after undo clears the redo branch. Cancelling a grouped edit restores the redo branch that existed before that group. ## Editing granularity | Operation | Undo unit | | --- | --- | | One `dispatch` with multiple steps | One entry for the complete transaction. | | Continuous move, resize or other editor gesture | Live previews followed by one committed entry. Cancellation restores the baseline. | | IME composition | Provisional story updates grouped into one operation when composition finishes. | | Ordinary text edits outside composition | Each dispatched text transaction. No general time-based typing coalescing is exposed. | | Explicit history group | Multiple dispatches sharing a group token, closed as one operation. | ## Permissions and events `configuration.hooks.beforeChange` evaluates transactions, grouped previews, gesture previews and history restoration. Undo and redo use `DiagramChangePhase.commit` and origins `undo` / `redo`; they receive new operation IDs. Their restoration of previously admitted snapshots bypasses ordinary element-lock checks, but **does not bypass the host admission hook**. `configuration.hooks.onChange` receives one completed nonempty edit, including undo/redo. It excludes previews, cancellation, camera movement and selection-only changes. Use `session.addListener` when the host needs provisional state. See [events and hooks](/agent-docs/api/hooks.md) for proposal and event property tables. ## Keyboard shortcuts With the canvas or paragraph editor focused: | Action | Shortcut | | --- | --- | | Undo | Command/Ctrl + Z | | Redo | Command/Ctrl + Shift + Z, or Command/Ctrl + Y | The paragraph editor closes composition before invoking command-based undo/redo and resynchronizes its editing buffer from canonical state. ## Current scope History stays in the owning session’s memory and is excluded from document JSON. Use `canUndo` and `canRedo` for toolbar state. Stack inspection, limits, clearing, persistence and per-author undo are not exposed. Disposing a borrowed session preserves the owner’s history. --- # Events and hooks Configure host integration in `DrawingConfiguration.hooks`. The session applies document admission consistently, so a mounted canvas, headless session, custom tool and direct transaction use the same boundary. ## On this page | Category | What you can do | | --- | --- | | [Configure callbacks](#drawinghooks) | Provide permissions and interaction callbacks. | | [Domain invariants](#fixed-domain-validation) | Validate all changed documents consistently. | | [Proposed changes](#diagramchangeproposal) | Inspect an edit before accepting it. | | [Decisions](#diagrampolicydecision) | Allow or reject an operation with a reason. | | [Committed changes](#diagramdocumentchange) | Observe accepted content updates. | | [Interactions](#interaction-callbacks) | Respond to regions and connection events. | ## `DrawingHooks` ```dart const DrawingHooks({ DiagramBeforeChange? beforeChange, DiagramDocumentChangeListener? onChange, ShapeRegionInteractionHandler? onShapeRegionInteraction, DiagramConnectionEventHandler? onConnectionEvent, }) ``` All four properties are final and default to `null`. | Callback | Signature | Purpose | | --- | --- | --- | | `beforeChange` | `DiagramPolicyDecision Function(DiagramChangeProposal)` | Admit or reject a validated candidate before publication. | | `onChange` | `void Function(DiagramDocumentChange)` | Observe a completed, nonempty document edit. | | `onShapeRegionInteraction` | `bool Function(ShapeRegionInteraction)` | Handle input on a declared shape region. | | `onConnectionEvent` | `void Function(DiagramConnectionEvent)` | Observe the UI connection lifecycle. | ## Example ```dart final session = DrawingSession( document: document, configuration: DrawingConfiguration( hooks: DrawingHooks( beforeChange: (proposal) { if (readOnly) { return const DiagramPolicyDecision.deny('This drawing is read-only.'); } return const DiagramPolicyDecision.allow(); }, onChange: (change) { // Queue persistence or host work using this immutable snapshot. saveQueue.add(change.after); }, ), ), ); ``` ## Fixed domain validation Domain kits can supply `documentValidator` when creating the session. This is a pure, synchronous `DiagramPolicyDecision Function(DiagramDocument)` fixed for the source session's lifetime. It checks initial content and changed candidates, including previews, imports and undo/redo, before application permission hooks run. | Boundary | Lifetime | Failure | | --- | --- | --- | | `documentValidator` | Fixed when the source session is created; cannot be removed by settings or hook changes. | Initial denial throws `DiagramChangeDenied`; edit denial returns a decision without admitting the candidate. | | `beforeChange` | Configurable application hook. | Rejects publication based on current permissions or host policy. | Validators must not edit the drawing or depend on changing application permissions. The workflow kit uses this boundary for primitive configuration and connection rules. Clearing a permission hook cannot turn those domain rules off. Asynchronous schema validation requires a separate preparation contract; returning a `Future` from either callback is unsupported. ## `DiagramChangeProposal` The argument to `beforeChange`. Read this immutable event in your callback; `elements` is calculated only when requested. | Property | Type | Meaning | | --- | --- | --- | | `before` | `DiagramDocument` | Operation baseline, not the previous pointer or IME preview. | | `after` | `DiagramDocument` | Proposed content after the edit. | | `scene` | `ResolvedScene` | Geometry matching the proposed document. | | `currentDocument` | `DiagramDocument` | Current content, including the previous preview when applicable. | | `currentScene` | `ResolvedScene` | Geometry matching the current document. | | `phase` | `DiagramChangePhase` | `preview` or `commit`. | | `origin` | `DiagramDocumentChangeOrigin` | Entry path listed below. | | `operationId` | `int` | Store-local identity retained across previews and final commit. | | `historyOperationId` | `int?` | Original accepted operation being undone or redone; null for other edits. | | `participantId` | `String` | Initiating participant captured with the operation. Set through session construction, defaulting to `local`; this is attribution, not authentication. | | `label` | `String` | Operation label. | | `elements` | `DiagramElementChangeSet` | Lazy baseline-to-candidate delta; computed only when read. | ## `DiagramPolicyDecision` | Member | Arguments | Result | | --- | --- | --- | | `const DiagramPolicyDecision.allow()` | None | `allowed == true`, `reason == null`. | | `const DiagramPolicyDecision.deny([String? reason])` | Optional explanation | `allowed == false`; retains the explanation with `code == DiagramDenialCode.policy`. | | `const DiagramPolicyDecision.denied(DiagramDenialCode code, {String? reason})` | Required category and optional explanation | Categorized denial for host handling. | | `bool allowed` | Read-only property | Whether admission succeeded. | | `String? reason` | Read-only property | Optional denial explanation. | | `DiagramDenialCode? code` | Read-only property | Null for success; stable category for denial. | | `void requireAllowed()` | None | Returns normally when allowed; otherwise throws `DiagramChangeDenied`. | The denial categories are `policy`, `locked`, `unavailableTarget`, `invalidSelection`, `unsupportedOperation`, and `conflict`. Branch on `code`, not the human-readable reason. `DiagramChangeDenied.decision` contains the rejected decision. Its string representation uses the reason when supplied. ## `DiagramDocumentChange` The argument to `onChange`. Read this immutable event in your callback; `elements` is calculated only when requested. | Property | Type | Meaning | | --- | --- | --- | | `before` | `DiagramDocument` | Baseline before the completed edit. | | `after` | `DiagramDocument` | Accepted content after the edit. | | `scene` | `ResolvedScene` | Geometry matching the accepted document. | | `label` | `String` | Completed operation label. | | `origin` | `DiagramDocumentChangeOrigin` | `transaction`, `import`, `interaction`, `groupedEdit`, `undo`, or `redo`. | | `operationId` | `int` | Matches the admission proposal for this operation. | | `historyOperationId` | `int?` | Original accepted operation being undone or redone; null for other edits. | | `participantId` | `String` | Matches the participant captured by the admission proposal. | | `elements` | `DiagramElementChangeSet` | Lazy baseline-to-final delta. | ### `DiagramElementChangeSet` | Property | Type | Meaning | | --- | --- | --- | | `insertedIds` | `Set` | Identities added to the document. | | `removedIds` | `Set` | Identities removed from the document. | | `updatedIds` | `Set` | Existing identities whose elements changed. | | `orderChanged` | `bool` | Element order changed, including insertion or removal. | Sets are unmodifiable. Computing the delta scans the documents in O(n). Replacing the document identity treats all old elements as removed and all new elements as inserted, even when local element IDs match. ## Document admission `beforeChange` receives a `DiagramChangeProposal` containing the operation's `before` document, validated candidate publication, currently published snapshot, label, origin, phase and operation ID. Its `after` document and lazy `elements` delta describe the actual candidate, including custom steps and indirectly removed elements. Return `DiagramPolicyDecision.allow()` or `DiagramPolicyDecision.deny(reason)`. The hook cannot rewrite the candidate or synchronously mutate the session. Transformations belong in explicit transactions. An exception aborts admission and propagates to the caller; it never becomes an implicit allow. | Entry path | Admission behavior | | --- | --- | | `dispatch` | Checks the proposed transaction before history or publication changes. | | `updateValues` | Uses ordinary transaction admission. Invalid value fields/types/ranges reject before publication. | | `beginValueEdit` | Returns a gesture lease; each `preview` and the final `commit` use the gesture rules below. | | Gesture preview | Checks baseline-to-candidate with `preview` phase. Denial restores the baseline while retaining gesture ownership for recovery. | | Gesture commit | Rechecks with `commit` phase. Denial restores the baseline and closes the gesture without a history entry. | | Grouped text editing | Checks provisional changes, then checks the complete operation when the group closes. Final denial restores the group baseline and prior redo branch. | | Snapshot import | Checks the complete replacement and expected revision through the same transaction pipeline. | | Undo and redo | Checks the actual restoration before advancing history. | | Cancellation | Always restores the baseline, without consulting admission. | | Camera, selection, grids and layout refresh | Remain available without document permission. | Low-level `session.dispatch`, `undo` and `redo` return a `DiagramPolicyDecision`. Convenience creation commands return an identity on success and throw `DiagramChangeDenied` on denial; they never return an ID for content that was not inserted. Operation IDs are local to the source session. A gesture or composition retains its ID across previews and final admission. Reading the optional element delta currently computes an O(n) document diff; receiving a proposal alone does not allocate that diff. ## Completed changes and live state Use `session.addListener` for live state, including interaction previews and provisional text composition. Use `hooks.onChange` for completed transactions, committed gestures, grouped edits, undo and redo. Cancellation does not emit a completed change. Session subscriptions stop immediately on removal or disposal, including callbacks already queued for the current publication. Removing and re-adding a callback creates a new subscription that starts with the next publication. A callback registered on the source session remains owned by that source; disposing a Flutter adapter does not remove it. For multiple independently owned consumers, use `session.addDocumentChangeListener(listener)` and `session.removeDocumentChangeListener(listener)`. These observe the same completed events as `onChange`, exclude camera/selection updates, and are removed when the session is disposed. Changing configuration does not replace these subscriptions. `change.operationId` matches the admission proposal for that operation. Grouped edits and gesture previews keep the same ID through their final commit. Undo and redo each receive a new ID. IDs are local to the source session lifetime, not persistent document IDs or globally unique audit identifiers; a denied proposal has no corresponding completed change. A grouped edit closes when composition finishes or an unrelated edit takes ownership. Its event contains the complete baseline-to-final delta. If final admission is denied, the unrelated operation is also left unapplied; submit a fresh intent against the restored state. Observers cannot synchronously edit the drawing. Queue later work as a separate command. All observers receive the publication before callback failures are forwarded to the current error zone. ## Embedded control edits Widget controls use the same document hooks as headless commands. There is no separate widget persistence or authorization callback. | Control action | Live state | Completed `onChange` | | --- | --- | --- | | `shape.updateValues(patch)` | Publishes an admitted transaction. | One event for a nonempty accepted edit. | | `shape.beginValueEdit().preview(patch)` | Publishes an admitted preview relative to the gesture baseline. | None until commit. | | `edit.commit()` | Rechecks commit admission. | One baseline-to-final event if accepted and changed. | | `edit.cancel()` or denied commit | Restores the baseline. | None. | Here `shape` is the `ShapeWidgetContext` supplied to the native control builder. The corresponding session methods are `session.updateValues(elementId, patch)` and `session.beginValueEdit(elementId)`. A continuous slider should use a value edit so persistence sees one completed operation instead of every pointer sample. Use the live session listener when a host preview needs to follow each sample. Compare `ShapeElement.values` in `before` and `after` for IDs in `elements.updatedIds`; this is not a field-level diff. Check permissions in admission, not the widget’s `enabled` flag. A denied commit restores the gesture baseline without adding history. ## Interaction callbacks `hooks.onConnectionEvent` observes connection lifecycle events. `hooks.onShapeRegionInteraction` observes declared region interactions. These events describe UI activity; they do not replace document admission and are not synthesized for headless commands. A handled region gesture retains the callback that received its start. Replacing `onShapeRegionInteraction` affects future gestures; remaining updates, end, or cancel callbacks for the current gesture stay with its initiating handler. ### `ShapeRegionInteraction` | Property | Type | Meaning | | --- | --- | --- | | `elementId` | `String` | Shape receiving input. | | `regionId` | `String` | Declared region ID. | | `interactionId` | `String` | Declared interaction ID. | | `phase` | `ShapeRegionInteractionPhase` | `start`, `update`, `end`, or `cancel`. | | `worldPoint` | `DiagramPoint` | Pointer position in world coordinates. | | `localPoint` | `DiagramPoint` | Pointer position in shape-local coordinates. | Return `true` at `start` to claim the interaction. Returning `false` permits fallback handling. Return values for subsequent phases are ignored. ### `DiagramConnectionEvent` | Property | Type | Default / meaning | | --- | --- | --- | | `phase` | `DiagramConnectionEventPhase` | Required: `portHoverEnter`, `portHoverExit`, `started`, `targetChanged`, `committed`, or `canceled`. | | `connectorId` | `String?` | `null` when unavailable. | | `source` | `ConnectorEndpoint?` | Source endpoint, when available. | | `target` | `BoundEndpoint?` | Bound target, when available. | | `decision` | `DiagramPolicyDecision` | Defaults to `allow()`; describes the associated policy decision. | The callback returns `void`. Observing an event does not create or authorize a connector. The existing session `elementCreationPolicy` and `connectionPolicy` callbacks still filter their respective service intents. They are not universal permission gates: custom transactions bypass those service filters but always pass through `beforeChange`. Put application-wide mutation rules in `beforeChange`. Policy callbacks are read-only. Nested commands, ID allocation, disposal and policy evaluation throw `StateError`; run later commands after the callback completes. Failure leaves the session usable. Commands reject disposed sessions. External host side effects cannot be rolled back. ## Ownership and lifetime Replace hooks through `session.setConfiguration(session.configuration.copyWith(hooks: ...))`. The next preview and final commit use the new callback. Replacement is prohibited during candidate evaluation or observer delivery. Advanced ownership adapters are documented in [Architecture](/architecture/ownership). Initial document loading is validated but does not replay thousands of creation events. Authorizing access to a snapshot belongs to the host. Hooks are synchronous client-side controls; asynchronous approval and server authorization remain host responsibilities. See [persistence and imports](/agent-docs/api/persistence.md) for revision-checked replacement. ## Actions versus hooks Use [named actions](/agent-docs/guide/actions.md) for an edit initiated by a button, key or painted region. Use `beforeChange` to admit or deny the resulting transaction and `onChange` to observe publication. An action cannot bypass these hooks. A matching named region action consumes the region interaction and invokes on release inside; unmatched region IDs continue through the region hook. --- # API reference For day-to-day integration, start with the [common tasks map](/agent-docs/guide/common-tasks.md). It covers command outcomes, configuration ownership and platform capabilities. Choose a reference by the object or operation you are working with. The [getting started guide](/agent-docs/guide/getting-started.md) covers your first canvas. - [API layers and ownership](./ownership) — host versus advanced APIs and resource lifetimes - [Session](/agent-docs/api/session.md) — commands, observation and lifetime - [Configuration](/agent-docs/api/configuration.md) — appearance and behavior - [Shapes](/agent-docs/api/shapes.md) — definitions, registry and instances - [Embedded widgets](/agent-docs/api/widgets.md) — native controls, canonical values and input ownership - [Connectors](/agent-docs/api/connectors.md) — bindings, routing and labels - [Canvas](/agent-docs/api/canvas.md) — mounting and optional chrome - [Navigation](/agent-docs/api/navigation.md) — camera state, coordinates and immediate positioning - [Animation](/agent-docs/api/animation.md) — viewport transitions, cancellation and path effects - [Grids](/agent-docs/api/grids.md) — snapping and custom patterns - [Text](/agent-docs/api/text.md) — shared paragraphs and lists - [Undo and redo](/agent-docs/api/history.md) — transactions and history - [Persistence and imports](/agent-docs/api/persistence.md) — serialization and snapshot replacement - [Output](/agent-docs/api/output.md) — PNG snapshots, native SVG text and portable content - [Events and hooks](/agent-docs/api/hooks.md) — integration and admission ## The object model | Object | Responsibility | | --- | --- | | `DiagramDocument` | Immutable authored shapes, standalone text, connectors, parent relationships, text stories and styles. | | `FramedElement` | Shared placement and transform contract for shapes and standalone text. | | `TextElement` | Standalone rich text with a required story and no shape appearance fields. | | `TextElementDefinition` | Standalone text bounds, layers, sizes, and interaction capabilities, exposed through `ShapeRegistry.text`. | | `ShapeDefinition` | One shape type: outline, layers, text regions, ports, defaults and interaction capabilities. | | `ShapeRegistry` | Immutable, validated collection of definitions, plus connector capabilities. | | `ShapeElement` / `ConnectorElement` | Authored instances with stable IDs. A shape's `type` selects its definition. | | `DrawingSession` | Canonical state, resolved scene, commands, history, configuration, subscriptions and lifetime. | | `DrawingConfiguration` | Canvas policy, background, grouped grid options, rotation increments, paint detail, overlays, effects and integration callbacks. | | `DrawingGridConfiguration` | Grid visibility, snapping, spacing and pattern; a value inside the drawing configuration. | | `DrawingCanvas` | The bounded Flutter widget that displays and edits one session. | A session supports one mounted canvas at a time. Content operations also work without mounting it; camera and visual animation commands need a mounted canvas. Separate sessions are possible for separate editors or agent work, but shared-document synchronization, merging and collaborative history are not provided automatically. ## Use the toolkit Start with one import in a Flutter application: ```dart import 'package:vyuh_diagram_kit/vyuh_diagram_kit.dart'; ``` | Task | API | | --- | --- | | Start editing | Create a `DrawingSession` and display it with `DrawingCanvas`. | | Create content | `session.createShape`, `createText`, `createConnector`. | | Change appearance | `session.setShapeStyle` and text formatting commands. | | Edit objects | Selection, movement, resize, grouping, clipboard and deletion commands. | | Undo or redo | `session.undo()`, `session.redo()`. | | Save or load | `session.document.encode()`, `session.importJson(...)`. | | Export | `session.exportSvg(...)`, `session.exportPng(...)`. | | Navigate or animate | `session.navigateToElement(...)`, `session.animateToSelection(...)` and path effects. | | Add domain concepts | Register shape definitions, values, ports and widget builders; use the same editing commands. | A workflow kit adds workflow meanings and validation to these building blocks. It does not need a second selection, connection, history or export system. For framework implementation details, see [architecture](../reference/architecture). For plain Dart agents, see the [agent guide](/agent-docs/agents/index.md). - [Actions and keyboard bindings](/agent-docs/guide/actions.md) - [Complete custom-library recipe](/agent-docs/guide/build-a-library.md) --- # Graph layout `GraphLayoutAlgorithm` is the extension API for node placement. It lives in the pure Dart engine and is exported by the kit. Algorithms receive immutable node sizes, directed edges and spacing options, and return top-left positions. They do not own rendering, input, document state or connector routing. ```dart (await session.layoutNodes( algorithm: const ElkGraphLayout(), direction: GraphLayoutDirection.right, nodeSpacing: 60, rankSpacing: 100, )).requireAllowed(); ``` `ElkGraphLayout` uses the published pure-Dart [elk package](https://pub.dev/packages/elk), pinned to 0.2.0. No JavaScript engine, worker script, npm dependency or network service is required. `CircularGraphLayout` is also available on every platform. On native Dart platforms, ELK runs in an isolate to keep computation off the UI thread. Flutter web executes the same Dart code in its compiled runtime; it yields before starting, but the calculation itself is synchronous. Large web graphs can therefore block interaction; measure representative workloads before enabling large layouts in an interactive flow. `layoutNodes` applies all positions through one revision-checked transaction. Undo restores the previous arrangement. A document change, active edit, newer layout request, or `session.cancelLayout()` prevents a pending result from being applied. Cancellation invalidates the result; it does not interrupt computation. Invalid or incomplete output cannot partially move the graph. Layout uses resolved node sizes and world coordinates. It supports root-level leaf nodes, optionally filtered by `nodeIds`; locked, immovable, nested and child-owning nodes reject. Internal edges inform placement, while boundary bindings remain intact. The diagram router recomputes connections; ELK routes are not imported. ## Custom algorithms Implement `GraphLayoutAlgorithm.layout(GraphLayoutRequest)` and return a `GraphLayoutResult` containing exactly one finite position per requested node. Synchronous Dart implementations and isolate-backed implementations use the same API. No editor extension or private controller is needed. Try the [topology graph](/agent-docs/examples/topology.md). --- # Navigation and animation These APIs have separate references so their arguments and lifecycles are easy to find: - [Navigation](/agent-docs/api/navigation.md): immediate camera movement, coordinate conversion, camera state, and presentation listeners. - [Animation](/agent-docs/api/animation.md): timed camera movement, cancellation, connector path effects, particles, and custom effect registration. Both use the session's mounted canvas. They do not add document history entries. For content history, see [Undo and redo](/agent-docs/api/history.md). --- # Navigation Navigate through `DrawingSession`. These methods move the mounted canvas camera immediately; they do not move elements, change the document, or add undo entries. For timed movement, use the separate [Animation API](/agent-docs/api/animation.md). ```dart session.navigateToPoint(const DiagramPoint(240, 160), zoom: 1.25); final found = session.navigateToElement('review', padding: 48); ``` ## On this page | Category | What you can do | | --- | --- | | [Pointer and keyboard](#pointer-and-keyboard-navigation) | Configure edge auto-pan and Arrow-key movement. | | [Attach a view](#mounting-and-coordinates) | Understand when camera commands are available. | | [Navigate](#immediate-navigation-methods) | Move or fit the view to content. | | [Coordinates](#coordinate-conversion) | Convert pointer positions into drawing coordinates. | | [Read the view](#camerastate) | Observe camera position, zoom and viewport. | | [Observe changes](#presentation-listeners) | React to view changes without tracking document edits. | ## Pointer and keyboard navigation While dragging an element, handle, marquee or connection, entering the padding inside a canvas edge starts auto-pan. Speed increases toward the edge and continues while the pointer stays there. Corners pan on both axes. Leaving the edge area, releasing or cancelling the drag stops auto-pan. Hover alone does not move the canvas. The active edit follows the camera and remains one undo step. Escape cancels the active gesture, including modifier combinations such as Space-drag panning, Shift rotation and Shift+Alt resizing. Document previews roll back without an undo entry; marquee selection returns to its starting selection. Panning stops at the current camera position. Remaining pointer movement and release cannot resume or commit the canceled gesture. With canvas focus, Arrow keys move selected elements by one world unit in the pressed direction. Shift+Arrow moves them by ten world units; holding a key repeats movement. With no selection, neither shortcut moves the canvas. Text editing and focused widget controls retain their own arrow-key behavior. Selection movement supports undo and respects locks and host admission. ```dart final configuration = DrawingConfiguration( navigation: const NavigationConfiguration( edgePanEnabled: true, edgePadding: 48, maximumPanSpeed: 600, keyboardStep: 1, shiftMultiplier: 10, ), ); ``` Distances use logical screen pixels; `maximumPanSpeed` uses pixels per second. Numeric settings must be finite and positive. Set `edgePanEnabled: false` to disable pointer auto-pan. Finite-canvas limits still constrain navigation. ## Mounting and coordinates Mount `DrawingCanvas(session: session)` before calling navigation or coordinate-conversion methods. Calls on a detached or disposed session throw `StateError`. For initial navigation, wait until the canvas has completed its first layout, for example in a post-frame callback. | Quantity | Coordinate space | | --- | --- | | `DiagramPoint` passed to navigation | World coordinates in the document. | | `DiagramRect` passed to navigation | World bounds to fit in the viewport. | | `padding` | Logical screen pixels on each side of the viewport. | | `zoom` | Scale factor: `1` means 100%; clamped to the current camera limits. | Finite-canvas policy may constrain the requested camera center. Fitting preserves aspect ratio and chooses the smaller of the horizontal and vertical scales, within zoom limits. A rectangle or viewport with nonpositive width or height leaves the camera unchanged. ## Immediate navigation methods ```dart void navigateToPoint(DiagramPoint point, {double? zoom}) void navigateToRectangle(DiagramRect bounds, {double padding = 48}) bool navigateToElement(String elementId, {double padding = 48}) bool navigateToRegion(String elementId, String regionId, {double padding = 48}) bool navigateToSelection({double padding = 48}) ``` | Method | Required arguments | Optional arguments | Result | | --- | --- | --- | --- | | `navigateToPoint` | `point`: requested world center. | `zoom`: preserve current zoom when omitted. | `void`; centers the view without fitting content. | | `navigateToRectangle` | `bounds`: rectangle to frame. | `padding = 48`. | `void`; centers and fits the bounds. | | `navigateToElement` | `elementId`: existing resolved element ID. | `padding = 48`. | `true` when found; `false` if absent. Fits its resolved bounds. | | `navigateToRegion` | `elementId`, `regionId`: owner and declared layer ID. | `padding = 48`. | `true` when found; `false` if either is absent. Fits the world bounds of the transformed layer. | | `navigateToSelection` | None. | `padding = 48`. | `true` when selection bounds exist; `false` for no resolved selection. | `regionId` is a shape-layer definition ID, not a text story ID or text slot ID. Region navigation resolves layers on shapes and text elements; a frame has no region target through this method. Rotated regions are fitted using their axis-aligned world bounds. Every immediate navigation command cancels an active viewport animation, including an element or region lookup that returns `false`. A `true` result means the target was resolved; it does not guarantee that zoom limits permit a perfect fit. ## Coordinate conversion ```dart DiagramPoint screenToWorld(DiagramPoint screenPoint) DiagramPoint globalToWorld(Offset globalPosition) ``` | Method | Argument | Return | | --- | --- | --- | | `screenToWorld` | Point relative to the canvas's top-left, in logical pixels. | World point under that canvas position. | | `globalToWorld` | Flutter global pointer position, such as `details.globalPosition`. | World point after converting through the canvas render box and camera. | `globalToWorld` also throws `StateError` if the mounted canvas has not been laid out. Do not pass a global pointer coordinate to `screenToWorld` when the canvas is embedded below a header or beside a panel. ## `cameraState` ```dart DiagramCameraState? get cameraState ``` Returns the mounted camera snapshot, or `null` while detached. Reading it after disposing the session throws `StateError`. The immutable snapshot exposes: | Property | Type | Meaning | | --- | --- | --- | | `center` | `DiagramPoint` | World point at the viewport center. | | `zoom` | `double` | Current projection scale. | | `viewportSize` | `DiagramSize` | Laid-out canvas size in logical pixels. | | `minZoom` | `double` | Lower zoom limit; camera-state constructor default `0.25`. | | `maxZoom` | `double` | Upper zoom limit; camera-state constructor default `4`. | `DiagramCameraState.copyWith(...)` creates a snapshot; it does not update a session. Use navigation methods to change the mounted camera. Camera snapshots require finite centers, positive finite zoom and ordered zoom limits, and nonnegative finite viewport dimensions. Zero-sized viewports are allowed before layout. Invalid snapshots throw `ArgumentError` without changing state or notifying observers, including in release builds. ## Presentation listeners ```dart void addPresentationListener(VoidCallback listener) void removePresentationListener(VoidCallback listener) ``` | Member | Contract | | --- | --- | | `addPresentationListener` | Registers a no-argument callback for transient camera and animation changes. It can be registered before mounting. Throws after disposal. | | `removePresentationListener` | Removes the same callback instance; safely does nothing after disposal. | Read `session.cameraState` or `session.pathAnimations` inside the callback. Camera listeners run after the new camera state is published to the session, including layout, user navigation, and animation. Identical camera geometry does not notify. These listeners are separate from document events and undo/redo. ```dart void onPresentationChanged() { final camera = session.cameraState; if (camera != null) { final percentage = (camera.zoom * 100).round(); // Update the host's zoom indicator with percentage. } } session.addPresentationListener(onPresentationChanged); // During host cleanup: session.removePresentationListener(onPresentationChanged); ``` For gesture behavior, including touch pan and pinch, see [DrawingCanvas](/agent-docs/api/canvas.md#touch-navigation). For canvas bounds and grid configuration, see [Configuration](/agent-docs/api/configuration.md) and [Grids](/agent-docs/api/grids.md). ## Selecting inside groups Click an element in a group to select its group. Double-click it without modifiers to enter the group and select that child; subsequent clicks select children within that group. Press Escape to return to the containing group. **Cmd-click** (macOS) or **Ctrl-click** selects a child without entering its group; **Shift-click** adds or removes selection. Then drag, delete or edit the child normally without affecting siblings. Select a child before clicking its text to edit. On connection labels, Shift-click toggles the connector selection and Command/Ctrl-click selects the connector directly. These modified clicks do not start label editing. Use an unmodified double-click to edit the label. Outside text editing, modifier-clicking a checklist marker selects its text owner without checking or unchecking the item. An ordinary click still toggles it. --- # Output Both Flutter and pure Dart hosts use `session.exportSvg(options)` and `session.exportPng(options)`. Flutter supplies image loading and raster rendering. Pure Dart supplies SVG output with prepared resources; PNG requires a host `DrawingOutput` raster adapter and otherwise throws `UnsupportedError`. The playground includes an **Export diagram** menu for SVG and PNG downloads. Both are generated locally in the app. Exports contain the diagram bounds with padding, without selection handles, the minimap or other editor controls. Embedded widgets use their declared portable fallback. ## On this page | Category | What you can do | | --- | --- | | [Export PNG](#session-exportpng) | Capture the drawing as an image. | | [PNG options](#pngoptions) | Choose bounds, scale and background. | | [Export SVG](#session-exportsvg) | Export vector geometry and text. | | [Resources](#images-and-fonts) | Provide images and fonts for output. | | [Failures](#failures) | Handle unavailable or invalid resources. | ## `session.exportPng` ```dart Future exportPng( PngOptions options, { ImageProvider Function(DiagramImageSource)? imageProvider, }) ``` | Argument | Contract | | --- | --- | | `options` | Required positional crop, raster scale, background, and image-loading timeout. | | `imageProvider` | Optional resolver for authenticated or application-owned images. | The committed drawing snapshot is captured when called, before asynchronous image loading. Both sessions validate finite, positive crop dimensions and positive resource limits before invoking the output backend. PNG also requires a finite, positive scale and finite, positive scaled dimensions. Invalid options fail the returned future with an `ArgumentError`, without loading resources or changing the drawing. Custom backends receive the same validation; backend-specific allocation limits still apply. ## `PngOptions` ```dart const PngOptions({ required DiagramRect bounds, double pixelRatio = 1, DiagramColor? background, Duration resourceTimeout = const Duration(seconds: 30), int resourceConcurrency = 4, int maximumDecodedImageBytes = 256 * 1024 * 1024, int maximumOutputPixels = 64 * 1024 * 1024, }) ``` | Property | Default | Contract | | --- | --- | --- | | `bounds` | Required | Finite world-space crop with positive width and height. Negative world positions are allowed. | | `pixelRatio` | `1` | Finite, positive raster scale; does not reflow text. | | `maximumOutputPixels` | 64 × 1024 × 1024 | Positive limit on rounded output width × height. Oversized requests fail with `ArgumentError` before invoking the output backend. Hosts may configure this limit. Flutter’s renderer additionally caps output at 8192 pixels per side and 16 × 1024 × 1024 pixels overall; raising this option does not raise those renderer limits. It does not bound encoder or decoder peak memory. | | `background` | `null` | Transparent when omitted. | | `resourceTimeout` | 30 seconds | Positive image-loading timeout, not a deadline for the complete render. | | `resourceConcurrency` | `4` | Positive maximum number of simultaneous image loads. Failure or timeout stops queued loads. | | `maximumDecodedImageBytes` | 256 MiB | Positive budget for decoded source-image RGBA bytes retained during PNG output, deduplicated by URI. Exceeding it rejects export with `StateError`; images are never silently downsampled or omitted. Excludes decoder peaks, output pixels, recorded pictures and Flutter’s global cache. | Options are immutable. Validation occurs during export, not in the const constructor. ## `PngSnapshot` | Property | Type | Contract | | --- | --- | --- | | `bytes` | `Uint8List` | Copied, unmodifiable PNG bytes. | | `documentId` | `String` | Captured document identity. | | `revision` | `int` | Captured document revision. | | `textLayoutRevision` | `int` | Captured scene's text-layout revision. | | `bounds` | `DiagramRect` | Requested world crop. | | `width` | `int` | `ceil(bounds.width * pixelRatio)` pixels. | | `height` | `int` | `ceil(bounds.height * pixelRatio)` pixels. | ## Example Export a captured document without mounting a canvas: ```dart final snapshot = await session.exportPng( const PngOptions( bounds: DiagramRect.fromLTWH(0, 0, 1200, 800), pixelRatio: 2, background: DiagramColor(0xffffffff), ), ); // Hand these bytes to your application's save/share/download integration. final pngBytes = snapshot.bytes; ``` Bounds define a world-space crop; pixel ratio changes raster resolution without reflowing text. Background defaults to transparent. Empty documents use the requested background. Flutter limits output to 8192 pixels per side and 16 × 1024 × 1024 pixels overall, alongside the configured budget. Invalid or oversized requests fail before allocation. Headless adapters may impose different limits. The result contains immutable PNG bytes, pixel dimensions, world bounds, document ID, document revision and text-layout revision. Constructing a `PngSnapshot` validates the static PNG container, chunk checksums and ordering; malformed bytes throw `ArgumentError`, and dimensions that disagree with the encoded header throw `StateError`. This structural check does not decompress or validate the pixel stream; adapters must still supply a decodable PNG. The headless session checks custom adapter dimensions against the crop and scale (rounded up to whole pixels); a mismatch fails with `StateError`. It captures the current drawing snapshot when called, excluding uncommitted gesture previews. Later edits, navigation or session disposal do not retarget an in-flight export. Calling it after disposal fails. The same scene painter renders canonical shapes, connectors, labels, ports and clipping at full detail. Grid, selection, hover, text-entry placeholders and editor overlays are excluded. Runtime animation effects and arbitrary Flutter widget overlays are not canonical document content and are not included. ## Images and fonts Required images share the canvas image-resolution implementation. Export waits for them and rejects the first failed load immediately (even while other images remain pending), or a resource timeout (30 seconds by default), rather than painting a loading placeholder. A host can pass `imageProvider` to supply an authenticated or application-owned Flutter `ImageProvider` for each `DiagramImageSource`. Text must already have Flutter-shaped line and marker geometry in the captured scene. Normal `DrawingSession` instances provide this. Custom integrations must supply compatible text layout; see [text backends](/architecture/text-backends). Load application fonts before constructing or refreshing the session; export preserves the captured shaping, including any font fallback already present, and does not silently reshape it after an asynchronous font load. ## Failures | Condition | Result | | --- | --- | | Call after session disposal | Synchronous `StateError`. | | Invalid crop, scale, timeout, or excessive pixel allocation | Future fails with `ArgumentError`. | | Mismatched document and geometry or missing Flutter text geometry | Future fails with `StateError`. | | Image loading fails | Future propagates the loading failure. | | Image loading exceeds the timeout | Future fails with `TimeoutException`. | | PNG encoder returns no bytes | Future fails with `StateError`. | Rendering failures propagate. Temporary render resources are released on success or failure. For custom renderers that already own a resolved document, see [output composition](/architecture/output#exportdiagrampng). ## Output limits Exports capture a static drawing. Runtime animation and arbitrary native widget pixels are excluded; widgets use their portable content. The host owns file names, saving and sharing. PNG output is limited to the dimensions described above and does not tile oversized images. ## `session.exportSvg` ```dart Future exportSvg( SvgOptions options, { ImageProvider Function(DiagramImageSource)? imageProvider, }) ``` Captures the current drawing snapshot before asynchronous image loading. Later edits, navigation, and session disposal do not change the export. Calling after disposal throws `StateError`. This Flutter adapter uses the same image loader as the canvas and PNG export; the underlying scene codec remains pure Dart. | Argument | Contract | | --- | --- | | `options` | Required positional `SvgOptions`. | | `imageProvider` | Optional resolver for authenticated or application-owned images. Decoded images are embedded as PNGs. | ### `SvgOptions` | Property | Type / default | Contract | | --- | --- | --- | | `bounds` | Required `DiagramRect` | Finite, positive world-space crop. Does not reflow content. | | `resourceTimeout` | `Duration(seconds: 30)` | Positive deadline for image loading and encoding into embedded PNG resources; excludes final SVG serialization. | | `resourceConcurrency` | `4` | Positive maximum simultaneous image preparations. Each decoded image is released after embedding preparation; queued work stops on failure or timeout. Does not bound Flutter's global cache or encoded output bytes. | | `maximumDecodedImageBytes` | `256 * 1024 * 1024` | Positive decoded-source admission budget for the private SVG image cache. Exceeding it fails export with `StateError`; images are not replaced with placeholders. Decoder transients, independent handles, Flutter's global cache and encoded output bytes are outside this budget. | | `maximumResourceBytes` | `64 * 1024 * 1024` | Positive combined byte budget for all supplied font faces and prepared PNG resources, before base64 expansion. Images are counted once per URI. Exceeding it rejects with `StateError` before retaining the next encoded resource. Encoding transients, base64 strings and final SVG markup are outside this budget. | | `fonts` | Empty `Iterable` | Host-prepared faces, copied into an immutable list. The codec embeds referenced weight/style faces. If an exact face is unavailable, supplied family variants remain available for viewer fallback. | ### `SvgSnapshot` | Property | Type | Contract | | --- | --- | --- | | `svg` | `String` | Self-contained SVG source. The host owns saving/downloading. | | `documentId` | `String` | Captured document identity. | | `revision` | `int` | Captured document revision. | | `textLayoutRevision` | `int` | Captured scene text-layout revision. | | `bounds` | `DiagramRect` | Requested crop. | ```dart final snapshot = await session.exportSvg( SvgOptions(bounds: DiagramRect.fromLTWH(0, 0, 1200, 800)), ); final svgSource = snapshot.svg; ``` Invalid XML text rejects before image loading begins. Resource errors propagate immediately, even if another image is still pending; a loading deadline fails with `TimeoutException`. Temporary images are released on success or failure. No document commands or camera changes occur. Widget slots export their declared portable fallback, not a screenshot of the mounted widget. Native text inside those fallbacks is included. For a publication already captured by the host, the editor package exposes `exportDiagramSvg(publication, options: options, imageProvider: provider)` with the same result and lifetime contract. ## Pure Dart SVG export Import `package:vyuh_diagram_engine/vyuh_diagram_engine.dart` and use the same session export method: ```dart final result = await session.exportSvg( SvgOptions( bounds: DiagramRect.fromLTWH(0, 0, 1200, 800), ), ); // result.svg contains the SVG document. ``` For image content, pass `output: DrawingSvgOutput(resources: preparedResources)` when constructing the session. `DiagramSvgResources` contains prepared PNG images and font bytes. This adapter does not fetch or decode resources. Missing required images and unsupported content reject the export, including outside the crop. Resource preparation remains the host's responsibility. The result captures the committed document and text-layout revision before asynchronous export begins. Custom `DrawingOutput` adapters must return that captured document identity, document revision, text-layout revision and requested crop. The session rejects mismatched metadata with `StateError`, including when an adapter completes after the session changes or is disposed. Supported content includes shape outlines, fills and styled borders, pressure ink, region fills, dividers, ports, connector bodies, terminal markers, shadows and embedded PNG image layers. Owner transforms, intersecting ancestor clips and the canvas's separate border/port/marker passes are preserved. Native ``/`` output includes standalone text, shape stories, list markers and connector labels. It preserves captured line breaks, alphabetic baselines, text-region and ancestor clips, foreground colors, font sizes/families and run marks. Label badges retain fill, border and corner geometry in the final label pass. Runtime animations, grids, hover and selection are excluded. Unknown resolved element/outline implementations and XML-incompatible characters reject export. The SVG viewer shapes the native characters. Glyphs, font fallback, ligatures and spacing can differ from Flutter; this is **not** an outline-level or pixel-identical text snapshot. Export does not ask SVG to wrap paragraphs again. Justified lines use the captured line width with SVG `textLength`/spacing adjustment; the viewer's gap distribution may differ. Superscript/subscript retain the same OpenType `sups`/`subs` features used by the canvas, so support depends on the font. Pure-Dart bootstrap measurements remain approximate; provide a font-aware layout backend when exact host line breaks are required. PDF output is not implemented. ### Image resources `DiagramSvgScene.imageSources(scene)` validates supported scene content and returns an immutable, URI-deduplicated list of `DiagramImageSource` values to prepare. It performs no I/O and rejects unsupported content even outside the crop. ```dart final resources = DiagramSvgResources(images: { imageSource.uri: DiagramSvgImage(decodedPngBytes), }); final svg = DiagramSvgScene.encode(scene, bounds: crop, resources: resources); ``` | Object / property | Contract | | --- | --- | | `DiagramSvgImage(pngBytes)` | Snapshots static PNG bytes; rejects malformed containers, invalid dimensions, CRC errors, APNG and unknown critical chunks with `ArgumentError`. | | `image.width`, `image.height` | Intrinsic PNG pixel dimensions. | | `image.pngBytes`, `image.dataUri` | Immutable bytes and self-contained `data:image/png;base64,...` content. | | `DiagramSvgResources(images: ...)` | Immutable URI-to-image map. The codec does not fetch URLs or load files. | The producer must supply decodable PNG pixels; container validation does not inflate or validate the IDAT pixel stream. Decode/transcode other image formats before constructing a resource. The original URI is only an identity key and is never emitted as a remote SVG image reference. Declared contain, cover and fill behavior uses the same resolved layer rectangle and clipping. An image slot without an authored source retains the canvas's portable placeholder. Shadows use the declared color, local offset and Gaussian sigma before the shape fill, under the same owner and ancestor transforms. Independent SVG rendering has been checked for image fitting and shadows; exhaustive renderer and font parity across viewers remain open. ## Embedded font resources ```dart final face = SvgFont( family: 'IBM Plex Sans', bytes: fontBytes, format: SvgFontFormat.ttf, weight: 400, ); final snapshot = await session.exportSvg( SvgOptions(bounds: crop, fonts: [face]), ); ``` | Member | Contract | | --- | --- | | `SvgFont(family:, bytes:, format:, weight: 400, style: SvgFontStyle.normal)` | Immutable font byte snapshot with bounded container/header/table validation. Does not decode glyphs or compressed streams. | | `format` | `ttf`, `otf`, `woff`, or `woff2`; must match the supplied container. | | `family` | Nonempty name matching the authored `TextRun.fontFamily`. | | `weight` / `style` | Weight from 1 to 1000; style `normal` or `italic`. Supply separate faces as needed. | | `bytes` / `dataUri` / `cssRule` | Immutable bytes, embedded data URI and safely escaped `@font-face` declaration. | | `DiagramSvgResources(fonts: [...])` | Pure codec resource collection. Family matching and duplicate detection ignore casing; duplicate family/weight/style faces reject. | The host supplies valid font bytes it can embed; neither codec nor session fetches fonts. Without a supplied face, the viewer resolves the named family; runs without a family use `sans-serif`. Embedded font support varies by SVG consumer. The encoder escapes family names as literal CSS strings and never accepts raw CSS or remote font URLs. Image loading uses `imageProvider`; it does not resolve fonts. Implementation details for custom renderers are in [output architecture](/architecture/output). Applications export through `session.exportSvg` or `session.exportPng`. --- # Persistence and imports JSON persistence lives in `vyuh_diagram_codec_json`; SVG export lives in `vyuh_diagram_codec_svg`. Shared adapter contracts and output values belong to `vyuh_diagram_types`. Headless sessions depend on neither codec implementation. Import `vyuh_diagram_codec_json.dart` and supply `codec: const DiagramJsonCodec()` to `session.importJson(...)` or the headless `DrawingSession.fromJson(...)`. For another format, use `session.importDocument(payload, codec: adapter, expectedRevision: session.document.revision)`. Headless SVG export requires an explicit `output: DrawingSvgOutput()` from the SVG package when creating the session. JSON preserves authored content for reopening; SVG exports a resolved scene and does not provide a lossless document round trip. Save authored content with `session.document.encode()` and restore it with `session.importJson()`. The current document format is version 20; imports accept versions 6–20. Configure the same custom shape registrations when restoring their content. Version 20 preserves the optional connector `bundleKey`, an explicit compatibility key for collapsed-frame connection presentation. Older documents leave it unset. The key does not merge or replace the underlying connectors. Version 19 persists a frame's `isCollapsed` state while retaining its expanded geometry, descendants, and original connector bindings. Older documents start with their frames expanded. Version 18 adds persisted paths from explicit elbow rerouting. Older documents start without saved reroutes and retain normal endpoint routing. Version 17 adds editable path nodes and multi-stop linear/radial fills. Solid color payloads from older versions retain their appearance. Version 16 adds explicit point-anchor semantics for whole-shape connector bindings. Older documents retain outline binding behavior. Point anchors store normalized frame coordinates, so they remain attached through owner transforms. ```dart final json = session.document.encode(); ``` Camera state, grid settings, host callbacks and runtime path animations are session configuration or presentation, not saved document content. ## On this page | Category | What you can do | | --- | --- | | [Load a document](#import-into-an-existing-session) | Import JSON into a live session. | | [Handle rejection](#rejection-and-recovery) | Keep the existing document when an import fails. | | [Format migration](#core-format-migrations) | Understand supported persisted versions. | | [Custom migrations](#host-schema-migrations) | Upgrade application-defined data. | ## Import into an existing session To open saved JSON as a new session in Flutter or plain Dart: ```dart final session = DrawingSession.fromJson(json); ``` This constructor accepts the same configuration, registry, and policy arguments as `DrawingSession`, with JSON replacing `document:`. It validates initial content and starts with empty history. Malformed JSON throws `FormatException`. Supply the custom shape registry used by the document. For explicit host migrations, use `importJson` with `migrations` and `hostVersion` as described below. Capture the revision **before** waiting for a file or server response: ```dart final expectedRevision = session.document.revision; final json = await loadFile(); // Supplied by your application. final decision = session.importJson( json, expectedRevision: expectedRevision, ); if (!decision.allowed) { showMessage(decision.reason ?? 'Import was not allowed.'); } ``` For an already decoded document, call `session.replaceDocument(document, expectedRevision: ...)`. Replacement uses one canonical transaction. It validates the imported content against the session registry, consults `hooks.beforeChange`, clears the current selection and emits one completed change with `import` origin. Undo restores the previous document and selection; redo restores the imported document. Current camera and configuration remain in place. Imported revision numbers are normalized to the session's monotonic revision sequence. A different document ID represents a different identity scope. Even if both documents contain a shape named `shape-1`, the change delta reports removal from the old document and insertion into the new one. ## Rejection and recovery | Stage / condition | Result | Recovery | | --- | --- | --- | | `importJson`: malformed JSON, missing fields, invalid enum/value, or unsupported core version | `FormatException` | Correct the input or use a reader supporting that format. No live session has been changed. | | `DiagramDocumentMigrations` construction: incomplete, duplicate or out-of-range steps | `ArgumentError` | Fix the host migration chain before loading documents. | | `migrate`: host version outside the declared supported range | `FormatException` | Supply the appropriate host migration chain; core and host versions are separate. | | `migrate`: an upgrade callback throws | The original exception propagates; later steps do not run | Fix the failed upgrade. Intermediate immutable candidates are not published. External callback side effects cannot be rolled back by the library. | | `replaceDocument` / `importJson`: stale `expectedRevision` | `DiagramRevisionConflict` | Resolve the conflict or reload. Do not blindly retry with the newest revision. Publication and existing redo remain unchanged. | | Candidate fails registry or geometry validation | Validation exception | Correct the candidate or use its intended registry. The existing publication and history remain unchanged. | | `hooks.beforeChange` denies import | `DiagramPolicyDecision` with `allowed == false` | Display `reason` if provided. No replacement, completed-change event or history entry is added. | | Replacement during an owned pointer gesture | `StateError` | Commit or cancel the gesture first; import cannot take over its lease. | | Session already disposed | `StateError` | Use a live session. | `importJson` decodes before attempting replacement, so a malformed file reports a format error even if its captured revision is also stale. Host migrations are explicit: supply `migrations` and `hostVersion` together to `importJson`. The session decodes, upgrades the candidate, then validates and replaces it once. Omitting either argument while supplying the other throws `ArgumentError`. Successful replacement during text editing closes the old editor. A stale text client cannot mutate reused IDs in the imported document. Initial loading into a new `DrawingSession` validates its snapshot but does not emit an import operation or replay creation hooks. Authorizing access to the initial snapshot belongs to the host. ## Current boundaries This API replaces a complete snapshot. It does not merge two documents, remap pasted IDs or synchronize concurrent agents. Clipboard commands retain their separate merge semantics while using the same transaction authority. Core JSON migrations and explicit host document migration chains are supported, as described below. [PNG output and SVG scene output](/agent-docs/api/output.md) are available; full-content SVG and PDF output remain incomplete. Runtime animation bindings are not imported or reset by snapshot replacement; hosts that bind presentation to document identity should update those bindings from the import change event. ## Core format migrations The decoder upgrades a private parsed payload through consecutive version steps before constructing immutable model values. Current-format decoding does not reinterpret registry shape IDs. | Step | Meaning | | --- | --- | | 6 → 7 → 8 | Preserve existing fields; compatible optional fields retain their established defaults. | | 8 → 9 | Convert legacy connector text into labels with stable, collision-free attachment IDs. | | 9 → 10 | Add the historical badge presentation with padding that fits small labels. | | 10 → 11 | Normalize legacy `diamond` shapes to polygons, defaulting to four sides. | Version 11 preserves a custom registry type named `diamond` verbatim. Older files retain their historical polygon interpretation; they cannot distinguish a former built-in alias from an intended custom type using that same name. Versions 11 and 12 preserve that distinction. Malformed JSON fields, non-finite point coordinates, negative or non-finite frame and label dimensions, unsupported versions and unknown closed-grammar enum values throw `FormatException`. Registry-specific semantic validation remains at the session admission boundary. Unknown custom shape IDs are preserved by the codec and must exist in the restoring session's registry. ## Standalone text migration Version 12 writes `TextElement` as `kind: text`, with frame, rotation, parent, stacking order, required story, sizing mode, and content scroll offset. Run colors and other rich-text formatting are preserved. Shape-only fields are absent. Versions 6–11 migrate legacy `kind: shape, type: text` records into this distinct element. IDs, placement, story blocks, runs, marks, colors, sizing, and scroll remain intact. Previously suppressed decoration is removed; a rectangle fill is never converted into text color. Migration rejects legacy text with nonempty ports, owned children, bound connector endpoints, or a missing story. It does not silently orphan relationships or invent story identities. Resolve those incompatible records before importing. The current format also rejects the old shape representation and shape-specific fields on `kind: text` records. ## Object lock persistence Version 13 adds a required boolean `isLocked` to every shape, text element, and connector. Versions 6–12 migrate with `isLocked: false`. Copies preserve the flag unless explicitly changed; malformed flags in current-format JSON are rejected. The session enforces locks for new changes and inherited ownership. The inspector exposes a lock toggle and hides editing fields while locked. Undo/redo replays admitted history and still consults host admission. ## Host schema migrations `DiagramDocumentMigrations` upgrades a decoded document through a complete, immutable chain of host-defined steps. Host schema versions are independent of the core document format version: store your host version alongside the diagram in your application's persistence envelope. | API | Arguments | Contract | | --- | --- | --- | | `DiagramDocumentMigration(...)` | `fromVersion: int`, `upgrade: DiagramDocument Function(DiagramDocument)` | Upgrades one host version to `fromVersion + 1`. | | `DiagramDocumentMigrations(...)` | `minimumSupportedVersion: int`, `currentVersion: int`, `steps: Iterable` | Copies and validates the complete chain. Rejects gaps, duplicates and steps outside the supported range. Versions must satisfy `0 <= minimum <= current`. | | `migrate(document, fromVersion: ...)` | Immutable decoded document and its persisted host version | Runs steps in version order. Unsupported versions throw `FormatException`; callback failures propagate and stop subsequent steps. Current-version input is returned unchanged. | ```dart final migrations = DiagramDocumentMigrations( minimumSupportedVersion: 1, currentVersion: 3, steps: [ DiagramDocumentMigration(fromVersion: 1, upgrade: upgradeV1ToV2), DiagramDocumentMigration(fromVersion: 2, upgrade: upgradeV2ToV3), ], ); final expectedRevision = session.document.revision; final saved = await loadHostEnvelope(); // Host-owned storage and envelope. final decision = session.importJson( saved.diagramJson, migrations: migrations, hostVersion: saved.schemaVersion, expectedRevision: expectedRevision, ); ``` Migration creates candidate immutable values; it does not publish intermediate versions or add history. The final `replaceDocument` still checks the expected revision, registry, geometry and host admission hook. Persist the current host version only with a successfully accepted and saved document. Upgrade callbacks must deterministically preserve or remap identities, ownership, ports, stories and connections. Avoid host I/O: it cannot be rolled back. Upgrades run after core decoding; they do not repair malformed JSON, replace live registries, merge concurrent edits or infer schema changes. `ShapeElement.copyWith(type: newType)` preserves the remaining authored fields when a host migration renames a registered type. It constructs a candidate only; the destination registry must admit its text regions, values, ports and geometry. See the runnable [host migration example](/agent-docs/examples/migrations.md). ## Label borders Version 14 adds badge `stroke`, `strokeWidth`, and `strokeStyle`. Versions 6–13 migrate with a zero-width border, retaining existing appearance. Current-format badge fields are required and validated. Plain labels cannot carry decoration. Older readers reject version 14 rather than silently discarding label borders. ## Shape values Version 15 requires a `values` object on every shape, empty when unused. It persists with history and copies. Values support null, booleans, strings, finite numbers, lists and string-keyed maps. Nested data is copied and frozen; widgets and callbacks cannot be serialized. Versions 6–14 migrate shapes with empty values. Current-format files with missing, null or non-object `values` are rejected, as are non-finite numeric values. The codec preserves custom keys without interpreting them; the restoring registry and session admission own their semantic validation. Text elements do not carry shape values. --- # DrawingSession `DrawingSession` is the application API for reading and editing a drawing. Content commands work without a canvas; camera and visual-effect commands use the mounted `DrawingCanvas`. Use session methods to create elements, change their appearance, connect them, and manage history. See [sessions and collaboration](/agent-docs/api/collaboration.md) for sharing content and the participant model. ## On this page | Category | What you can do | | --- | --- | | [Create a session](#constructors) | Start a drawing and configure its capabilities. | | [Read content](#document-access) | Inspect the current immutable document. | | [Observe state](#session-properties) | Read selection, configuration and view information. | | [Edit content](#editing-commands) | Create, style, transform, connect and remove elements. | | [Observe changes](#listeners) | Subscribe to content and presentation updates. | | [Move the view](#navigation-and-animation) | Navigate and animate a mounted canvas. | | [Dispose safely](#lifetime-and-errors) | Release the session and handle rejected operations. | ## Constructors Start with `DrawingSession()` for an empty drawing. Pass `document:` to load or seed content. Flutter supplies text layout automatically. Use `DrawingSession.fromJson(json)` to open saved JSON directly. It accepts the same optional constructor settings and validates the saved content without creating an undo entry. See [persistence](/agent-docs/api/persistence.md) for loading into an existing session and applying host migrations. ### Flutter and plain Dart This page documents the Flutter kit's `DrawingSession`, including canvas APIs. For headless Dart, `vyuh_diagram_engine` provides the shared content API, layout, commands and transactions without Flutter. See [package ownership](/architecture/#package-ownership). Import `package:vyuh_diagram_kit/vyuh_diagram_kit.dart` in Flutter, or `package:vyuh_diagram_engine/vyuh_diagram_engine.dart` on the Dart VM. Both provide `DrawingSession` with the same content commands, immutable snapshots, selection, history, listeners, and JSON import/save API. ```dart final session = DrawingSession(); final id = session.createShape( type: Shapes.rectangle, worldCenter: DiagramPoint.zero, ); session.moveElements([id], const DiagramPoint(40, 20)).requireAllowed(); final json = session.document.encode(); session.dispose(); ``` When omitted, `document` defaults to a new empty document with a unique ID. Each session owns its own independent drawing. Supply `DiagramDocument.empty('your-id')` when your application assigns document IDs. Both constructors accept a nonempty `participantId` (default `local`). Read it through `session.participantId`; admission proposals and completed changes retain that identity. It is attribution, not authentication. Borrowing a session retains its identity and shared selection/history; it does not attach an independent user. Both hosts accept `DrawingConfiguration(supportedFonts: ..., defaultFontFamily: ...)` and update it through `session.setConfiguration(...)`. Flutter extends these portable defaults with canvas and widget settings. The plain Dart constructor accepts `beforeChange` and an optional `textLayout` backend. Supply a font-aware backend for accurate text geometry. Flutter adds mounted navigation, visual effects and image loading. Headless export supports prepared-resource SVG and PNG through a host raster adapter. The constructor below is the Flutter variant. ```dart DrawingSession({ DiagramDocument? document, String participantId = 'local', DrawingConfiguration? configuration, ShapeRegistry? shapeRegistry, DiagramTextLayoutEngine? textLayout, DiagramToolRegistry? tools, ElementCreationPolicy? elementCreationPolicy, DiagramConnectionPolicy? connectionPolicy, DiagramDocumentValidator? documentValidator, DiagramTool? activeTool, }) ``` | Argument | Type | Default / behavior | | --- | --- | --- | | `document` | `DiagramDocument` | A new empty drawing with a unique ID when omitted. | | `configuration` | `DrawingConfiguration?` | `DrawingConfiguration()` when omitted. | | `shapeRegistry` | `ShapeRegistry?` | Resolver uses `ShapeRegistry.standard()` when omitted. | | `textLayout` | `DiagramTextLayoutEngine?` | `FlutterDiagramTextLayoutEngine()` when omitted. | | `tools` | `DiagramToolRegistry?` | Uses `DiagramToolRegistry.standard()` when omitted. Supply a custom-only registry to replace the defaults; its `defaultTool` is initially active unless `activeTool` is supplied. Activating an unregistered tool throws `StateError` without changing the drawing or current tool. | | `elementCreationPolicy` | `ElementCreationPolicy?` | Receives the prepared `ShapeElement` or `TextElement` with defaults and identity already supplied. Returns an allow/deny decision before insertion. | | `connectionPolicy` | `DiagramConnectionPolicy?` | Optional rules for connecting elements. | | `documentValidator` | `DiagramDocumentValidator?` | Fixed domain invariant checked for initial content and every changed document, including imports and history. Use hooks for replaceable application permissions. | | `availableTools` | Registered tool definitions | Build tool controls from the session’s registered tools. | | `activeTool` | `DiagramTool?` | Explicit value, otherwise supplied `tools.defaultTool`, otherwise `DiagramTools.select`. | ## Refresh text layout Call `session.refreshLayout()` after updating your host text backend's font environment or layout revision. It refreshes derived geometry for the current preview and committed snapshot without changing content or adding undo entries. A shaping failure leaves both publications unchanged. Previously captured export snapshots retain their original geometry. Mounted Flutter canvases observe font loading automatically. Headless hosts can use this method after preparing fonts; it does not download fonts itself. ## Document access Document reads use the last committed immutable snapshot. `session.document` provides these lookups: | Member | Result | Behavior | | --- | --- | --- | | `elements` | `List` | Immutable authored order. | | `elementsById` | `Map` | Immutable ID lookup into this snapshot; missing IDs return `null`. | | `elementById(String id)` | `DiagramElement?` | Any element with that ID, or `null`. | | `shapeById(String id)` | `ShapeElement?` | A shape with that ID, otherwise `null`. | | `connectorById(String id)` | `ConnectorElement?` | A connector with that ID, otherwise `null`. | | `textById(String id)` | `TextElement?` | Standalone text with that ID, otherwise `null`. | Keeping a document or lookup retains that snapshot, not a live view of later edits. Read the session's current document after a change. Apply edits through session commands; assigning or removing entries from `elementsById` is rejected. An element's saved frame describes its authored size. Text that grows to fit its content can have larger visible bounds. Read `session.snapshot.scene.elementById(id)?.bounds` for the resolved visible bounds; use the same captured snapshot when combining content and geometry. Reopening a document recomputes those bounds with the configured text backend. While an edit is being previewed, the canvas shows the preview but `document`, `scene`, and `snapshot` retain the committed content and matching geometry. JSON saving and PNG/SVG exports therefore exclude an edit that may still be cancelled. Committing the edit makes its content available through these APIs. ## Session properties ### Content and geometry | Property | Type | Meaning | | --- | --- | --- | | `snapshot` | `DrawingSnapshot` | Immutable revision-matched document and resolved geometry. | | `document` | `DiagramDocument` | Immutable authored content; use `document.encode()` to save JSON. | | `scene` | `ResolvedScene` | Committed geometry matching `document`. | | `text` | `TextEditing` | [Text formatting, lists and story replacement](/agent-docs/api/text.md#text-commands), for the drawing. Retained commands reject calls after session disposal. | ### Selection and tools | Property | Type | Meaning | | --- | --- | --- | | `selection` | `DrawingSelection` | Current selection. | | `activeTool` | `DiagramTool` | Current registered tool. | | `canAlign`, `canDistribute`, `canReorder`, `canRemoveFrames` | `bool` | Availability for the current selection. | ### Configuration and history | Property | Type | Meaning | | --- | --- | --- | | `configuration` | `DrawingConfiguration` | Current drawing configuration and change hook. | | `canUndo`, `canRedo` | `bool` | Whether the respective history operation is available. | ### Mounted view and animations | Property | Type | Meaning | | --- | --- | --- | | `cameraState` | `DiagramCameraState?` | Mounted camera state, or `null` when detached. | | `pathEffects` | `DiagramPathEffectRegistry` | Current configuration's effect registry. | | `pathAnimations` | `Map` | Active animations keyed by animation ID; empty when detached. | ### Lifecycle status | Property | Type | Meaning | | --- | --- | --- | | `isDisposed` | `bool` | Whether `dispose()` has completed. | | `isAttached` | `bool` | Whether a canvas currently claims this session. | Except for `isDisposed` and `isAttached`, these getters require a live session. ## Editing commands Commands below act on the current document through shared validation and history. Each category links to its detailed reference where additional arguments are needed. ### Create elements | Signature | Returns | Behavior | | --- | --- | --- | | `addElement(DiagramElement element, {bool select = true})` | `DiagramPolicyDecision` | Insert an authored record without factory defaults; see [shapes](/agent-docs/api/shapes.md). | | `createShape({required String type, required DiagramPoint worldCenter, ...})` | `String` | Materialize registry defaults and return the new shape ID; [full arguments](/agent-docs/api/shapes.md#session-createshape). | | `createText({required DiagramPoint worldCenter, ...})` | `String` | Create standalone text; [full arguments](/agent-docs/api/shapes.md#session-createtext). | | `insertConnectedElement({required source, required element, ...})` | `({String elementId, String connectorId})` | Creates a shape or standalone text and its incoming connection together; named arguments are documented in [creation](/agent-docs/api/shapes.md#session-insertconnectedelement). | ### Document replacement and transactions | Signature | Returns | Behavior | | --- | --- | --- | | `dispatch(DiagramTransaction transaction, {int? expectedRevision})` | `DiagramPolicyDecision` | Submits one atomic edit through session admission. Supply the revision captured before asynchronous work to reject a stale edit with `DiagramRevisionConflict`, before evaluating its steps. | | `replaceDocument(DiagramDocument document, {required int expectedRevision})` | `DiagramPolicyDecision` | Revision-checked replacement; see [persistence](/agent-docs/api/persistence.md). | | `importJson(String json, {required int expectedRevision, DiagramDocumentCodec codec, DiagramDocumentMigrations? migrations, int? hostVersion})` | `DiagramPolicyDecision` | Loads JSON with the kit's default codec or a supplied codec. Supply migrations and hostVersion together. Invalid input leaves the document unchanged. | ### Undo and redo | Signature | Returns | Behavior | | --- | --- | --- | | `undo()` | `DiagramPolicyDecision` | Requests undo through session history. | | `redo()` | `DiagramPolicyDecision` | Requests redo through session history. | ### Selection | Signature | Returns | Behavior | | --- | --- | --- | | `selectElements(Iterable elementIds)` | `void` | Replaces object selection; missing and non-selectable elements are filtered by the admitted scene. | | `selectAll({String? ownerId})` | `void` | Selects every selectable element, or only direct children of the supplied owner. Includes connectors. | | `clearSelection()` | `void` | Clears selection. Selection commands are transient, preserve document/scene and create no undo entries; they work before mounting. | | `setSelection(selection)` | `void` | Applies a typed selection, filtering it against the current drawing. | ### Connections and labels Create connections, adjust their routing and appearance, and edit their labels without mounting a canvas. See [connectors](/agent-docs/api/connectors.md) for endpoint fields and admission rules. | API | Contract | | --- | --- | | `connect(sourceId, targetId, {sourcePortId, targetPortId, router, select = true})` | Connects existing elements, assigning and returning a fresh connector ID. Ports are optional; routing defaults to straight. | | `createConnector(connector, {select = true})` | Inserts a validated connector; returns its ID. | | `setConnectorStyle(id, {color, strokeWidth, strokeStyle, startArrowhead, endArrowhead})` | Changes supplied appearance fields in one undo step; returns an admission decision. | | `setConnectorRouter(id, router)` | Changes route kind and clears route controls in one undo step; returns an admission decision. | | `setConnectorRouting(id, {portSpacing, portExtension, cornerRadius})` | Changes routing dimensions in one undo step. Extension and rounding require an elbow route. | | `reconnect(id, {required atStart, required endpoint})` | Changes one endpoint with connection policy and transaction admission. | | `addConnectorLabel(id, {text, story, fraction = .5, offset = DiagramPoint.zero, size})` | Adds a definition-admitted label; returns its ID. Supply plain `text` or a formatted `story`, never both. | | `updateConnectorLabel(id, labelId, {story, fraction, offset, size, presentation})` | Updates specified label fields through shared admission and history. | | `selectConnectorLabel(id, labelId)` / `removeConnectorLabel(id, labelId)` | Selects or deletes one owned label. | ### Preview, commit and cancel Use these controls when building a custom gesture. Ordinary edits can call the content commands directly. | API | Contract | | --- | --- | | `beginEdit(label)` | Advanced transaction editing: returns a `DiagramInteractionLease` with preview, commit and cancel. Finish the lease before another content operation. Session disposal cancels its active lease and rolls back its preview, including when borrowing a source session. It does not cancel another owner's successor lease. | | `isInteracting` / `cancelInteraction()` | Queries or cancels the current interaction, restoring its baseline. | ### Grouping and frames | Signature | Returns | Behavior | | --- | --- | --- | | `canGroup` / `canFrame` / `canUngroup` | `bool` | Checks selection ownership and registered container availability, or removal capability for selected containers. Ungrouping and `canRemoveFrames` also reject locked direct children whose ownership would change. Host admission runs when executing. | | `groupSelection()` | `DiagramPolicyDecision` | Groups at least two objects with the same owner, using admitted selection bounds and the registered group minimum, default style and value fields. Works without a mounted canvas. | | `frameSelection()` | `DiagramPolicyDecision` | Wraps selected sibling objects in a frame without moving them. Accepts a single object. | | `ungroupSelection()` | `DiagramPolicyDecision` | Removes selected groups or frames while retaining children, including collapsed contents. Nested removals reparent to the first surviving owner, preserving geometry. | | `removeFrames()` | `DiagramPolicyDecision` | Removes selected frame containers while preserving their contents. | Group boundaries automatically enclose their current children, including during drag previews and after undo or redo. Nested groups update from the inside out. The boundary is a rectangle in the group’s orientation, subject to its registered minimum size. Frames keep their authored size. ### Clipboard and duplication | Signature | Returns | Behavior | | --- | --- | --- | | `copySelection()` | `DiagramClipboardPayload` | Captures selected objects, owned descendants, and connectors whose two bound targets are included, as an immutable payload. Crossing connectors are excluded unless explicitly selected or owned by a captured container. The host owns clipboard storage. | | `paste(DiagramClipboardPayload payload, {DiagramPoint offset = const DiagramPoint(24, 24)})` | `DiagramPolicyDecision` | Allocates fresh identities and inserts through admission as one undo unit. Selects copied selection identities on success. | | `duplicateSelection({DiagramPoint offset = const DiagramPoint(24, 24)})` | `DiagramPolicyDecision` | Copies and pastes selected objects without changing the host clipboard. | `paste()` assigns fresh IDs to copied elements and remaps internal references. External bindings survive only within the same document when their targets still exist. Otherwise, external parents are cleared and endpoints use their captured positions plus the paste offset. Independent drawings need distinct document IDs. `DrawingSession()` assigns one automatically; pass an existing document when reopening a saved drawing. ### Appearance and custom content | Signature | Returns | Behavior | | --- | --- | --- | | `setImageSource(String elementId, DiagramImageSource? source)` | `DiagramPolicyDecision` | Sets or removes authored image content without mounting. Target must declare an image visual; `null` removes the source. [Full contract](/agent-docs/api/shapes.md#image-content). | | `updateValues(String elementId, Map patch)` | `DiagramPolicyDecision` | Patches registered shape fields through admission and undo; [values and validation](/agent-docs/api/shapes.md#custom-values). | | `setShapeStyle(String elementId, ShapeStyle style)` | `DiagramPolicyDecision` | Replaces editable appearance without changing text or geometry; [style contract](/agent-docs/api/shapes.md#shape-appearance). | | `setShapeStyles(Map styles)` | `DiagramPolicyDecision` | Replaces several appearances atomically. All targets must pass admission; one changed batch produces one undo entry. | | `beginValueEdit(String elementId)` | `DiagramShapeValueEdit` | Starts a bounded preview/commit edit for a slider or similar control; [full lifecycle](/agent-docs/api/shapes.md#continuous-control-edits). | ### Locking and deletion | Signature | Returns | Behavior | | --- | --- | --- | | `setElementsLocked(Iterable ids, bool locked)` | `DiagramPolicyDecision` | Locks or unlocks existing objects in one admitted history operation. Missing IDs deny the complete command. Locked owners protect descendants. | | `canDeleteSelection` | `bool` | Checks deletion capabilities for selected objects and their dependants, or the selected connector label. Host admission runs when executing the edit. | | `deleteSelection()` | `DiagramPolicyDecision` | Deletes selected objects or a selected connector label through the engine. Does not delete text-editing content. | | `removeElements(Iterable elementIds)` | `DiagramPolicyDecision` | Removes objects, descendants and attached connectors as one undo unit. A missing requested ID or protected descendant denies the entire operation. Works without a mounted canvas. | ### Position, rotation and arrangement | Signature | Returns | Behavior | | --- | --- | --- | | `moveElements(Iterable ids, DiagramPoint delta)` | `DiagramPolicyDecision` | Translates explicit roots and their owned contents once in world units, without changing selection. Duplicate and nested IDs do not double-move children. Unknown IDs deny the entire operation; non-finite deltas throw `ArgumentError`. | | `align(DiagramAlignment alignment)` | `DiagramPolicyDecision` | Aligns the current selection. | | `rotateElements(Iterable ids, {required DiagramPoint pivot, required double radians})` | `DiagramPolicyDecision` | Applies a relative rotation around a world-space pivot to roots and owned contents once. Preserves selection, local sizes and bound endpoint identities; rotates free endpoints and curve controls. Unknown IDs deny atomically; non-finite inputs throw `ArgumentError`. | | `resizeElements(Iterable ids, DiagramSize size, {bool fromCenter = false, bool preserveAspectRatio = false})` | `DiagramPolicyDecision` | Resizes roots and their owned contents once, preserving selection and bindings. A single framed root uses its rotated local axes; multiple roots use world selection bounds. Keeps the opposite top-left corner fixed, or the center when requested. Clamps through all registered member minima. Locked or non-resizable members and unknown IDs deny atomically; non-finite or non-positive sizes throw `ArgumentError`. Text may grow vertically to fit its content. | | `distribute(DiagramDistribution distribution)` | `DiagramPolicyDecision` | Distributes the current selection. | | `reorder(DiagramLayerOrder order)` | `DiagramPolicyDecision` | Changes selection stacking in the document. | ### Tools and configuration | Signature | Returns | Behavior | | --- | --- | --- | | `setActiveTool(DiagramTool tool)` | `void` | Selects an active tool for subsequent interaction. | | `setConfiguration(DrawingConfiguration value)` | `void` | Applies behavior and presentation settings without replacing document history. | Every command applies the drawing’s validation and permissions. Use the corresponding `can…` getter to enable arrangement controls. ### PNG export ```dart Future exportPng( PngOptions options, { ImageProvider Function(DiagramImageSource)? imageProvider, }) ``` Captures the current publication and exports it asynchronously. `options` is required; `imageProvider` optionally supplies Flutter image providers. A mounted canvas is not required. Once started, an export survives later edits or session disposal. See [output](/agent-docs/api/output.md) for options and resource contracts. ### SVG export ```dart Future exportSvg( SvgOptions options, { ImageProvider Function(DiagramImageSource)? imageProvider, }) ``` Captures the same publication and lifetime contract as PNG. Produces vector geometry with embedded image resources and declared widget fallbacks. Native text retains resolved line positions and styles, with optional embedded font faces. Glyph shaping depends on the SVG viewer. See [output](/agent-docs/api/output.md#session-exportsvg) for the option and snapshot tables, loading deadlines, and supported content. ## Listeners | Signature | Returns | Contract | | --- | --- | --- | | `addListener(VoidCallback listener)` | `void` | Notifies when session properties change; read `document`, `selection` or `activeTool`. Duplicate registration through the session is ignored. | | `removeListener(VoidCallback listener)` | `void` | Removes a registration made through this session; safe after disposal. | | `addDocumentChangeListener(DiagramDocumentChangeListener listener)` | `void` | Observes committed document changes, including undo/redo. Ignores camera and selection updates; duplicate registration is ignored. | | `removeDocumentChangeListener(DiagramDocumentChangeListener listener)` | `void` | Removes this session's content subscription; safe after disposal. Session disposal removes remaining owned subscriptions. | | `addPresentationListener(VoidCallback listener)` | `void` | Observes transient camera and path-animation changes separately from content. | | `removePresentationListener(VoidCallback listener)` | `void` | Removes a presentation listener; no-op after disposal. | `VoidCallback` is `void Function()`. Internal editor state is not exposed by session notifications. `DiagramDocumentChangeListener` is `void Function(DiagramDocumentChange change)`. Content listeners and `configuration.hooks.onChange` observe the same canonical completed events. Listener subscriptions survive configuration replacement and are independently removable; the hook is the configuration-owned callback. ## Navigation and animation These methods require a mounted canvas. See [navigation](/agent-docs/api/navigation.md) and [animation](/agent-docs/api/animation.md) for detailed options. ### Coordinate conversion | Signature | Returns | Behavior | | --- | --- | --- | | `screenToWorld(DiagramPoint screenPoint)` | `DiagramPoint` | Converts canvas-local screen coordinates to world coordinates. | | `globalToWorld(Offset globalPosition)` | `DiagramPoint` | Converts a Flutter global position through the laid-out canvas. | ### Immediate navigation | Signature | Returns | Behavior | | --- | --- | --- | | `navigateToPoint(DiagramPoint point, {double? zoom})` | `void` | Centers on a world point; omitted zoom preserves zoom, supplied zoom is clamped to camera limits. | | `navigateToRectangle(DiagramRect bounds, {double padding = 48})` | `void` | Fits world bounds with viewport padding. | | `navigateToElement(String elementId, {double padding = 48})` | `bool` | Fits the element; `false` if missing. | | `navigateToRegion(String elementId, String regionId, {double padding = 48})` | `bool` | Fits the shape region's world bounds; `false` if unresolved. | | `navigateToSelection({double padding = 48})` | `bool` | Fits selection bounds; `false` when no bounds are available. | | `stopViewportAnimation()` | `void` | Cancels the current camera animation. | ### Connection path animations | Signature | Returns | Behavior | | --- | --- | --- | | `startPathAnimation(DiagramPathAnimation animation)` | `bool` | Adds/replaces the animation by ID; `false` for invalid animation, unregistered kind, or missing connector. | | `stopPathAnimation(String animationId)` | `bool` | Removes the animation; `false` if the ID is absent. | | `clearPathAnimations()` | `void` | Removes all active path animations. | ### Animated navigation Every method returns `Future`. Each has these additional named parameters: | Parameter | Type | Default | | --- | --- | --- | | `duration` | `Duration` | `const Duration(milliseconds: 450)` | | `curve` | `Curve` | `Curves.easeInOutCubic` | | Method | Required positional arguments | Other named parameters | | --- | --- | --- | | `animateToPoint` | `DiagramPoint point` | `double? zoom` | | `animateToRectangle` | `DiagramRect bounds` | `double padding = 48` | | `animateToElement` | `String elementId` | `double padding = 48` | | `animateToRegion` | `String elementId, String regionId` | `double padding = 48` | | `animateToSelection` | None | `double padding = 48` | The future resolves to `true` on completion and `false` for a missing target/selection or cancellation. Starting a new camera animation cancels the previous one. Immediate navigation also cancels the current camera animation. Reduced-motion behavior or a nonpositive duration applies the target immediately and returns `true`. ## Lifetime and errors Identity-returning commands throw `DiagramChangeDenied` for policy denial. For decision-returning commands, inspect `allowed` or call `requireAllowed()` to use that same exception path. A decision has nullable `code` and `reason`: success has neither; denial has a stable `DiagramDenialCode` and optional human detail. The categories are `policy`, `locked`, `unavailableTarget`, `invalidSelection`, `unsupportedOperation` and `conflict`. Host policies using `deny(reason)` default to `policy`; `denied(code, reason: ...)` sets a category. These are policy results, not a replacement for invalid-input/lifecycle errors. ### Connected insertion migration `createConnectedElement` is deprecated. Use `insertConnectedElement` with the same arguments and read `.connectorId` from the returned record where an older caller expected a string. `.elementId` is also available. The insertion remains atomic, preserves the supplied element and uses the same admission/history path. | Condition | Result | | --- | --- | | `replaceDocument` / `importJson` expected revision differs from the current document revision | `DiagramRevisionConflict`. | | Shape creation is denied by creation, connection, or document-admission policy | `DiagramChangeDenied`, retaining the decision and reason. | | Requested creation size is non-finite or nonpositive | `ArgumentError`. | | Connected creation specifies an invalid target port, omits a required port, or reuses a connector ID | `StateError`. | | Call a live-session API after disposal | `StateError`. Listener removal and repeated `dispose()` remain safe. | | Mount a second canvas on the same session | `StateError`; each simultaneous canvas needs a separate session. | | Call a mounted presentation method while detached | `StateError`. | | `globalToWorld` before the canvas is laid out | `StateError`. | | `setImageSource` targets a missing shape or a shape without an image visual | Denied `DiagramPolicyDecision`; document unchanged. | | Image source is neither an HTTP(S) URL nor an image data URI | `ArgumentError`. | | Flutter adapter supplies a non-null admission hook different from its source session's hook | `ArgumentError` from construction or `setConfiguration`. | | Call `dispose()` while the canvas is still mounted | `StateError`; unmount first. | `void dispose()` releases the session's subscriptions and presentation resources. For a Flutter adapter created with `DrawingSession.fromSession(source)`, the source drawing, history and source-owned subscriptions remain available until the source itself is disposed. Unmounting and remounting without disposal also retains content and history. Runtime settings and path animations are not document content. Subscriptions owned by a session stop as soon as its teardown starts. Canceling its preview can still notify other owners observing the same drawing. Repeated `dispose()` calls are safe; other live-session calls reject during teardown. If disposal is rejected during notification delivery, the session remains usable and disposal can be retried after notification finishes. Capture `session.snapshot` before disposal if you need to retain the document and its matching geometry. The same disposal method applies in pure Dart. See [Architecture](/architecture/ownership) for adapter ownership details. ## Example ```dart final session = DrawingSession( document: DiagramDocument.empty('my-drawing'), configuration: DrawingConfiguration( grid: DrawingGridConfiguration(visible: true), ), ); void onChange() { final document = session.document; // Persist with document.encode() or update host UI from document. } session.addListener(onChange); // Return this widget inside a bounded layout: final canvas = DrawingCanvas(session: session); // After the canvas is mounted and laid out: await session.animateToSelection(); // After removing the canvas from the widget tree: session.removeListener(onChange); session.dispose(); ``` Use `createConnector`, `reconnect`, and the connector-label methods on the session for ordinary connection editing. `setSelection` accepts a typed immutable selection. Use `session.document.encode()` to save JSON; `session.snapshot` captures the revision-matched document and geometry. The session does not expose its mutable engine. Paragraph alignment applies only to ordinary paragraphs. Bulleted lists, numbered lists, and checklists remain left-aligned in their region, including nested items. Markers reserve their measured width plus a half-em gap before the item text. Arrangement commands return denial for insufficient selection or protected frame removal, and return the host admission decision for edits. An already-satisfied alignment or distribution succeeds without publishing a document change or adding history. Frame removal unwraps children and requires every selected frame to be deletable. ## Named actions (Flutter kit) These methods are provided by `DrawingSessionActions` in the kit, including the small `drawing.dart` entry point. They are not part of the plain-Dart session. | Method | Result | Behavior | | --- | --- | --- | | `canInvokeAction(id, {elementId, slotId})` | `bool` | Checks current availability without staging edits. | | `invokeAction(id, {elementId, slotId})` | `DiagramPolicyDecision` | Executes synchronous staged changes as one atomic Undo operation. | An omitted target uses the current single-object or text selection. Unknown actions return `unsupportedOperation`; missing targets return `unavailableTarget`; locked targets return `locked`; disabled actions return `policy`. Use `requireAllowed()` for the same exception path as creation commands. The context offers `updateValues`, `resize`, and `updateShape` for target edits; `replace`, `insert`, `remove`, and `select` handle advanced changes. It expires after the callback. See the [complete library recipe](/agent-docs/guide/build-a-library.md) and [action contract](/agent-docs/guide/actions.md). --- # Shapes, elements and registries Describe a shape through these related concepts: | Concept | What it describes | | --- | --- | | **Geometry** | Frame, contour and paths, including the geometry used for clipping and hit testing. | | **Content layout** | Title, body and footer regions, arranged using rows, columns, stacks, sizing and padding. | | **Appearance** | Fills, stroke styles, widths and colors; text has its own formatting properties. | | **Connections** | Explicit named ports or virtual anchors, attachment rules and connection snapping. | | **Alignment snapping** | Geometric features that help align a moving shape with nearby shapes. | Connection snapping chooses where a connector attaches. Explicit ports provide named anchors; portless shapes offer virtual anchor candidates and allow other attachment positions according to their connection policy. The saved binding keeps the connector attached as the shape changes. See [connection anchors](/agent-docs/api/connectors.md). Alignment guides serve a different purpose: they align shapes while moving them. Currently they use the sides and centers of nearby resolved element bounds, subject to the snap-guide policy and candidate limit. Internal title/body/footer regions and ports do **not** currently contribute separate alignment candidates. Selectable children contribute as document elements; composition parts do not. A future declaration of snappable internal regions should reference their shared resolved geometry, rather than introduce independently calculated guide positions. This is an extension point to develop, not an available per-part snapping API. See [shape composition](/agent-docs/guide/shape-composition.md) for reusable content primitives and [widgets inside shapes](/agent-docs/api/widgets.md) for live Flutter content. ## On this page | Category | What you can do | | --- | --- | | [Create elements](#session-createshape) | Create registered shapes, standalone text or connected elements. | | [Appearance](#shape-appearance) | Set fills, strokes and other supported styles. | | [Custom definitions](#shapedefinition) | Describe geometry, content, defaults and capabilities. | | [Register definitions](#shaperegistry) | Make custom shape types available to the drawing. | | [Edit and lock](#mutation-and-locking) | Update elements while respecting editing capabilities. | | [Images](#image-content) | Set or clear image content. | | [Custom values](#custom-values) | Declare and edit fields for controls and domain kits. | ## Shape appearance ### Frame and group transforms Standard frames own children and can clip them. Moving a frame moves its subtree; resizing it changes only its boundary, preserving child geometry. Standard frames disable rotation. Hosts can explicitly enable rotation in their registered frame definition; enabled rotation carries the subtree with it. Logical groups resize their contents together. `ShapeTransformBehavior` declares `resizesChildren` (default `true`); the standard frame sets it to `false`. This policy is shared by session resize commands and pointer handles. A group selected around a frame still scales its complete contents as one group transformation. Try [frames and ownership](/agent-docs/examples/containment.md). For rotatable shapes, `ShapeTransformBehavior(showRotationHandle: true)` adds a visible rotation handle. It defaults to `false`; corner rotation remains available. `ShapeCapabilities(rotatable: false)` disables both. See [custom shapes](/agent-docs/guide/custom-shapes.md) for registration examples. `session.setShapeStyle(elementId, style)` changes a shape's editable appearance without a replacement transaction at the call site. Flutter and pure Dart hosts use this same session method. The inspector shares that command. | Argument / result | Contract | | --- | --- | | `elementId: String` | Existing shape identity; missing IDs and standalone text are denied. | | `style: ShapeStyle` | Complete style value. Use the current style's `copyWith` to retain other fields. | | Editable fields | Changed fill, stroke and corner fields must be enabled by the registered capabilities and decoration grammar. | | Return | `DiagramPolicyDecision`; locks and host admission can deny the edit. Invalid numeric or color values fail canonical validation. | | History | One admitted change is one undo unit. An identical style is a no-op. Selection, content and geometry remain unchanged. | ```dart final shape = session.document.shapeById(id)!; session.setShapeStyle( id, shape.style.copyWith(fill: const DiagramColor(0xFFE4E4E7)), ).requireAllowed(); ``` Text formatting uses `session.text`; connector appearance has its own model. Use `session.setShapeStyles(Map styles)` for atomic multi-shape styling in Flutter or Dart. Supply complete styles keyed by shape ID. All targets must pass validation and permissions; otherwise none change. Changed batches produce one event and one undo step. Empty or unchanged batches produce neither. ```dart session.setShapeStyles({ for (final shape in selectedShapes) shape.id: shape.style.copyWith(strokeStyle: StrokeStyle.dashed), }).requireAllowed(); ``` Initial authored styles and imports retain their separate document-validation contract; editable-field capabilities govern this command and the inspector. ## Connection declarations | Constructor | Port ownership | | --- | --- | | `ShapeConnectionDefinition.none()` | No connections. | | `ShapeConnectionDefinition.outline()` | Connections bind to the shape outline. | | `ShapeConnectionDefinition.fixedPorts(ports)` | The definition owns the named ports. | | `ShapeConnectionDefinition.defaultSidePorts(...)` | Ports are generated on the configured sides. | | `ShapeConnectionDefinition.instancePorts(...)` | Instance ports replace the definition's defaults when present. | For `instancePorts`, `defaultPorts` supplies an immutable list of named fallback ports. When nonempty it takes precedence over generated `defaultSides`. An empty instance port list uses these defaults; it does not disable connections. The `fixedPorts` field is reserved for fixed-port definitions. Both kinds of declared ports are checked for unique IDs, valid placement, and valid appearance during registry admission. `defaultSides` must contain unique sides, in the desired port order. Repeating a side is rejected at registration because generated ports use the side name as their identity. To place several ports on the same side, declare named `fixedPorts` or `defaultPorts` with distinct IDs instead. ```dart ShapeConnectionDefinition.instancePorts( defaultPorts: [ PortDefinition.onSide(id: 'input', side: PortSide.left), PortDefinition.onSide(id: 'output', side: PortSide.right), ], ) ``` ## Appearance before drawing Select Draw to configure fill, stroke color, stroke style and stroke width before pressing on the canvas. These session-local choices do not modify selected objects or create undo entries. Live ink and the finished drawing use the same appearance. A visible fill colors the area enclosed by the path and its closing edge. Existing drawings remain editable through their properties. | Session API | Arguments | Result | | --- | --- | --- | | `shapeCreationStyle(type)` | Registered shape type ID | Current `ShapeStyle`, falling back to the definition's default | | `setShapeCreationStyle(type, style)` | Registered type ID and valid `ShapeStyle` | Updates subsequent creations of that type; an explicit creation style takes precedence | Custom tools opt into the same inspector through `DiagramToolBehavior.creationShapeType`. Standard shape and drawing tools use this contract. Creation choices are not serialized into the document; each created shape stores its final appearance normally. Choosing a creation tool clears the old selection so these defaults are visible. Once a drawing is selected, its own properties take priority—even if Draw remains active. Stroke style changes on that selection participate in undo/redo. New ink uses restrained pressure variation (80–120% of the chosen width). Pressure is carried through each local smoothed segment; appending points does not rescale the earlier stroke's pressure coordinates. Solid ink has no repeated sample-based oscillation or whole-stroke taper. ## Definitions and elements A **definition** describes a reusable shape type. An **element** is one immutable record in the document. The **registry** connects its `type` to the definition. The **session** inserts and changes records through the canonical engine. | Type | Package | Role | | --- | --- | --- | | `DiagramElement` | `vyuh_diagram_types` | Base authored record with identity, stacking and lock state. | | `ShapeElement` | `vyuh_diagram_types` | A shape instance: `type`, frame, appearance and content. | | `TextElement` | `vyuh_diagram_types` | Standalone text; shared rectangular mechanics without shape appearance. | | `ConnectorElement` | `vyuh_diagram_types` | Connection endpoints, route, markers and labels. | | `ShapeDefinition` | `vyuh_diagram_engine` | Reusable geometry, content slots, ports and interaction grammar. | | `ShapeRegistry` | `vyuh_diagram_engine` | Definition lookup and validation. | | `DrawingSession` | `vyuh_diagram_kit` | Host commands, state, configuration and lifetime. | Import `package:vyuh_diagram_kit/vyuh_diagram_kit.dart` in a Flutter host; it exports these types. Pure Dart hosts import `package:vyuh_diagram_engine/vyuh_diagram_engine.dart` and use the same session commands. ## `session.addElement` Use an element directly when you already know its identity, frame and content: ```dart DiagramPolicyDecision addElement( DiagramElement element, { bool select = true, }) final decision = session.addElement(ShapeElement( id: 'review-card', type: Shapes.card, frame: const DiagramRect.fromLTWH(200, 160, 240, 160), )); ``` | Argument / result | Contract | | --- | --- | | `element` | Required immutable record. Shapes, text and connectors use this same method. | | `select` | Defaults to `true`; selects the inserted record. `false` preserves selection. | | Result | `DiagramPolicyDecision`; denied changes return a reason. Call `requireAllowed()` to throw on denial. | | Defaults | Preserves the supplied record; does not invent an ID, story, style, size or parent. | | Validation | Registry, identity, ownership, bindings and the session's `beforeChange` admission still apply. Invalid records throw; no partial insertion occurs. | | History | One admitted transaction and undo entry. Works before mounting a canvas. | The factory-specific `elementCreationPolicy` and `connectionPolicy` are not called by raw insertion. Put permissions that must cover **every** mutation in [the canonical `beforeChange` hook](/agent-docs/api/hooks.md), which also covers transactions, imports and history. ## `session.createShape` Use named arguments when the registry should supply defaults. ```dart final cardId = session.createShape( type: Shapes.card, worldCenter: const DiagramPoint(320, 240), size: const DiagramSize(240, 160), ); ``` Returns `String`, the created element ID. These are all named arguments: | Argument | Type | Default / behavior | | --- | --- | --- | | `type` | `String` | Required registered shape type. Standalone text uses `createText`. | | `worldCenter` | `DiagramPoint` | Required center in world coordinates. | | `id` | `String?` | Allocated when omitted; explicit IDs must be unique. | | `size` | `DiagramSize?` | Definition's `defaultSize`; requested dimensions are clamped up to its minimum. | | `style` | `ShapeStyle?` | Definition's `defaultStyle`. | | `values` | `Map` | Empty overrides by default; materializes all declared `valueFields` defaults. See [custom values](#custom-values). | | `text` | `String?` | Plain text with generated story and paragraph IDs; line breaks preserve blank and trailing paragraphs. Supply either `text` or `textStory`. The registered type must support text; otherwise creation fails without changing content or history. | | `textStory` | `TextStory?` | Materialized from the definition's declared text slots when omitted. | | `textSizingMode` | `DiagramTextSizingMode?` | Omit to use the definition’s `textCreation.sizingMode`; explicit `autoWidth` opts into intrinsic text width. | | `ports` | `List` | Empty; admitted by the registered connection grammar. | | `normalizedPath` | `List` | Empty; normalized authored-path geometry when supported. | | `pathWidths` | `List` | Empty; per-point pressure multipliers when supported. | | `polygonSides` | `int?` | Omitted; only applicable to a supporting polygon definition. | | `imageSource` | `DiagramImageSource?` | Omitted; only applicable to a supporting image definition. | | `parent` | `DiagramShapeParentPlacement` | `const AutoDiagramShapeParent()`: deepest accepting owner beneath the center. | | `select` | `bool` | `true`: select the new shape. | | `zIndex` | `int` | `0`. | Use `const RootDiagramShapeParent()` for explicit root placement, or `OwnedDiagramShapeParent(ownerId)` for an explicit child-owning shape. Missing or unsuitable explicit owners throw. Registry admission, `elementCreationPolicy` and `beforeChange` run before successful publication. Denial throws rather than returning a successful ID. The operation creates one undo entry and requires no mounted canvas. ## `session.createText` ::: tip Creation rules `elementCreationPolicy` receives the actual immutable shape or text element that would be created, including its allocated ID, frame and default content. It runs before insertion, including drawing previews. Use the element's properties to allow or deny creation. For rules covering imports and authored insertions too, use the session's `beforeChange` hook. ::: ```dart final textId = session.createText( worldCenter: const DiagramPoint(320, 240), text: 'Your notes', ); ``` Returns the new `String` ID. Named arguments are `worldCenter` (required), `id`, `size`, `text`, `textStory`, `textSizingMode`, `parent`, `select`, and `zIndex`, with the same types/defaults as above. Omitted size and story come from `ShapeRegistry.text`. There are no fill, border, port, image or path arguments. Use `text:` for plain text; the session assigns its story and paragraph IDs. Line breaks (LF, CRLF, or CR) create separate paragraphs, including blank and trailing lines. An empty string creates one empty paragraph. Use `textStory:` for formatted paragraphs and lists. Supplying both throws `ArgumentError` before changing the drawing. Omitting both keeps the registered default story. ## `session.insertConnectedElement` Pass the element you want to insert. The session adds it and its incoming connection atomically, and returns both identities as `result.elementId` and `result.connectorId`: ```dart final result = session.insertConnectedElement( source: BoundEndpoint(elementId: existingShapeId), element: ShapeElement( id: 'review-card', type: Shapes.rectangle, frame: const DiagramRect.fromLTWH(420, 190, 200, 100), ), ); ``` | Argument | Meaning | | --- | --- | | `source` | Required source endpoint: bound to an existing element or a free world point. | | `element` | Required `ShapeElement` or `TextElement`. Its identity, geometry, content, appearance and parent are preserved. | | `targetPortId` | Required for port-only targets; otherwise omit. Must name a declared target port. | | `connectorId` | Optional explicit ID; allocated when omitted. | | `router` | Defaults to `ConnectorRoute.straight`. | | `connectorZIndex` | Defaults to -1. The connector shares the target's parent. | | `select` | Defaults to true; selects the new element. False preserves selection. | | Return | A record with `elementId` and `connectorId` string fields. | One Undo removes both records; Redo restores their identities and binding. Invalid identities, unsupported targets, missing ports, connection-policy denial or document admission failure insert neither record. No mounted canvas is needed. ::: tip Authored elements and defaults This method preserves an authored element, like `addElement`; it does not materialize registry defaults or call the factory-specific `elementCreationPolicy`. Use `beforeChange` for permissions that must cover every insertion. Connection policy and canonical document validation still apply. `createShape` and `createText` supply registry defaults when you want the session to construct an element. ::: ## `ShapeElement` | Constructor property | Type | Default / contract | | --- | --- | --- | | `id`, `type` | `String` | Required identity and registry type. | | `frame` | `DiagramRect` | Required authored frame. | | `rotation` | `double` | `0`, radians around the frame center. | | `style` | `ShapeStyle` | `const ShapeStyle()`; raw records do not acquire definition defaults. | | `values` | `Map` | Empty immutable value map by default; raw insertion must supply every declared field. | | `textStory` | `TextStory?` | `null`. | | `textSizingMode` | `DiagramTextSizingMode` | `fixedWidth`. | | `ports`, `normalizedPath`, `pathWidths` | Lists | Empty immutable snapshots; subject to the definition's grammar. | | `polygonSides`, `imageSource` | `int?`, `DiagramImageSource?` | `null`. | | `contentScrollOffset` | `double` | `0`; text content scroll intent. | | `parentId` | `String?` | `null`; explicit owner identity when present. | | `clipContent` | `bool` | `false`; requires a child-clipping definition. | | `isLocked` | `bool` | `false`; per-instance editing lock. | | `zIndex` | `int` | `0`; stacking order. | Records are immutable. Use `copyWith` to construct a replacement, then submit it through a command or transaction. Geometry and story data are validated on admission. ## `ShapeDefinition` Registry admission rejects invalid default appearance, region fills, shadow colors, and port styles with `ArgumentError`. Colors must be unsigned 32-bit ARGB values; shape border width and corner radius must be finite and nonnegative. Port size must be finite and positive; port border width may be zero. Transparent colors remain valid. These are value-validity rules, independent of which appearance controls a definition exposes. Authored shapes and ports follow the same rules at document admission. Invalid transactions or imports throw `StateError` before publication and preserve the previous document, scene and history; values are not silently clamped. | Constructor property | Type | Default / contract | | --- | --- | --- | | `type` | `String` | Required open registry key. | | `displayName` | `String?` | Optional nonblank human-readable kind name for inspector titles and accessibility. Defaults to the type ID; supply a localized name when constructing the registry. Does not change serialized identity. | | `outline` | `ShapeOutlineDefinition` | Outline grammar; defaults to a rectangle. | | `textBounds` | `ShapeBoundsDefinition` | Text-region bounds grammar; defaults to a 12-unit inset. | | `supportsText` | `bool` | `true`. | | `minimumSize` | `DiagramSize` | `24 × 24`. | | `defaultSize` | `DiagramSize` | `160 × 100`. | | `defaultStyle` | `ShapeStyle` | `const ShapeStyle()`. | | `shadow` | `ShapeShadowDefinition?` | `null`. | | `capabilities` | `ShapeCapabilities?` | `ShapeCapabilities()` when omitted. | | `layers` | `List` | Empty immutable snapshot; text slots, regions, dividers, visuals and widget slots. | | `valueFields` | `Map` | Empty immutable declarations by default; typed scalar fields, defaults and constraints for `ShapeElement.values`. | | `connection` | `ShapeConnectionDefinition` | `const ShapeConnectionDefinition.outline()`. | `TextStory? createTextStory(String elementId)` materializes the declared text slots and initial content. It returns `null` when text is unsupported or not editable. It does not reset an existing element's story. ### Minimum size `minimumSize` sets the creation and resize floor in world units. `defaultSize` sets the initial size and must meet that floor. Both require positive finite dimensions. Explicit creation sizes below the minimum are clamped. | Definition | Minimum width × height | | --- | --- | | Rectangle, ellipse, polygon | `24 × 24` | | Freehand drawing | `4 × 4` | | Card | `140 × 88` | | Frame | `120 × 80` | | Comment | `80 × 80` | | Image | `80 × 60` | | Group | `1 × 1`, additionally constrained by its children | | Playground controls example | `280 × 260`, to accommodate the native controls | For your own type, declare the minimum with its geometry: ```dart ShapeDefinition( type: 'small-marker', outline: const ShapeOutlineDefinition.ellipse(), textBounds: const ShapeBoundsDefinition.frame(), supportsText: false, minimumSize: const DiagramSize(8, 8), defaultSize: const DiagramSize(32, 32), capabilities: ShapeCapabilities(textEditable: false), ) ``` Both dimensions must be finite and positive. A group or multiple selection also respects the minimum of each selected child. Text that grows to fit its content can require more height than the declared floor. The minimum is not a substitute for choosing enough room for a native widget's controls. ## `ShapeRegistry` | Construction | Behavior | | --- | --- | | `ShapeRegistry.standard(...)` | Built-in shape definitions. | | `ShapeRegistry.withStandard(definitions, overrides: ..., ...)` | Add unique definitions; intentionally replace named built-ins through `overrides`. | | `ShapeRegistry(definitions, ...)` | Curated set supplied by the host. | Duplicate IDs are rejected. Definitions are immutable. Register required types before opening a document; live replacement of registered definitions is not supported. An already registered type can be instantiated at any time. Shape registration does not install a toolbar button. `DiagramToolRegistry` separately declares creation tools, shortcuts and contributions. See [custom shapes](/agent-docs/guide/custom-shapes.md) for a complete example. ## Mutation and locking | Operation | API | Behavior | | --- | --- | --- | | Lock / unlock | `session.setElementsLocked(ids, locked)` | Returns `DiagramPolicyDecision`; one admitted reversible change. | | Delete | `session.removeElements(ids)` | Removes the ownership/dependency closure; locks, deletion capabilities and admission apply. | | Move | `session.moveElements(ids, delta)` | Translate the specified elements together. | | Resize | `session.resizeElements(ids, size, fromCenter: false, preserveAspectRatio: false)` | Resize the specified elements with optional centered and proportional sizing. | | Rotate | `session.rotateElements(ids, ...)` | Rotate the specified elements around a pivot. | | Replace text | `session.text.replace(elementId: id, before: story, after: editedStory)` | Replace a story after validating its expected content. | Each command publishes and undoes as one operation. For custom atomic edits, see [transactions and previews](/architecture/transactions). Use `beforeChange` for permissions; editing capabilities and instance locks are not an authorization boundary for advanced host-authored transactions. ## Image content ```dart DiagramPolicyDecision setImageSource( String elementId, DiagramImageSource? source, ) ``` Call `session.setImageSource(id, source)` in Flutter or pure Dart. The command requires no mounted canvas. This sets the authored source; image fetching and decoding belong to rendering or export. | Argument / outcome | Contract | | --- | --- | | `elementId` | Existing shape whose definition includes a `ShapeVisualLayerDefinition` with `ShapeLayerVisual.image`. | | `source` | Image source to store; `null` removes it. | | Missing shape or no image visual | Returns a denied `DiagramPolicyDecision`. | | Invalid source URI | Throws `ArgumentError` before publication. | | Locked shape or rejected host permission | Returns a denied decision without publishing the edit. | | Changed content | One admitted transaction and undo entry. | | Identical content | Allowed no-op; no content-change event or undo entry. | Commit or cancel an active interaction before submitting a content change. As with other content commands, edits after disposal are rejected. ## Text elements and rectangular mechanics `TextElement` is a distinct sibling of `ShapeElement`. Both extend `FramedElement`, so movement, resize, rotation, parent placement, selection, and history share the same mechanics. Text owns a required rich-text story; it has no shape style, fill, border, ports, image, path, or child-clipping fields. Configure standalone text through `ShapeRegistry.text`, using the `text` argument on `ShapeRegistry.standard`, `ShapeRegistry.withStandard`, or the registry constructor. Do not register a `ShapeDefinition` with `Shapes.text` or construct `ShapeElement(type: Shapes.text)`. ```dart final registry = ShapeRegistry.standard( text: TextElementDefinition( minimumSize: const DiagramSize(32, 24), defaultSize: const DiagramSize(200, 48), ), ); ``` `ShapeDefinition.textCreation` and `TextElementDefinition.textCreation` use a `ShapeTextCreation` declaration: | Property | Type | Contract | | --- | --- | --- | | `sizingMode` | `DiagramTextSizingMode` | Default for session/engine creation and drag-created frames; defaults to `fixedWidth`. | | `clickSizingMode` | `DiagramTextSizingMode` | Default for click creation; inherits `sizingMode` unless specified. The standard text definition specifies `autoWidth`. | Explicit `textSizingMode` arguments override these creation defaults. Existing records retain their authored sizing mode. Auto-width defaults require growing text capabilities and are rejected during registry admission otherwise. Custom shape tools may explicitly request auto-width on click; the built-in text tool consumes the definition without imposing an additional sizing default. `TextElementDefinition` declares text bounds, text layers, minimum/default sizes, and shared interaction capabilities. Its defaults grow text height and retain the selection outline during transforms. The definition rejects shape surfaces and child ownership/clipping. It does not introduce a second paragraph editor. Its `connection` defaults to `ShapeConnectionDefinition.outline()`. Standalone text accepts center and edge anchors and shares contour clipping with shapes for every router. Use `ShapeConnectionDefinition.none()` to disable bindings. Text does not declare named ports. Its rectangular geometry remains independent of shape fill and border presentation. Built-in images expose their pixels across the full frame, without fill, border, corner rounding or the corresponding inspector controls. Custom definitions can declare a different surface contract. Authored drawings use **Stroke color**, **Stroke style** and **Stroke width** in the inspector. Choose Solid, Dashed or Dotted. Solid preserves captured pressure and smooth caps through the shared ink geometry used for painting and SVG. The former Hand-drawn option is no longer offered; its serialized value remains readable for existing documents. Text color belongs to `TextRun.color`. A null run color inherits the text region foreground. See [standalone text](/agent-docs/api/text.md#standalone-text) for the record and [persistence](/agent-docs/api/persistence.md#standalone-text-migration) for legacy imports. ## Transform geometry for custom commands The pure Dart engine exposes geometry operations for custom commands and tools. They return immutable elements; they do not bypass publication, permissions, ownership closure, or history. Submit results through normal transactions. | API | Arguments | Result | | --- | --- | --- | | `DiagramElementTransform.translate` | `DiagramElement element`, `DiagramPoint delta` | Translates a shape/text frame or connector free points. | | `DiagramElementTransform.rotate` | `DiagramElement element`, required `DiagramPoint pivot`, required `double radians` | Rotates placement about a world-space pivot; framed element dimensions remain unchanged. | | `DiagramResizeTransform` | Required `DiagramRect fromBounds`, `DiagramRect toBounds`; `double rotation = 0` | Creates one immutable mapping for all elements in a resize selection. Bounds are expressed in its unrotated coordinate frame. | | `resize.mapPoint` | `DiagramPoint point` | Maps a world point through the resize coordinate frame. | | `resize.apply` | `DiagramElement element`; optional `DiagramRect resolvedFrame`, `bool fixTextWidth = false` | Maps frame or free connector geometry. A horizontal text resize can set fixed wrapping width. | Use the resolved frame captured at the beginning of the gesture for auto-grown text. Use bounds from that same scene revision, and enforce registry minima when choosing `toBounds`. The mapping preserves text, styles, local paths, and bound endpoint identity. The resolver determines the final bound connector positions. Non-finite inputs and non-positive resize bounds are rejected. Hold **Shift** to preserve aspect ratio, **Alt/Option** to resize around the center, or both. These work on side and corner handles, including multi-selections, and can change during a drag. Minimum sizes remain enforced. Side-handle aspect resizing expands the perpendicular dimension equally. ## Rounded outline geometry Rounded rectangle radii fit to half the smaller frame dimension. The resolved outline uses those fitted arcs for containment and connector boundary anchors, as well as painting. A child clipped by a rounded parent cannot be hit through the parent's cut-off corners. Rotation is applied through the shared local/world transform; it does not change the local corner radius. See [the visual composition guide](/agent-docs/api/widgets.md#compose-a-shape-visually) for how title, body and footer regions, content layout, and widget slots fit inside one shape. ### Content layer containment Region fills, dividers and image visuals are clipped to their owning shape's resolved outline, including rounded corners and rotated outlines. Interactive regions use the same containment: clipped-away corners cannot activate a region. This is a rule of the layer grammar, independent of child-frame clipping. Text continues to use `ShapeCapabilities.clipTextToOutline`. Ports and shadows are separate productions and can extend beyond the outline. On-screen drawing and PNG output share the scene painter and therefore the same content clip. ## Custom values A registered shape can declare durable scalar fields for application controls. The document owns their values; Flutter widget state must not become a second source of truth. Mount controls through the [widget slot and builder APIs](/agent-docs/api/widgets.md). ```dart final definition = ShapeDefinition( type: 'control-card', outline: ShapeOutlineDefinition.rectangle(), textBounds: const ShapeBoundsDefinition.inset(8), valueFields: { 'progress': NumberValueField( defaultValue: 25, minimum: 0, maximum: 100, ), 'status': ChoiceValueField( defaultValue: 'draft', values: ['draft', 'ready'], ), 'enabled': BooleanValueField(defaultValue: true), }, ); // Register definition in the session's shape registry first. final id = session.createShape( type: 'control-card', worldCenter: const DiagramPoint(200, 150), values: {'progress': 50}, ); final decision = session.updateValues(id, {'status': 'ready'}); ``` | API | Arguments | Behavior | | --- | --- | --- | | `ShapeDefinition.valueFields` | `Map` | Immutable declarations; unknown fields are rejected. | | `BooleanValueField` | Required `bool defaultValue` | Accepts only booleans. | | `NumberValueField` | Required `num defaultValue`; optional `minimum`, `maximum`; `integer = false` | Finite numbers within inclusive bounds; integer fields require an integral value. | | `ColorValueField` | Required `int defaultValue` | Unsigned 32-bit ARGB color, such as `0xFF3355FF`; uses a color picker. Can be specialized for domain constraints. | | `ChoiceValueField` | Required `String defaultValue` and `List values` | Restricts a value to named choices. | | `TextValueField` | Required `String defaultValue`; optional `List allowedValues` | Strings, optionally restricted to the declared choices. | | `definition.createValues(overrides)` | Optional value map | Materializes defaults and validates overrides, useful when constructing a `ShapeElement` directly. | | `session.createShape(values: ...)` | Partial value map | Supplies remaining defaults once during creation. | | `session.updateValues(elementId, patch)` | Shape ID and partial value map | Returns `DiagramPolicyDecision`; one admitted update is one undo unit. | | `ShapeElement.values` | Read-only value map | Preserved by copy, duplication, history and JSON. | Direct element insertion must contain every declared field; admission does not silently change supplied records. Invalid keys, types or ranges throw before publication. Host permission and lock rejection returns a denied decision. An unchanged patch creates no history entry. Text elements do not accept custom shape values. `ObjectValueField` supports structured JSON for domain payloads; use the scalar fields for ordinary editable properties. Custom field validation must be read-only and free of external side effects. During shape creation, grouping, and document admission, attempts to dispatch nested edits from a validator throw `StateError` before the candidate is published. Return whether the value is valid; submit any follow-up edit separately. `ShapeStyle.fields` describes the standard fill, stroke, stroke width, corner radius and stroke style using these same field types. A style remains one typed value; this metadata does not create a second property store. Custom color fields store ARGB integers in `ShapeElement.values`, while typed appearance properties use `DiagramColor`. ### Continuous control edits Use `session.beginValueEdit(elementId)`, call `preview(patch)` during a drag, then `commit()` or `cancel()`. Previews start from the original values; commit adds one undo step. Cancellation, denial or owner disposal restores the original values. Only one interaction can own the session, and closed edits reject later callbacks. ```dart final edit = session.beginValueEdit(id); edit.preview({'progress': 40}); edit.preview({'progress': 70}); final decision = edit.commit(); ``` ```dart DiagramShapeValueEdit beginValueEdit(String elementId) // Methods and property on the returned edit: bool get isActive DiagramPolicyDecision preview(Map patch) DiagramPolicyDecision commit() void cancel() ``` | Member | Contract | | --- | --- | | `beginValueEdit(elementId)` | Requires an existing unlocked resolved shape and an idle session. Throws `StateError` for a missing or locked shape or conflicting interaction. Does not require a mounted canvas. | | `isActive` | Whether this edit still owns the interaction. Becomes `false` after completion, cancellation or session disposal. | | `preview(patch)` | Merges the patch into the **starting** value map, validates and submits a preview. Include all fields intended for the current preview; successive calls do not accumulate patches. Returns the admission decision. | | `commit()` | Requests a committed edit through admission. An inactive edit returns denial and cannot commit another interaction. | | `cancel()` | Restores the starting state while active; safely does nothing after the edit closes. | Unknown fields and invalid values throw during validation; denial is reserved for rejected admitted operations. Handle both outcomes in host controls, and cancel an active edit when its control is unmounted or its gesture is abandoned. The [widget context](/agent-docs/api/widgets.md) supplies the same edit contract with mounted slot ownership checks. Pure Dart hosts use the same `session.beginValueEdit(id)` and `session.updateValues(id, patch)` methods. ### Appearance editing limits `ShapeDefinition.appearanceEditing` groups inspector bounds in an immutable `DiagramAppearanceEditing` value. Connector-label badges use the same declaration through `ConnectorLabelingCapability.appearanceEditing`. | Property | Default | Contract | | --- | --- | --- | | `maximumBorderWidth` | `32` | Finite, positive upper limit for the inspector's border-width field. | | `maximumCornerRadius` | `200` | Finite, positive upper limit for the inspector's radius field. | For example, `appearanceEditing: const DiagramAppearanceEditing(maximumBorderWidth: 64, maximumCornerRadius: 500)` permits larger inspector edits. Registry construction rejects invalid limits. Capabilities still determine whether the fields are available. These limits govern editing controls; they do not normalize imported or programmatically authored appearance values. ### Fallback stroke defaults `ShapeDefinition.strokeDefaults` accepts `ShapeStrokeDefaults`. Canvas painting and SVG consume the same resolved values: | Property | Default | Used when | | --- | --- | --- | | `visibleBorderColor` | `0xff111111` | An `alwaysPaintBorder` shape has a fully transparent authored stroke. Must have nonzero alpha. | | `minimumVisibleBorderWidth` | `1.5` | Width floor for the always-visible border pass. | | `minimumLayerStrokeWidth` | `1` | Width floor for outline-based visual layers and image placeholders. | Widths must be finite and positive. Authored visible colors and widths above these floors are preserved. These defaults affect the rendered projection; they do not mutate the stored `ShapeStyle` or enable a disabled capability. ## Plain line `Shapes.line` is a standard shape: a horizontal stroke through its frame center. Resize its length or rotate it to change orientation. It has no text, ports, arrowheads, or connector endpoint bindings. `DiagramTools.line` remains the existing connector tool. ```dart session.createShape( type: Shapes.line, worldCenter: const DiagramPoint(160, 120), size: const DiagramSize(200, 8), ); ``` ## Editable paths A line or polyline is an editable path with straight segments and corner nodes. The same `DiagramVectorPath` can mix straight and cubic Bézier segments, and can be open or closed. Closed paths require at least three nodes; open paths require two. Only closed paths paint their fill. ```dart final pathId = session.createPath( worldCenter: const DiagramPoint(240, 160), size: const DiagramSize(240, 160), path: DiagramVectorPath.polyline( const [DiagramPoint(0, 0), DiagramPoint(1, 0), DiagramPoint(1, 1)], closed: true, ), ); session.paths.setSegmentKind( pathId, 'node-0', DiagramPathSegmentKind.cubic, ); session.paths.setHandleMode( pathId, 'node-1', DiagramPathHandleMode.symmetric, ); session.paths.splitSegment(pathId, 'node-0', newNodeId: 'extra'); ``` For authored curves, construct `DiagramVectorPath(nodes: [...])` using `DiagramPathNode(id:, position:, incoming:, outgoing:, mode:, segment:)`. Positions and tangent offsets use frame-local unit coordinates. `segment` describes the segment leaving that node, including the closing segment. Node IDs are unique within their path and survive edits and persistence. `session.paths` exposes `setClosed`, `setHandleMode`, `setSegmentKind`, `splitSegment`, `removeNode`, and `moveControl`. Each returns an admission decision and records one undo entry when allowed. Invalid node counts, IDs or coordinates are rejected without changing the document. `splitSegment` uses De Casteljau subdivision to preserve cubic geometry. Moving a node recomputes the frame while preserving the other points through rotation and containment. Handle modes: `corner` is independent; `smooth` keeps opposite directions; `symmetric` also keeps equal lengths. Choose **Line / path** (`P`) and drag to create a path. Drag vertices to edit it or hover and drag a midpoint to insert a point. The inspector controls closure, point type and insertion/removal. Smooth and Symmetric add Bézier handles; Corner collapses them. Edits support commit, cancel and undo. The playground also exposes **Polygon** (`G`) in its toolbar. ## Surface fills and rounded corners `ShapeStyle.fill` accepts the closed `DiagramFill` grammar. Existing `DiagramColor` values remain valid solid fills: ```dart final gradient = DiagramLinearGradient( begin: const DiagramPoint(0, 0), end: const DiagramPoint(1, 1), stops: const [ DiagramGradientStop(0, DiagramColor(0xFF6366F1)), DiagramGradientStop(0.4, DiagramColor(0xFFEC4899)), DiagramGradientStop(1, DiagramColor(0xFFFFC857)), ], ); session.setShapeStyle(pathId, ShapeStyle(fill: gradient, cornerRadius: 12)); ``` Use `const DiagramFill.none()` for no fill, or `DiagramRadialGradient(stops:, center:, radius:)` for a radial fill. Gradient coordinates are normalized to the shape frame; radial radius is a fraction of its shorter side. They move and rotate with the shape. Stops are immutable, ordered, and contain at least two finite offsets in `[0, 1]`; duplicate offsets permit hard color transitions. Each stop supports alpha. Linear endpoints must differ, and radial radius must be positive. Invalid paints fail admission. The inspector uses a single gradient bar: click to add a stop, drag its handle to change its position, and select it to edit its color or remove it. Arrow keys adjust the selected stop; Delete removes it while retaining at least two stops. It also supports fill kind, stop positions, linear angle, and radial center/radius. JSON, canvas, minimap and SVG preserve the full fill. SVG uses native linear/radial gradient resources. Sweep gradients are not currently supported. `representativeColor` is available for contrast or simplified host UI; it is not a replacement for rendering the full gradient. `cornerRadius` is measured in world units. Rectangles, regular polygons and editable paths support it; long radii clamp to adjacent segment lengths. Path rounding changes resolved geometry without rewriting the authored nodes or handles. Smooth joins and open endpoints are preserved. Ellipses have no corners. Custom definitions expose editing with `ShapeCapabilities(cornerRadiusEditable: true)` and a corner-capable outline production. Registered region layers also accept `DiagramFill`. **API migration:** `ShapeStyle.fill` is now `DiagramFill`. Code that needs a solid color should pattern-match `DiagramColor(:final argb)`; code choosing contrast may use `fill.representativeColor`. Existing `ShapeStyle(fill: DiagramColor(...))` construction remains source-compatible. ## Simple paths and declarative inspectors Use `session.createLine(start: ..., end: ...)` for two world-space points, or `session.createPolyline(points: ..., closed: true)` for a polygon. Open paths require two points; closed paths require three. Both return an element ID and create one undo step. Use `session.createPath` when you need explicit normalized nodes, tangents and frame geometry. `session.paths.addPoint(id, after: nodeId)` splits a segment without changing its curve and returns the new point ID. `setHandleMode` converts that point to a corner, smooth or symmetric handle. `ShapeDefinition(type: 'card')` provides a rectangle with inset text. Its inspector follows its capabilities. Use `ShapeInspectorDefinition([...])` to select and order supported editors; duplicates reject. Inspector visibility does not change permissions. See [common tasks](/agent-docs/guide/common-tasks.md) for examples. ## Compound shapes and libraries Use `ShapeDefinition.composition` to arrange reusable parts with `ShapeRow`, `ShapeColumn`, `ShapeStack`, `ShapeSized` and `ShapePadding`. Composition contributes minimum dimensions and compiles into ordinary shape layers. See [the composition guide](/agent-docs/guide/shape-composition.md) for layout rules, named text slots, parts, and the five starter libraries. --- # Paragraphs and text regions For a plain-text rename, call `session.text.setPlainText(elementId: id, text: 'New text').requireAllowed()`. This keeps story identity, resets formatting to plain paragraphs and uses the same grammar, admission and undo path as rich editing. Specify `storyId` for multiple connector labels and `slotId` for multi-slot stories. Other slots are preserved. See [common tasks](/agent-docs/guide/common-tasks.md) for failure handling. A `TextStory` contains `TextParagraph` blocks. Each paragraph contains `TextRun` spans for inline text and formatting. `ParagraphKind` chooses ordinary text or a list item; `TextMark` supplies formatting such as bold and italic. Use `session.text` to edit this content through the session's history and validation. Paragraph alignment affects ordinary paragraphs only. Lists remain left-aligned, including nested items. Markers reserve a half-em gap before their text. Checklist markers are clickable and use the same resolved geometry as painting. ## On this page | Category | What you can do | | --- | --- | | [Text content](#text-inside-any-shape) | Use the same text model across shapes and labels. | | [Inline styling](#textrun) | Describe text and its marks. | | [Lists](#lists-and-checklists) | Create bulleted, numbered, nested and checklist content. | | [Allowed formatting](#grammar-configuration) | Choose supported paragraph and mark styles. | | [Paragraph formatting](#paragraph-commands) | Align paragraphs, change direction, and format lists. | | [Font size](#adjust-font-size) | Adjust a character range using registered font defaults. | | [Text commands](#text-commands) | Replace stories or toggle checklist items through history. | | [Whole-story scope](#whole-story-paragraph-formatting) | Understand empty paragraphs and explicit selection boundaries. | | [Standalone text](#standalone-text) | Create an independent text element. | ## Text inside any shape Named `ShapeTextLayerDefinition` slots let a card have separate title and body regions while keeping one canonical story. A `singleLine: true` region accepts inline formatting and exits editing on Enter. A multiline region supports paragraphs, nested ordered/unordered lists and checklists. Default line height is 1.3. Wrapped visual lines share that line height; successive authored blocks have a separate 6-unit after-gap. The final block adds no trailing gap to the text box. Empty paragraphs retain the same line and caret geometry as populated paragraphs. The platform keyboard follows the active region: single-line slots request a text keyboard with a Done action; multiline slots request a multiline keyboard with a Newline action. Done exits a single-line slot without inserting a paragraph. Moving between slots updates the existing IME connection; ordinary caret movement does not reconfigure it. These platform-contract checks do not certify real-device mobile selection, keyboard viewport handling or IME behavior. Inside an active text region, touch-and-hold selects a word; dragging after the hold extends selection by whole words using the same resolved geometry as mouse selection. A finger move before the hold threshold does not perform mouse-style drag selection. A second finger transfers control to canvas navigation and exits text editing through the shared lifecycle. Selection handles, a mobile clipboard menu and keyboard occlusion remain open; touch navigation and text selection still need real-device validation. Text reflows in each declared region. The definition's text sizing policy decides whether the owner grows or retains its bounds. Display, selection and editing consume the same resolved lines rather than measuring a separate text widget. ## `TextRun` ```dart TextRun( String text, { Set marks = const {}, String? fontFamily, double? fontSize, DiagramColor? color, }) ``` | Property | Default | Meaning | | --- | --- | --- | | `text` | Required | Inline text, with UTF-16 offsets used by editing commands. | | `marks` | Empty set | Immutable inline formatting marks. | | `fontFamily` | `null` | Inherits the renderer's font family. | | `fontSize` | `null` | Uses the standard diagram font size. | | `color` | `null` | Inherits the text region foreground; an explicit color paints the glyphs, not the owner rectangle. | `copyWith` preserves omitted properties. Explicit `null` clears `fontFamily`, `fontSize`, or `color` back to inheritance. Colors persist through JSON, text splitting, merging, and paragraph editing. Clear formatting also clears the explicit color. Inspector formatting follows the current selection across all text owners: | Selection | Inline properties (font, size, color, marks) | Paragraph properties | | --- | --- | --- | | Text range | Only selected characters, including ranges across blocks | Blocks touched by the range | | Caret while editing | Next typed characters; existing content is unchanged | The caret's block | | Element or connector label | The whole story | All applicable blocks; titles retain their single-line contract | Formatting is stored in canonical runs and blocks and survives JSON export/import. Caret-only typing preferences belong to the mounted editing session until text is inserted; they are not serialized as document content. ## Paragraph commands For inline formatting, use `session.text.toggleMark(elementId: id, mark: TextMark.bold)` or `session.text.clearFormatting(elementId: id)`. Both accept optional `storyId`, `start = 0`, and `end` arguments. Omit the range to affect the story's text, or supply UTF-16 offsets to format selected characters only. Range endpoints may be supplied in either order and are clamped to the story. Unselected runs and paragraph structure are preserved. Clear formatting removes explicit marks, font, size, and color from the selected text. A collapsed range does not change document formatting or configure future typing. Format paragraphs through `session.text` without building replacement stories. These commands work for shape text, text boxes, and connector labels. | Command | Formatting argument | Purpose | | --- | --- | --- | | `setAlignment` | `alignment: ParagraphAlignment` | Align ordinary paragraphs. | | `setDirection` | `direction: ParagraphDirection` | Set reading direction. | | `toggleList` | `kind: ParagraphKind` | Toggle ordered, unordered, or checklist items. | | `changeIndent` | `delta: int` | Adjust list nesting. | Each requires `elementId` and accepts an optional `storyId`, `start = 0`, and optional `end` UTF-16 offsets. Omitted `end` reaches the story's end. A range ending at the next paragraph's start excludes that paragraph. Lists retain fixed alignment. ```dart session.text.toggleList( elementId: textId, kind: ParagraphKind.checklistItem, ).requireAllowed(); ``` For shapes, text boxes, and connectors with one label, the story is inferred. Supply `storyId` for a connector with multiple labels; omitting it denies the edit without changing any label. The same inference applies to `toggleChecklist`, which still requires `blockId`. Commands return an admission decision and record a changed operation as one undo entry. Locked or unavailable targets are denied. Toggling an already-applied list kind restores ordinary paragraphs; invalid list kinds throw `ArgumentError`. ## `session.text.replace` Publishes one replacement story through the same admission and history pipeline for shape text, standalone text and connector labels. Build `after` with the pure `DiagramTextCommands` helpers, then submit it with the current `before` story. ```dart DiagramPolicyDecision replace({ required String elementId, required TextStory before, required TextStory after, String label = 'Edit text', StorySelection? selectionAfter, DiagramHistoryGroup? historyGroup, }) ``` | Argument | Contract | | --- | --- | | `elementId` | Owning element ID. For a connector label, this is the connector ID; `before.id` identifies its label story. | | `before` | Current canonical story. A stale story throws `StateError` without publishing or adding history. | | `after` | Replacement with the same story ID. The target's paragraph/region grammar is validated before publication. | | `label` | Description of the history operation. | | `selectionAfter` | Optional text selection referring to this element and story. A different identity throws `ArgumentError`. Anchor and focus offsets are independently clamped to the resulting story's UTF-16 length, preserving range direction. | | `historyGroup` | Optional active history group for combining a gesture's edits. Omit for a separate undo unit. | The result is a `DiagramPolicyDecision`: missing, locked or noneditable targets are denied, as are edits rejected by host admission. `requireAllowed()` converts a denied decision into `DiagramChangeDenied`. Calls through a disposed session throw `StateError`. The published document, resolved scene and mapped selection agree before observers receive the update. Undo/redo restores the admitted story and selection; offset correction does not create another history entry. This command does not mount an editor, request keyboard focus or establish an IME connection. See the [range-formatting example](/agent-docs/examples/formatting.md) for a complete call. ## Lists and checklists The paragraph inspector offers bullets, numbers and checklists. While editing, it changes the selected paragraphs; with an element selected, it changes the applicable story blocks. Single-line regions do not accept lists. | Input at the start of a paragraph | Result | | --- | --- | | `- ` or `* ` | Bulleted item | | `1. ` | Numbered item | | `[] ` or `[ ] ` | Unchecked checklist item | | Enter after an item | Next item of the same kind and nesting level; new checkboxes start unchecked | | Tab / Shift+Tab | Nest beneath the preceding item / outdent | | Enter on an empty item | Outdent one level, or leave the list at the root | | Click a checkbox | Toggle its checked state, with undo/redo | Prefixes also work before existing text: move to the start, type the marker and its trailing space, and the paragraph becomes a list item while retaining its text and formatting. The caret stays before that text. Immediate Backspace restores the literal marker. Typing elsewhere in an existing line does not automatically convert it. These shortcuts are shared by all multiline paragraph regions. The shared registry controls allowed list kinds; shortcut spellings are fixed. List kinds can be mixed at nested levels. Kind, nesting depth, checked state, and formatted text runs persist through document JSON export/import. ## Grammar configuration Inline appearance is validated at the same story-admission boundary for every text owner. A run's `fontSize` may be `null` (inherit) or finite and strictly positive. Its `color` may be `null` or an unsigned 32-bit ARGB value, including transparent. These validity rules do not clamp authored text to inspector font size limits. Invalid edits and imports throw `StateError` before publication. Direct paragraph-layout calls also validate their placement inputs before shaping: region slot IDs must be nonempty and unique, bounds must be finite with nonnegative dimensions, and block spacing and list indentation must be finite and nonnegative. Region foreground and default font size must be valid. Invalid layout arguments throw `ArgumentError`; duplicate slots are never silently merged. | Rule | Current configuration | | --- | --- | | Single-line versus multiline named shape region | `ShapeTextLayerDefinition.singleLine` | | Single-line versus multiline connector label | `ConnectorLabelingCapability.singleLine` | | Allowed bulleted, numbered and checklist blocks | `ShapeRegistry.paragraphGrammar` (`ParagraphGrammar`) | | Maximum list nesting depth | No host-configurable limit yet | | Input shortcut spellings | Shared built-in mapping; not configurable yet | Configure one grammar for all text owners: ```dart final session = DrawingSession( document: DiagramDocument(id: 'drawing', revision: 0, elements: []), shapeRegistry: ShapeRegistry.standard( paragraphGrammar: const ParagraphGrammar( bullets: true, numberedLists: true, checklists: false, ), ), ); ``` All three options default to `true`. Ordinary paragraphs are always allowed. Single-line regions further restrict content to one ordinary paragraph. Disabled list shortcuts remain literal text and their inspector actions are hidden. Canonical admission rejects forbidden blocks from API edits or imports; it does not silently discard or flatten content. This grammar is shared across shapes, standalone text, and multiline connector labels. Per-region list allowlists, maximum nesting depth, and custom shortcut spellings are not yet configurable. ## Standalone text `TextElement` owns rich text without a shape surface. It extends `FramedElement`, sharing rectangle-based placement and transformation mechanics with shapes. It has no `style`, ports, path, image, or child-clipping properties. Text inside a card remains a story in that card's declared regions; both owners use the same paragraph commands, resolved lines, selection, and IME pipeline. ```dart final caption = session.createText( id: 'caption', worldCenter: const DiagramPoint(150, 100), size: const DiagramSize(220, 80), text: 'A little room for ideas', ); ``` `createText` returns the element ID and creates one undoable edit. Supply `textStory` instead of `text` when you need rich formatting. Read the resulting `TextElement` with `session.document.textById(caption)`; its stored fields are listed below. | Constructor field | Default | Meaning | | --- | --- | --- | | `id` | Required | Stable element identity. | | `frame` | Required | Rectangle in the owner's coordinate system. | | `textStory` | Required | Canonical paragraphs, lists, runs, and formatting. | | `rotation` | `0` | Rotation in radians. | | `parentId` | `null` | Optional containing element; text cannot own children itself. | | `zIndex` | `0` | Stacking order. | | `textSizingMode` | `DiagramTextSizingMode.fixedWidth` | Wrapped fixed width or intrinsic `autoWidth`. | | `contentScrollOffset` | `0` | Authored text content scroll offset. | `type` is a getter returning `Shapes.text`, not a constructor argument. `copyWith` preserves identity and omitted fields; explicit `parentId: null` removes its parent. `DiagramDocument.textById` retrieves standalone text, while `framedElementById` retrieves either text or a shape. The text tool starts a clicked box in auto-width mode; dragging creates a wrapping box. Auto-width does not limit paragraph count. Both modes support Enter-created paragraphs and use the configured sizing policy for height. Configure standalone text with `registry.text` / `TextElementDefinition`; see [registry configuration](/agent-docs/api/shapes.md#text-elements-and-rectangular-mechanics). ## Text commands ### Plain-text replacement `session.text.setPlainText({required String elementId, required String text, String? storyId, String? slotId})` returns `DiagramPolicyDecision`. The command retains the story ID and existing paragraph IDs where possible, allocates IDs for additional paragraphs, and resets marks, lists, direction and alignment to plain-paragraph defaults. Role and slot bindings are retained. CRLF, CR and LF split paragraphs; a trailing newline creates an empty paragraph. An empty string leaves one empty paragraph. Omit `storyId` for a shape, standalone text or a connector with exactly one label. Specify it for a connector with multiple labels. Omit `slotId` only when the selected story has one slot; otherwise supply an existing effective slot ID. Missing or ambiguous targets return `unavailableTarget` without publication. Other slots retain their original content. Registry-invalid content, such as a newline in a single-line slot, is rejected by admission. Undo restores the whole previous story in one step. Use `replace` when preserving rich formatting. ::: warning Bidirectional editing boundary With a collapsed caret, plain Left/Right traverses shaped visual caret positions. Shift+Left/Right extends the range through those positions. Canonical selections retain anchor and focus affinity through platform input, selection clamping and history. Shaped caret geometry, canvas caret painting and IME placement consume it at soft wraps. Pointer caret placement and dragging, vertical navigation, and Home/End preserve wrap affinity. Plain range collapse chooses visual endpoints, and Home/End uses shaped visual edges. Alt/Ctrl+Left/Right follows shaped word intervals; adding Shift preserves the selection anchor. Word deletion remains a logical content operation. Paragraphs support explicit left-to-right and right-to-left base direction. Automatic direction detection is not provided. ::: ### Paragraph direction Set `TextParagraph.direction` to `ParagraphDirection.ltr` (the default) or `ParagraphDirection.rtl`. `TextStory.paragraph` accepts the same `direction` argument. Direction is saved with the paragraph and inherited when Enter splits it. Start/end alignment and list gutters follow that direction. ```dart session.text.setDirection( elementId: 'notes', direction: ParagraphDirection.rtl, ).requireAllowed(); ``` | Argument | Meaning | | --- | --- | | `elementId` | Required text owner ID. | | `storyId` | Optional story ID. Inferred when the owner has one story; required to choose among multiple connector labels. | | `direction` | Explicit paragraph base direction. Inline mixed-direction runs still use normal text shaping. | | `start`, `end` | Optional UTF-16 range; all touched paragraphs are formatted. Omit to format the whole story. | The command uses normal validation and undo/redo. The inspector's **Direction** control formats selected paragraphs while editing, or the entire story otherwise. `session.text` exposes `TextEditing`, a session-owned facade without store access. Retained command references throw `StateError` after session disposal. Pure Dart hosts use the same `session.text` methods. It edits the existing story in a shape, standalone text element, or connector label. The canvas paragraph editor and checklist interaction use these same commands. Text layout comes from the session’s configured text backend; no canvas is required. ```dart final before = DiagramTextTarget.resolve( session.document.elementById(elementId), storyId, )!.story; final after = DiagramTextCommands.replaceText( story: before, blockIndex: 0, start: 0, end: before.blocks.first.plainText.length, text: 'Updated by the host', ); final decision = session.text.replace( elementId: elementId, before: before, after: after, ); ``` ### `replace(...) → DiagramPolicyDecision` | Argument | Type | Required / default | Meaning | | --- | --- | --- | --- | | `elementId` | `String` | Required | Owning shape, text element or connector ID. | | `before` | `TextStory` | Required | Expected current story. A stale value throws before publication. | | `after` | `TextStory` | Required | Replacement with the same story ID; registry and document validation still apply. | | `label` | `String` | `'Edit text'` | History and change-event description. | | `selectionAfter` | `StorySelection?` | `null` | Optional selection in this same owner/story; otherwise preserve current selection. | | `historyGroup` | `DiagramHistoryGroup?` | `null` | Existing history lease, used by composition input. Ordinary host edits can omit it. | Missing or non-editable targets return a denied decision. Locks and host admission remain effective. A changed story ID or selection targeting a different story throws `ArgumentError`. Successful edits publish state and resolved geometry atomically and participate in undo/redo. Retained commands reject access after the owning session is disposed. ### `replaceAll(replacements, ...) → DiagramPolicyDecision` Edit several stories in one publication and undo entry. Single-story `replace` uses this same command. A failed edit leaves the entire batch unpublished. ```dart session.text.replaceAll([ (elementId: titleOwner, before: oldTitle, after: newTitle), (elementId: bodyOwner, before: oldBody, after: newBody), ], label: 'Update invitation').requireAllowed(); ``` | Argument | Type | Default | Meaning | | --- | --- | --- | --- | | `replacements` | `Iterable` | Required | Named records with `elementId`, `before` and `after`; stories may belong to shapes, text elements or connector labels. | | `label` | `String` | `'Edit text'` | One history/event description for the batch. | | `selectionAfter` | `StorySelection?` | `null` | Must target one of the replaced stories. | | `historyGroup` | `DiagramHistoryGroup?` | `null` | Optional existing history lease. | Duplicate owner/story pairs and changed story identities throw `ArgumentError`. Stale expected stories throw before publication. Missing/noneditable targets and permission denials reject the whole batch. Registry grammar and fixed domain validation still apply to the complete result. ### `toggleChecklist(...) → DiagramPolicyDecision` | Argument | Type | Meaning | | --- | --- | --- | | `elementId` | `String` | Required text owner ID. | | `storyId` | `String?` | Inferred when the owner has one story; supply it for multiple connector labels. | | `blockId` | `String` | Required checklist-item ID, independent of line wrapping or block index. | Toggles the item's checked state as one undoable edit. Missing, non-editable or non-checklist targets return a denied decision. This command does not convert paragraphs into lists; use `session.text.toggleList` for that. Use `replace` for custom structural or run-formatting edits that need an explicit replacement story. Text measurement and baseline contracts are covered in [text backend architecture](/architecture/text-backends#baseline-geometry). ## Font-size editing bounds `ShapeDefinition.fontSizeRange`, `TextElementDefinition.fontSizeRange`, and `ConnectorLabelingCapability.fontSizeRange` declare one `DiagramTextFontSizeRange`. The resolved text target carries it to the inspector, selected-object shortcuts, selected-text formatting, and pending caret formatting. | Member | Type | Contract | | --- | --- | --- | | `minimum` | `double` | Defaults to `8`; finite and positive. | | `maximum` | `double` | Defaults to `96`; finite and at least `minimum`. | | `constrain(size)` | `double → double` | Clamps a finite positive requested size to the range. | | `adjust(size, delta)` | `(double, double) → double` | Adds a finite delta and clamps the result; the original size must be finite and positive. | Invalid declarations fail registry admission with `ArgumentError`. These are editing bounds: importing or resolving an already-authored size does not silently rewrite it. Run sizes still require finite positive values under document admission. ```dart TextElementDefinition( fontSizeRange: const DiagramTextFontSizeRange(minimum: 12, maximum: 48), ) ``` ## Whole-story paragraph formatting When `end` is omitted, session paragraph commands include the final empty paragraph, including one created by a trailing newline. An explicit `end` remains exclusive: a range ending at the next paragraph's start does not format that paragraph. This applies equally to Flutter and headless sessions. ## Adjust font size Use the same session command in Flutter or headless Dart: ```dart session.text.adjustFontSize( elementId: textId, delta: 2, start: 0, end: 5, ).requireAllowed(); ``` The command adjusts existing runs within the character range as one undo step. Omit the range to adjust all existing text. `defaultSize` supplies the size for runs without an explicit size (defaults to the resolved target’s font size); `range` accepts a `DiagramTextFontSizeRange` to bound the result. Reversed ranges are normalized. A collapsed range does not set future typing style. Invalid numeric arguments fail without publishing content or clearing redo. The command uses the resolved target’s registered bounds and default size automatically. Explicit `range` and `defaultSize` override those defaults. Connector labels normally use 14. --- # Widgets inside shapes A shape can mount Flutter controls in declared content slots. The shape registry owns slot geometry and portable fallback layers; `DrawingConfiguration.widgetRegistry` owns Flutter builders. Durable control values live in `ShapeElement.values` and use the same validation, admission and history as other document changes. Open the playground and choose **The basics** to try the controls card with a slider, dropdown and counter button. ## On this page | Category | What you can do | | --- | --- | | [Define widget content](#declare-a-slot) | Declare bounds, values and portable fallback content. | | [Register controls](#register-flutter-builders) | Connect widget keys to Flutter builders. | | [Read and edit values](#shapewidgetcontext) | Read current values and submit validated updates. | | [Continuous editing](#a-slider-with-one-undo-step) | Preview a drag and commit it as one undo action. | | [Lifecycle and export](#focus-lifecycle-and-output) | Handle focus, replacement and exported fallback content. | | [Pointer behavior](#backgrounds-and-pointer-ownership) | Keep controls interactive and other areas draggable. | ## Compose a shape visually Start with **shape geometry**, divide the interior into **content regions**, then choose a **content layout** and the content for each region. Use the same terms whether the content is portable diagram text and artwork or live Flutter controls. | Term | Meaning | Current API | | --- | --- | --- | | Shape geometry | The outer frame and visible contour. | `ShapeElement.frame`, `ShapeDefinition.outline` | | Content region | A meaningful area such as title, body or footer. These are names you choose, not special shape types. | Named text slots and layer IDs | | Content layout | How regions and their content share the shape's interior. | `ShapeRow`, `ShapeColumn`, `ShapeStack`, `ShapeSized`, `ShapePadding` | | Region content | What appears there: text, surfaces, images, artwork or controls. | Composition parts; `ShapeWidgetLayerDefinition` for a live widget slot | | Diagram layout | How whole shapes are positioned on the canvas. | Graph layout, including `ElkGraphLayout` | Prefer `ShapeDefinition.composition` for portable content layout. A column can give the title a fixed height, let the body fill the remaining space, and reserve a footer below it. A row can arrange an icon beside the title; a stack can layer content in the same region. See [shape composition](/agent-docs/guide/shape-composition.md) for a complete example and resizing rules. Composition compiles into the same layers and bounds used by the resolver. `ShapeBoundsDefinition.topBand(40)` is a lower-level bounds expression for a 40-unit title region; **band** is part of that API name, not a separate layout concept. `belowTopBand(40)` describes the remaining body region, and `inset` adds padding. These expressions remain useful for explicitly positioned layers and widget slots. There is currently no `ShapeGrid` or widget composition part. Use nested rows and columns for a fixed grid-like arrangement of portable parts. Declare live controls through `ShapeWidgetLayerDefinition`; its Flutter builder can use `Row`, `Column`, `Stack` or a Flutter grid inside the resolved slot. The outline controls the visible contour. Visual layers and widget slots are clipped to it; text has its own `clipTextToOutline` setting. Ports declare connection attachment points separately from content layout. Use `ShapeVectorLayerDefinition` for portable icons or line artwork. Each `ShapeVectorPath` supplies immutable points between 0 and 1, relative to the layer bounds. Paths can be closed and filled, or open with rounded stroke ends. Their color follows the owning shape's stroke. Canvas and SVG use the same resolved points. Prefer a `ShapeRow` for an icon beside title text. The lower-level `leftBand(width)` and `afterLeftBand(width)` expressions can also position those two regions when declaring layers directly. The widget builder receives the slot's dimensions and arranges its own controls inside that space. The diagram handles moving, resizing and rotating the owning shape. Portable fallback layers occupy the same slot when no builder is available and in exported output. ::: tip Shared coordinates Layer bounds, including widget fallback bounds, are expressed in the owning shape's coordinates. Only the Flutter widget's internal layout is local to its slot. ::: ## Declare a slot ```dart final controlDefinition = ShapeDefinition( type: 'host.control', outline: const ShapeOutlineDefinition.rectangle(), textBounds: const ShapeBoundsDefinition.frame(), supportsText: false, capabilities: ShapeCapabilities(textEditable: false), valueFields: { 'progress': const NumberValueField( defaultValue: 35, minimum: 0, maximum: 100, ), }, layers: [ ShapeWidgetLayerDefinition( id: 'control', bounds: const ShapeBoundsDefinition.inset(12), widgetKey: 'host.slider', fallback: const [ ShapeRegionLayerDefinition( id: 'control-background', bounds: ShapeBoundsDefinition.inset(12), fill: DiagramColor(0xFFEAF1FF), ), ShapeDividerLayerDefinition( id: 'control-track', bounds: ShapeBoundsDefinition.inset(20), ), ], ), ], ); ``` | `ShapeWidgetLayerDefinition` argument | Type | Contract | | --- | --- | --- | | `id` | `String` | Required stable layer identity; unique within the shape, including fallback layers. | | `bounds` | `ShapeBoundsDefinition` | Required slot geometry in the owning shape coordinate system. Must resolve to positive area. | | `widgetKey` | `String` | Required builder lookup key. | | `fallback` | `List` | Required nonempty list, copied immutably. Ordinary region, divider, image or text productions; nested widget slots are rejected. All fallback bounds must fit within the resolved slot. | Fallback bounds use the owning shape coordinate system, **not** coordinates relative to the slot. Fallback text uses declared story slots and the shared paragraph layout. See [shape values and definitions](/agent-docs/api/shapes.md) for field schemas and shape creation. ## Register Flutter builders ```dart final widgets = ShapeWidgetRegistry({ 'host.slider': (context, shape) => ProgressControl(shape: shape), }); final configuration = DrawingConfiguration(widgetRegistry: widgets); ``` Pass this configuration and the shape registry to `DrawingSession`. The builder receives tight slot dimensions. Lay out your control locally; the canvas applies camera movement, rotation, clipping and scene stacking. | API | Arguments / result | Contract | | --- | --- | --- | | `ShapeWidgetRegistry(builders)` | `Map` | Copies the map immutably. Keys must be nonempty without surrounding whitespace. | | `const ShapeWidgetRegistry.empty()` | No arguments | Default registry; all widget slots display their portable fallback. | | `builderFor(key)` | `String` → `ShapeWidgetBuilder?` | Returns `null` when the host has no builder. The mounted canvas retains the fallback. | | `require(key)` | `String` → `ShapeWidgetBuilder` | Throws `StateError` for an absent key. | | `ShapeWidgetBuilder` | `Widget Function(BuildContext, ShapeWidgetContext)` | Builds the live presentation of one resolved slot. | Builders are Flutter configuration, never document content. A document can be loaded without its widget builders, provided its shape definitions are registered. ## `ShapeWidgetContext` The canvas supplies a fresh immutable context when canonical state changes. Read values from that context instead of keeping a second authoritative copy in widget state. Widget state may retain transient UI resources, such as focus or an active edit handle. | Member | Type / arguments | Contract | | --- | --- | --- | | `owner` | `ResolvedShape` | Resolved shape snapshot and its canonical `source`. | | `slot` | `ResolvedShapeWidgetSlot` | Resolved bounds and slot definition. | | `values` | `Map` | Immutable canonical values from the owner. | | `readOnly` | `bool` | Host read-only state for this projection. | | `enabled` | `bool` | False when read-only or locked. Use it to disable controls. Live callbacks still recheck ownership and admission. | ### Update widget values Use a discrete update for a button or dropdown, and a continuous edit for a drag. Both participate in the drawing’s validation and undo history. | Member | Type / arguments | Contract | | --- | --- | --- | | `updateValues(patch)` | `Map` → `DiagramPolicyDecision` | Merges supplied fields into this owner's values as one discrete command. Unknown fields or invalid field values are rejected. | | `beginValueEdit()` | → `DiagramShapeValueEdit` | Starts a bounded continuous edit for this owner. Throws `StateError` when read-only or unavailable. | `enabled` is a UI hint, not an authorization boundary. Host admission hooks remain in the canonical command path; controls should handle denied decisions when the application needs to explain them. ## A slider with one undo step Use `updateValues` for discrete actions such as buttons, dropdown changes and keyboard slider adjustments. A drag owns a `DiagramShapeValueEdit` until it commits or cancels: | Edit member | Arguments / result | Contract | | --- | --- | --- | | `isActive` | `bool` | Whether the owned interaction lease is active. | | `preview(patch)` | `Map` → `DiagramPolicyDecision` | Validates and previews values. Each patch starts from the original values, not the previous preview; include every field being changed together. An inactive edit returns a denied decision. | | `commit()` | → `DiagramPolicyDecision` | Commits the accepted preview as one history operation. | | `cancel()` | `void` | Cancels the preview without adding an undo entry. | ```dart class ProgressControl extends StatefulWidget { const ProgressControl({super.key, required this.shape}); final ShapeWidgetContext shape; @override State createState() => _ProgressControlState(); } class _ProgressControlState extends State { DiagramShapeValueEdit? _edit; void _cancel() { _edit?.cancel(); _edit = null; } @override void dispose() { _cancel(); super.dispose(); } @override Widget build(BuildContext context) { final shape = widget.shape; return Listener( onPointerCancel: (_) => _cancel(), child: Slider( value: (shape.values['progress'] as num).toDouble(), max: 100, onChangeStart: shape.enabled ? (_) { _cancel(); _edit = shape.beginValueEdit(); } : null, onChanged: shape.enabled ? (value) { final edit = _edit; if (edit != null) { edit.preview({'progress': value}); } else { shape.updateValues({'progress': value}); } } : null, onChangeEnd: shape.enabled ? (_) { _edit?.commit(); _edit = null; } : null, ), ); } } ``` This example uses Flutter's `Slider`, `StatefulWidget` and `Listener`; import your Flutter UI library and `package:vyuh_diagram_kit/vyuh_diagram_kit.dart`. Provide the usual UI-library ancestors required by your controls. The complete playground example is in [widget_card.dart](https://github.com/vyuh-tech/vyuh_diagram/blob/main/apps/demo/lib/playground/widget_card.dart). ## Focus, lifecycle and output Replacing a mounted slot's builder cancels its active value edit and restores the starting values before the replacement builder receives its context. Late callbacks from the old edit cannot commit or cancel a successor edit. The mounted canvas interleaves controls with ordinary scene paint and clips them to their slot, the owner outline and clipping ancestors. Selection handles, ports and higher scene content retain their input priority. The accessibility tree retains the object selection entry and ordinary story text, while omitting fallback text replaced by a live slot. Native controls supply their own Flutter semantics; when a builder is absent, the fallback text is announced again. This follows the same block filtering used by paint. Mounted fallback text is also excluded from canvas text editing. Enter, clicks, caret movement and selections target the visible text regions; headless commands can still update the canonical fallback for portable output. The playground controls card is selectable, movable, resizable and rotatable. Its content belongs entirely to the widget, so it declares `supportsText: false` and `textEditable: false`. The inspector exposes its registered custom values, fill, border and locking, without paragraph or font controls. Declare these choices with `ShapeCapabilities`, so inspector and transform commands agree. Focused native controls own their keyboard input; Escape cancels the active value edit and returns focus to the canvas when it reaches the slot host. Starting a value edit from a pointer-only control gives its slot keyboard focus when no child already has it, so Escape can cancel a slider drag too. The slot does not add a Tab stop, and an already focused child keeps its focus. CDX dropdowns participate in Tab traversal and open with Enter or Space. While typing with an input method, composition owns navigation and confirmation keys; the dropdown resumes its normal keyboard behavior after composition ends. CDX formatted text controls defer filtering until composition finishes. Custom text controls should preserve the controller's composing range and avoid applying unfinished text as a diagram value. Idle offscreen slots may unmount. Focused or pointer-active slots remain mounted until their interaction ends, preserving control state during panning. Deletion, unmounting, read-only/locked transitions and pointer cancellation cancel active edits. Release widget resources in `dispose`; do not store Flutter objects in shape values. Fallback layers provide portable geometry and text for export. They are static declarations, with no automatic value binding or Flutter-widget conversion. Export uses these layers, not widget screenshots. See [Output](/agent-docs/api/output.md) for SVG text and resource limits. ## Backgrounds and pointer ownership The shape definition owns the node background and border. Keep the native widget root transparent (for example, `Material(color: Colors.transparent)`). Padding, labels and other space without a control gesture select and drag the owning node. Sliders, buttons, dropdowns and text fields retain their native gestures. Keep decorative overlays inside `IgnorePointer`. A full-slot `GestureDetector` or `AbsorbPointer` can block diagram input. Interactive controls stay outside that wrapper. Use the shape’s fill and border rather than painting a duplicate widget background. --- # Multi-selection and arrangement [Open runnable example](/examples-app/#/basics/arrangement) Start with three differently sized objects selected. **Align top** aligns their upper edges, then **Space evenly** equalizes the horizontal gaps while keeping the outer span. **Overlap** aligns their horizontal centers. **Moon to front** selects the moon and raises it above the other objects. Each content operation is a separate undo unit. Selection changes do not add to history. After selecting only the moon, distribution is denied: select all three again before retrying. Reset restores the original positions and empty history. ## Session commands | Call | Behavior | | --- | --- | | `selectAll()` | Select eligible document elements without modifying content. | | `align(DiagramAlignment.top)` | Align the current movable selection to its aggregate top edge. Requires at least two objects. | | `distribute(DiagramDistribution.horizontal)` | Equalize gaps between the selected objects. Requires at least three. | | `align(DiagramAlignment.horizontalCenter)` | Align object centers along the horizontal axis; their vertical positions are retained. | | `reorder(DiagramLayerOrder.front)` | Bring selected objects in front of their siblings. | Commands return `DiagramPolicyDecision`; this example uses `requireAllowed()` so the shared shell displays denials. Hosts can use `canAlign`, `canDistribute` and `canReorder` to enable controls, while execution still goes through admission. The example uses no app-specific geometry or ordering algorithm. ```dart session.selectElements(['moon', 'cloud', 'comet']); session.align(DiagramAlignment.top).requireAllowed(); session.distribute(DiagramDistribution.horizontal).requireAllowed(); session.undo().requireAllowed(); // Revert distribution; retain alignment. ``` Source: `apps/demo/lib/examples/basics/arrangement.dart`. For ownership and nested selections, see [frames and ownership](/agent-docs/examples/containment.md). This fixture does not qualify large-document gesture performance. --- # Finite and infinite canvas [Open runnable example](/examples-app/#/canvas/bounds) Choose **Bounded**, then **Far away**. The camera clamps the requested position to the finite world bounds. Switch to **Infinite** and repeat: the camera can leave the drawing behind. **Fit** returns to the document contents. The finite region is `(-240, -160, 1000, 600)` in world coordinates. It constrains camera navigation; it is not a parent frame and does not rewrite, clip or delete document elements. A viewport larger than the bounded region can pan within the range that keeps the region covered, as defined by the shared camera policy. Configuration and camera changes do not enter document undo history. Use an owning frame's clipping grammar when the document itself needs clipped content. See [Canvas API](/agent-docs/api/canvas.md) and [Navigation](/agent-docs/api/navigation.md). ## Source --- # Locks and capabilities [Open runnable example](/examples-app/#/basics/capabilities) The regular rectangle supports standard transforms. Lock it, select it and try **Move regular** or **Delete selection**: the command reports rejection without changing the document. Unlock it to resume editing; lock changes are undoable. The fixed-size card has no resize handles and does not rotate because its registered definition declares those limits. **Resize fixed** demonstrates the same rejection through the API. It remains selectable and movable. Instance unlocking does not override definition capabilities. Locking is an editing constraint, not authentication. Application permissions belong in the host admission hook; see [events and permissions](/agent-docs/examples/hooks.md). ## Source See [Shapes](/agent-docs/api/shapes.md) for the capability grammar and [Session](/agent-docs/api/session.md) for lock, selection and transform method contracts. --- # Copy, paste and duplicate [Open runnable example](/examples-app/#/integration/clipboard) The two cards and their bound, labeled connection belong to one group. **Copy original** captures an immutable subtree without adding history. **Paste** inserts that snapshot below the original, with new element, story and block identities. The copied connection binds to the copied cards. **Duplicate original** combines capture and paste. Each paste or duplicate is one undo unit; Undo removes the complete copy and Redo restores it. Repeated pastes use the same offset, so copies can overlap; drag a group to separate them. This demonstrates the toolkit's in-memory clipboard payload. It does not request browser clipboard permissions or demonstrate cross-application clipboard formats. Reset clears the example's captured payload along with its drawing and history. ## Source See [Session](/agent-docs/api/session.md) for copy, paste and duplication contracts. --- # Connected cards [Open runnable example](/examples-app/#/connections/cards) Move or resize a card and watch its connection follow the bound ports. Switch between straight, orthogonal, quadratic, and Bezier routes. Select the connector and press Enter to edit its label, or use **Select label** to target it directly. Router changes are validated transactions and can be undone. **Animate** starts a transient path effect; **Stop animation** stops it without editing the document. Reset creates a new session with empty undo history and clears its animations. The fixture uses `createShape`, `createConnector`, and named `BoundEndpoint` ports. Label selection uses `selectConnectorLabel`; path motion uses `startPathAnimation` and `stopPathAnimation`. Router changes use `setConnectorRouter`, sharing the inspector's validation and undo behavior. See [Connectors](/agent-docs/api/connectors.md), [Animation](/agent-docs/api/animation.md), and [Undo/redo](/agent-docs/api/history.md). ## Source --- # Create and connect atomically [Open runnable example](/examples-app/#/connectors/create-connected) `session.insertConnectedElement` inserts the target and incoming connector in one admitted operation. The target can be a registered shape or standalone text. Both use the same connection geometry and history pipeline. Try each creation button, then Undo: the target and connector disappear together. Redo restores both identities and the binding. **Try invalid port** requests a nonexistent port; the example reports the error and leaves the drawing unchanged. Use Fit after adding several targets to bring the expanded drawing into view. The method returns a record containing `elementId` and `connectorId`; the inline element keeps its supplied ID. Connection policy and document admission apply. Like `addElement`, authored insertion preserves supplied values rather than invoking the defaults factory. ## Source See [Shapes](/agent-docs/api/shapes.md) for the complete argument contract. --- # Connection admission [Open runnable example](/examples-app/#/connectors/admission) Connect the sender to the listener using the toolbar or ports. The quiet node rejects connections. Rejected ports turn red; releasing cancels the connection. Denied commands show a reason and leave history unchanged. Try undo and redo on an accepted connection. First, **Run checks** in the review panel. The missing-route finding selects both nodes when clicked. Create the connection, then run checks again to clear it. Undo restores the incomplete draft; another check finds the missing route again. Draft diagnostics are explicit and do not prevent editing. Admission rules reject invalid connections immediately. See [domain diagnostics](/agent-docs/api/diagnostics.md). The example uses one endpoint rule at two complementary boundaries: | API | Responsibility | | --- | --- | | `DrawingSession(connectionPolicy: ...)` | Evaluate hover, preview, creation and reconnection intent. `targetIsStart` preserves direction when moving the source endpoint. | | `DrawingSession(documentValidator: ...)` | Enforce the same invariant on initial content and every candidate document, including imports and history. It is fixed for the source session's lifetime. | A port hover without an opposite endpoint is allowed until a pair can be checked. This example requires bound endpoints, so a rejected target cannot leave a free connection behind. Application permissions can additionally use `DrawingConfiguration.hooks.beforeChange`. The validator scans connections in this small fixture. It demonstrates admission semantics, not a large-document performance policy or remote authorization. See [Session](/agent-docs/api/session.md) and [Events and hooks](/agent-docs/api/hooks.md). ## Source --- # Connector labels Connector labels are the words attached to a line. In a crowded drawing they can overlap equipment shapes or each other and become unreadable. **Arrange labels** moves the words into nearby free space; it does not reroute the line or move the equipment. It runs only when requested, not during every drag. Choose **Crowd labels** to place two labels together over a shape, then **Arrange labels** to move them into available space. Undo restores their previous positions in one step. Try the same sequence with plain text and large badges. ```dart session.arrangeLabels().requireAllowed(); // Or arrange selected connections while respecting all other labels. session.arrangeLabels(connectorIds: ['journey'], spacing: 12).requireAllowed(); ``` `requireAllowed()` means "throw an error if this edit is rejected." The example shell catches that error and displays its message. Applications can instead check the returned result's `allowed` and `reason` properties. Arrangement uses measured text and badge bounds, preserving placements that already fit. Positions are saved with the drawing and remain manually editable. This explicit action does not continuously reposition labels while you edit. It avoids leaf shapes and other labels, using a bounded set of nearby positions; it does not guarantee a global optimum or avoid every crossing line. If no placement fits, the operation reports a message and changes nothing. Locked connectors and connectors owned by groups are rejected in this initial version. [Open runnable example](/examples-app/#/connectors/labels) A picnic journey starts with one label. Select the connection and press Enter to edit its first label; double-click a label to edit that label. The framework owns those interactions. Enter adds a new line; Escape finishes editing. Labels support plain paragraphs and inline formatting, without lists. The editing outline stays visible while typing, and an empty label reserves one character's width. Use **Add label** for a second attachment. The default grammar permits two; a third request shows a denial without editing the document. **First label** and **Last label** choose the target for the other actions. With no label selected, actions target the first remaining label. | Action / public API | What changes | | --- | --- | | `addConnectorLabel` | Creates one attachment with its text and path fraction as one undo step. | | `selectConnectorLabel` | Selects an individual label without changing history. | | `updateConnectorLabel(..., fraction: ...)` | Moves the selected label along the path. You can also drag it. | | `updateConnectorLabel(..., story: ..., presentation: ...)` | Changes that label's text size/color and badge together; preserves the other label and connector endpoints. | | `removeConnectorLabel` | Removes one label. Undo restores its text, position and style. | **Large badge** applies 20px text and a subtle bordered surface. **Plain text** returns to 14px text without a backing surface. The example offers fixed choices to demonstrate the API; the playground inspector exposes more styling controls. Try styling the second label, moving it, deleting it, then stepping backward through Undo. Reset restores the initial document with empty history. JSON persistence preserves both labels and their presentation. See [Connectors](/agent-docs/api/connectors.md) and [Text](/agent-docs/api/text.md). With accessibility enabled, each nonempty visible label has its own **Connector label** target. Activating it selects that label; Enter edits it and Delete uses the same protected command as canvas interaction. The whole connector remains available as an aggregate target. ## Source --- # Frames and ownership [Open runnable example](/examples-app/#/basics/containment) The outer frame owns a nested frame and a rectangle. The nested frame owns an ellipse that extends beyond its right edge and a small sensor rectangle; clipping hides the ellipse's overflow. Both connect to the sibling rectangle. Each frame has a single-line title above its outline. Collapsible frames automatically show a bordered toggle to the left: **+** expands and **−** collapses. Frames without collapse support show only their title. The collapsed frame uses a blue fill; its contents retain their saved geometry. The control also exposes accessible Collapse and Expand actions. Pressing it and releasing outside cancels the action. - **Move outer** moves the complete tree once. - **Receiver → Collapse receiver**, after collapsing Equipment cell, shows the bundle count at both ends. Expand either frame to inspect its original shapes. - **Collapse inner frame** hides both children and bundles their two connections into one, with a **2** badge at the compact frame. **Expand inner frame** restores both connections to their original children. Saved geometry and bindings stay intact. - **Resize inner frame** changes its boundary to reveal or clip more of the ellipse. The ellipse's position and dimensions stay unchanged. - **Group contents** groups the equipment and receiver frames. The ellipse stays owned by the inner frame, and clipping is preserved. **Ungroup selection** removes the selected group while keeping its children in place. - **Remove inner frame** removes the container and transfers its child to the outer frame, preserving its geometry. The ellipse's previously clipped edge becomes visible. - **Delete outer tree** deletes the container and all its descendants. - **Undo** restores each operation, including ownership and clipping. Selection and manual transforms use the same public commands as the buttons. Reset restores the original document with empty undo history. Grouping requires at least two elements with the same immediate owner: selecting the ellipse and the sibling rectangle cannot group them across their different owners. Standard frames disable rotation. A host can explicitly enable `rotatable` on a registered frame definition; rotation then carries its descendants with it. Resizing a logical group scales its contents. Resizing a selected frame only changes its boundary, using `ShapeTransformBehavior(resizesChildren: false)`. ```dart session.setFrameCollapsed('inner', true); session.setFrameCollapsed('inner', false); ``` Collapsed frames stay movable. Expand them before resizing or editing their contents. Collapse and expand each form one undo step; save/load retains the collapse state and the expanded geometry. The frame registration uses an external title layer in both states and a blue fill for the compact presentation. Both read the same saved title. Bundling is opt-in on the frame through `connections: CollapsedConnectionPresentation.bundled`. Connections also need the same explicit `bundleKey`, such as `equipment-telemetry` in this example. Only compatible connections with the same projected endpoints, direction, appearance and routing settings combine. Connections with authored route points remain separate. The count describes original connections; bundling never deletes or rewrites them. When both endpoints attach to collapsed frames, each end shows the count. Canvas rendering, hit testing and SVG export share these badge bounds. Select the bundled connection and open Properties to see its original connection IDs and endpoints. Property controls apply to the selected representative only; changing it can separate it from the bundle. The canvas accessibility label also announces the number of connections. Expand the frame to edit members individually. Collapsed composition reuses the ordinary shape primitives: rows, columns, stacks, surfaces and text. Text slots must already exist in the expanded definition; collapsing changes which slots are visible, not their content. `size`, `style`, `outline`, and `textBounds` can also customize the compact frame. ## Source See [Session](/agent-docs/api/session.md) for removal, selection and transform contracts. --- # Make a crowded network readable Three links pass through a narrow corridor, where their names overlap on the actual paths. Equipment can also block the direct path between two devices. This example shows the two capabilities that address those problems. [Open runnable example](/examples-app/#/connectors/dense-network) 1. **Arrange labels:** the three overlapping connection names move into separate readable positions. Devices and connections stay in place. 2. **Move equipment:** the orange device moves into the backbone connection. The path stays in place, making the obstruction visible. 3. **Reroute connections:** the elbow now detours around the equipment. Move the equipment away, then reroute again to restore the direct route. 4. **Undo:** label arrangement, equipment movement and rerouting are separate edits. **Reset** restores the original crowded corridor for another comparison. Drag a blue device: its label follows its connector. Labels are anchored to a fraction of the path, with a small nearby offset; they are not fixed canvas text. After rerouting, use **Arrange labels** again if the new paths create overlaps. Label arrangement and obstacle rerouting are explicit commands. Ordinary dragging keeps connections attached without repeatedly searching for obstacle-free paths. ```dart session.connect('access', 'core', router: ConnectorRoute.orthogonal); final result = session.arrangeLabels(); if (!result.allowed) { // Show result.reason in your application's feedback area. } final rerouting = session.rerouteConnections(); if (!rerouting.allowed) { // Show rerouting.reason; the command leaves all connections unchanged. } ``` These are diagram editing capabilities. The network is illustrative; it does not discover devices or fetch live telemetry. See [routing and labels](/agent-docs/api/connectors.md) for current limits and result handling. ## Arrange inside a frame Choose **Containment → Frame network**, then use **Arrange labels**, **Move equipment**, and **Reroute connections** as before. The frame owns the devices and links; the planners respect active ancestor clips. **Collapse network** hides internal links; **Expand network** restores their geometry. Each action supports undo. Reset returns to the unframed fixture. ## Review the network design Open **Network review → Disconnect backbone**, then select **Run checks** in the diagnostics panel. The finding asks you to connect Access switch to Core router; selecting it locates the broken connector (or both devices if the connector was deleted). Choose **Connect to wrong device**, then frame and collapse the network and run checks again: selecting the finding selects the visible frame without expanding it. Choose **Reconnect backbone** and run checks again to clear the finding. Undo brings the disconnected draft back. This rule checks an explicit connection requirement using canonical endpoint bindings. A line ending near the router does not count as connected. The host owns the rule; the engine supplies revision-matched findings and undoable edits. It does not simulate network traffic or perform reachability analysis. ## Source --- # SVG and PNG downloads [Open runnable example](/examples-app/#/output/export) Edit the cards or connector label, then download SVG or PNG. Output captures the resolved document with padding, independent of camera zoom and selection. Neither format includes editor handles or toolbar controls. SVG uses native text and embeds the bundled font faces. PNG has a white background and a scale capped at two pixels per world unit, with its longest side limited to 4096 pixels. Resource preparation finishes before capturing the crop and publication. Downloads run locally in the browser. This fixture includes shapes, text regions, a bound connector and label, plus two embedded image resources. The round photo uses a registered elliptical outline to clip its cover image; the rectangular image uses contain. Resize either photo and compare the two exports. The live CDX **Select the cards** button uses its canonical caption as a portable text fallback in both outputs; exports do not include its click action. SVG text appearance depends on the fonts available to the viewer. See [Output](/agent-docs/api/output.md) for resource limits and failure handling. ## Source ### Document and shape definition Source: [fixture.dart](https://github.com/vyuh-tech/vyuh_diagram/blob/main/apps/demo/lib/examples/output/fixture.dart). ### Portable widget content Source: [portable_widget.dart](https://github.com/vyuh-tech/vyuh_diagram/blob/main/apps/demo/lib/examples/output/portable_widget.dart). ### Font resources The playground and this example share the same bundled font loader. Source: [export_fonts.dart](https://github.com/vyuh-tech/vyuh_diagram/blob/main/apps/demo/lib/export_fonts.dart). --- # Your first drawing Create and edit shapes through one `DrawingSession`. This focused example uses the same canvas, selection, text editing and undo commands as the playground. [Open runnable example](/examples-app/#/basics/first-drawing) ## Try it Click **Add shape**, drag the selected rectangle, delete it and undo the deletion. Double-click a shape's text to edit it. **Reset example** disposes this example's session and starts a fresh fixture. Source: `apps/demo/lib/examples/basics/first_drawing.dart`. See [Session](/agent-docs/api/session.md) for exact method signatures. ## Source --- # Floor plan [Open runnable example](/examples-app/#/custom-shapes/floor-plan) A simple house layout made from editable room rectangles and registered wall, door and window shapes. Use **Add piece** for rooms, straight walls, L walls, bracket walls, doors, windows, plain lines and dimensions. Drag, resize or rotate a piece; double-click room names and dimension labels to edit them. Undo and reset use the same controls as the other examples. The grid has a 4-unit base resolution. When zooming out hides intermediate lines, shape movement, resizing, and free connector endpoints snap to the displayed spacing instead of invisible subdivisions. Use the connector toolbar button to draw connections. Bound endpoints stay attached to their shapes. Plain lines use the standard `Shapes.line` registry definition: they are standalone strokes without arrowheads or endpoint bindings. Dimensions use a straight connector with arrowheads at both ends and a label at its midpoint. Labels are manually authored: this example does not calculate real-world measurements or automatically join walls and openings. --- # Flowchart [Open runnable example](/examples-app/#/custom-shapes/flowchart) Drag shapes to reroute their connectors. Double-click node or branch labels to edit them. Use the toolbar to add a process, decision, start/end or note; drag between ports to connect them. Undo restores edits. --- # Fonts and inheritance [Open runnable example](/examples-app/#/text/fonts) Choose a drawing default to compare the four bundled Google Fonts. The card titles, card bodies and connector label inherit the default. The standalone caption explicitly uses IBM Plex Mono, so changing the default preserves it. `DrawingConfiguration.supportedFonts` supplies one catalog to all text inspectors. `defaultFontFamily` applies to runs without an explicit family. Changing this host configuration does not rewrite the document's runs or add a document undo entry. The host must load the fonts; a catalog entry alone does not download a font. This example uses font assets already bundled with the examples app. See [Configuration](/agent-docs/api/configuration.md) and [Text](/agent-docs/api/text.md). ## Source --- # Range and whole-story formatting [Open runnable example](/examples-app/#/text/formatting) **Bold picnic** toggles bold on the first occurrence of “picnic”, preserving the word's underline and color. **Italic story** toggles italic across both the paragraph and checklist. **Clear picnic** targets the same word and clears marks and explicit appearance only in that first range. These buttons demonstrate explicit API offsets; if you edit the text, the range still refers to offsets 0–6, not a word search. Double-click the text to select a different range and try keyboard formatting. The action buttons do not represent or capture the current caret selection. **Save**, edit, then **Restore** verifies that mixed formatting is document content. Undo and redo operate on each admitted change; Reset restores the original fixture. ## Public commands ```dart final before = session.document.textById('invitation')!.textStory; final after = DiagramTextCommands.toggleMarkInRange( story: before, start: 0, end: 6, mark: TextMark.bold, ); session.text.replace( elementId: 'invitation', before: before, after: after, ).requireAllowed(); ``` The pure command constructs a story; `session.text.replace` validates and publishes it with history. A stale `before` story fails without overwriting newer text. Locked or noneditable targets return a denied decision; `requireAllowed` throws `DiagramChangeDenied`. Use `setMarkAcrossStory` for whole-story marks and `clearFormattingInRange` to clear a range. No second text model is created. Source: `apps/demo/lib/examples/text/formatting.dart`. See [text and paragraphs](/agent-docs/api/text.md) for caret, range, region and grammar rules. This example does not qualify browser IME or screen-reader behavior. --- # Drawing and erasing [Open runnable example](/examples-app/#/basics/freehand) Choose **Draw**, pick a stroke style and drag on empty canvas. Draw clears the selection so style choices configure future strokes. Choose **Select**, select one or more completed drawings, then change their style. The stored points and width samples remain unchanged; Undo restores the previous appearance. The Properties inspector provides three drawing presets: | Preset | Stroke behavior | | --- | --- | | Pen | Smoothed contour, pronounced pressure variation, round caps. | | Brush | The same pressure variation as Pen, with tapered ends. | | Marker | Constant width and round caps, independent of pressure. | Presets share the same captured contour and pressure samples. Changing a preset does not discard pressure, so switching from Marker back to Brush restores its variation. Brush tapers use fixed distances relative to stroke width. The inspector exposes color and width alongside the preset, without advanced tuning. New drawings start at 6 pixels. Mouse input simulates pressure by speed: slow movement thickens Pen and Brush, while fast movement thins them. Styluses use their reported pressure. Both presets capture a 30–170% width range; Brush adds tapered ends. Marker ignores those width samples when painting. While Draw is active, editing a selected drawing's appearance also sets the next stroke's appearance. Editing in Select mode changes only the selection. ```dart session.setShapeCreationStyle( Shapes.freehand, session.shapeCreationStyle(Shapes.freehand).copyWith( inkPreset: InkPreset.brush, strokeWidth: 8, ), ); ``` Choose **Eraser** and drag across a drawing. Undo restores the erased element and its original geometry. Reset restores the sample and clears history. The tool buttons use the standard registered draw, select and eraser tools. | API | Purpose | | --- | --- | | `setActiveTool(DiagramTools.draw)` | Activate the registered freehand gesture behavior. | | `setActiveTool(DiagramTools.eraser)` | Activate the registered eraser behavior. | | `shapeCreationStyle(Shapes.freehand)` | Read the current defaults for future strokes. | | `setShapeCreationStyle(type, style)` | Change creation defaults without rewriting existing elements or adding document history. | | `setShapeStyle(id, style)` | Change a completed drawing through registry admission and undo. | | `setShapeStyles(stylesById)` | Restyle multiple drawings atomically with one undo entry. | The style buttons apply to the entire selection of drawings. Mixed selections containing other element types report an error; they do not perform partial edits. Solid, dashed and dotted styles are shown. The sample has explicit variable-width geometry; live strokes use the framework's pressure/speed sampling rules. Source: `apps/demo/lib/examples/basics/freehand.dart`. The regression draws and erases with pointer gestures and checks undo restoration; a separate case verifies defaults and completed-stroke style ownership. This does not certify physical stylus pressure, browser latency or every pointer device. --- # Grids and snapping [Open runnable example](/examples-app/#/canvas/grids) Drag a card with **Snap on**, then compare with **Snap off**. **Hide** removes the background without changing snapping. Choose 20 or 40 world units for the shared interval. Dots, lines and the custom cross pattern use that same interval; the custom pattern adds a major line every five intervals. All controls update `DrawingConfiguration.grid` through the session. They do not rewrite document elements or enter document history. Moving a card remains an ordinary admitted edit and can be undone. Reset restores the fixture and its initial visible, snapping grid. See [Grids API](/agent-docs/api/grids.md) for normalization, layer limits and rotation snapping. ## Source --- # Transactions and undo Move two connected cards as one operation using the public drawing session. [Open runnable example](/examples-app/#/integration/history) ## Try it 1. Click **Preview 40**, then **Preview 80**. Both offsets are measured from the original positions; the second preview does not accumulate the first. 2. **Commit**, then **Undo** and **Redo**. Both cards restore together, and their connector follows the resolved ports. 3. Undo again, preview a move and **Cancel**. The original positions and existing redo branch remain available. 4. **Move pair 40** demonstrates a single synchronous transaction without previews. The session owns the edit lease and cancels it on disposal, including example reset. The example retains only the lease token, not a second document snapshot. An expired lease rejects further previews and commits. Buttons report denied commands through the shared example error surface. ## Source See [Undo, redo and transactions](/agent-docs/api/history.md) for admission, history ownership, selection restoration and revision behavior. --- # Events and permissions [Open runnable example](/examples-app/#/integration/hooks) The first card is protected by a host admission hook. Try deleting it with the button, or moving and editing it on the canvas. The second card and connection remain editable. A rejected command displays its reason and adds no undo entry. Change the connection, undo it, and redo it to compare an admitted operation. `DrawingConfiguration.hooks.beforeChange` checks the candidate before publication. UI gestures, semantic commands and document imports share this boundary. The example protects one identity; a real host supplies its own authorization context. This local hook does not replace authorization on a remote server. The change set is derived lazily and scans the documents when read. Avoid using this example as a performance policy for very large documents without profiling. See [Events and hooks](/agent-docs/api/hooks.md) and [LLM documentation](/agent-docs/agents/index.md). ## Source --- # Images Images use the standard selection and transform mechanics with an undecorated image surface. This example embeds two small original monochrome landscapes so it works without an external image server. [Open runnable example](/examples-app/#/basics/images) ## Try it Choose **Day** or **Night**, then try **contain**, **cover** and **fill**. Contain preserves the full image, cover crops to the frame, and fill stretches to the frame. Changing the image preserves the chosen fit. **Clear** removes the source; undo restores it. Use the canvas resize and rotation handles to transform it. This example uses canonical data URIs, not temporary browser blob URLs. File pickers, uploads, remote-resource permissions and large-image memory budgets are host responsibilities and are not demonstrated here. ## Source The fixture constants are in `apps/demo/lib/examples/basics/image_samples.dart`. See [image content](/agent-docs/api/shapes.md#image-content) for the command's admission and history contract, and [output](/agent-docs/api/output.md) for resource preparation during export. --- # Examples Explore a capability, try its controls, and use the linked API in your application. Each page contains a focused interactive example. **Open in new tab** opens the example gallery with navigation. For a complete editing surface, open the [playground](/playground/). For coding-agent reference files, see [Agents](/agent-docs/agents/index.md). ## Essentials | Example | Try it / observable result | API | | --- | --- | --- | | [Your first drawing](/agent-docs/examples/first-drawing.md) | Create, select, move and delete a few shapes using one session and canvas. | [Session](/agent-docs/api/session.md), [Canvas](/agent-docs/api/canvas.md) | | [Transform modifiers](/agent-docs/examples/transforms.md) | Resize from edges/corners, rotate, preserve ratio with Shift and resize from the center with Alt. | [Session](/agent-docs/api/session.md) | | [Multi-selection and arrangement](/agent-docs/examples/arrangement.md) | Select, align, distribute, change stacking order and undo separate edits. | [Session](/agent-docs/api/session.md) | | [Frames and ownership](/agent-docs/examples/containment.md) | Group and ungroup siblings, move nested contents, clip overflow, remove a frame while keeping children, or delete the complete tree. | [Shapes](/agent-docs/api/shapes.md) | | [Departmental swimlanes](/agent-docs/examples/swimlanes.md) | Arrange department frames in rows or columns without scaling tasks; collapse a lane to show its summary. | [Session](/agent-docs/api/session.md) | | [Locks and capabilities](/agent-docs/examples/capabilities.md) | Compare instance locking with a definition that disables resizing and rotation. | [Shapes](/agent-docs/api/shapes.md), [Hooks](/agent-docs/api/hooks.md) | | [Images](/agent-docs/examples/images.md) | Add/replace image content, transform it, and inspect its default undecorated appearance. | [Shapes](/agent-docs/api/shapes.md) | | [Drawing and erasing](/agent-docs/examples/freehand.md) | Pick stroke defaults before drawing, edit the finished stroke and erase with undo. | [Shapes](/agent-docs/api/shapes.md) | ## Custom shapes | Example | Try it / observable result | API | | --- | --- | --- | | [Flowchart](/agent-docs/examples/flowchart.md) | Edit a decision flow with branches, return paths and labels. | [Shapes](/agent-docs/api/shapes.md), [Connectors](/agent-docs/api/connectors.md) | | [Floor plan](/agent-docs/examples/floor-plan.md) | Add rooms, straight/L/bracket walls, doors, windows and editable dimension arrows. | [Shapes](/agent-docs/api/shapes.md), [Connectors](/agent-docs/api/connectors.md) | | [Topology graph](/agent-docs/examples/topology.md) | Edit titles and status, toggle editability, hover for details, and compare Circular and native ELK layouts. | [Graph layout](/agent-docs/api/layout.md), [Shapes](/agent-docs/api/shapes.md) | ## Text and paragraphs All examples use the same shared paragraph implementation, including when it is inside a shape or connector. The font catalog is drawing-wide. | Example | Try it / observable result | API | | --- | --- | --- | | [Single-line and multiline regions](/agent-docs/examples/regions.md) | Edit a card title/body and compare admitted body lists with rejected title lists. | [Text](/agent-docs/api/text.md), [Shapes](/agent-docs/api/shapes.md) | | [Range and whole-story formatting](/agent-docs/examples/formatting.md) | Format a range versus whole text; preserve mixed marks through save/load. | [Text](/agent-docs/api/text.md) | | [Paragraphs, lists and checklists](/agent-docs/examples/paragraphs.md) | Edit wrapping paragraphs, numbered/bulleted/checklist items and nesting; save and restore their structure. | [Text](/agent-docs/api/text.md) | | [Fonts and inheritance](/agent-docs/examples/fonts.md) | Configure the shared font catalog/default and compare explicit versus inherited font choices. | [Configuration](/agent-docs/api/configuration.md) | ## Connections and motion | Example | Try it / observable result | API | | --- | --- | --- | | [Crowded network](/agent-docs/examples/dense-network.md) | Arrange overlapping labels, move equipment into a connection, then explicitly reroute the elbow around it. | [Connectors](/agent-docs/api/connectors.md) | | [Connected cards](/agent-docs/examples/connected-cards.md) | Switch four routers on bound cards, select a label, and start/stop a path animation. | [Connectors](/agent-docs/api/connectors.md) | | [Anchors and contour routing](/agent-docs/examples/routing.md) | Compare straight, quadratic, cubic and elbow routes at center/edge anchors; inspect contour termination. | [Connectors](/agent-docs/api/connectors.md) | | [Explicit ports](/agent-docs/examples/ports.md) | Connect fixed and instance-defined ports; observe hover/snap feedback and owner transforms. | [Shapes](/agent-docs/api/shapes.md), [Connectors](/agent-docs/api/connectors.md) | | [Connector labels](/agent-docs/examples/connector-labels.md) | Enter/double-click to edit, add a second label, move it along the path and style/delete each label. | [Connectors](/agent-docs/api/connectors.md) | | [Connection admission](/agent-docs/examples/connection-admission.md) | Allow/reject source/target combinations through one endpoint rule for interaction, commands and imported documents. | [Hooks](/agent-docs/api/hooks.md) | | [Connected creation](/agent-docs/examples/connected-creation.md) | Create a target and incoming connector atomically, including standalone text; undo both together. | [Shapes](/agent-docs/api/shapes.md) | | [Path effects](/agent-docs/examples/path-effects.md) | Compare pulse and particles; vary count, size, speed and direction independently. | [Animation](/agent-docs/api/animation.md) | The crowded-network example demonstrates explicit obstacle rerouting; the routing example demonstrates endpoint anchors and contours. Port-label editing and effects on every freehand path are outside the current example scope because those capabilities are not complete. ## Canvas and tools | Example | Try it / observable result | API | | --- | --- | --- | | [Navigation and animation](/agent-docs/examples/navigation.md) | Jump or animate to cards/bounds, set normal scale and interrupt animation without editing content. | [Navigation](/agent-docs/api/navigation.md), [Animation](/agent-docs/api/animation.md) | | [View-only diagrams](/agent-docs/examples/view-only.md) | Select, pan, scroll and zoom without moving or editing diagram elements. | [Canvas](/agent-docs/api/canvas.md) | | [Grid snapping](/agent-docs/examples/grids.md) | Switch dot/line/custom patterns, spacing and increments; vary visibility independently of snapping. | [Grids](/agent-docs/api/grids.md) | | [Finite and infinite canvas](/agent-docs/examples/bounds.md) | Compare bounded camera policies with unrestricted workspace navigation. | [Canvas](/agent-docs/api/canvas.md) | | [Snap guides](/agent-docs/examples/snap-guides.md) | Align neighboring sides and centers with a toggleable guide overlay. | [Grids](/agent-docs/api/grids.md) | | [Snap guide policy](/agent-docs/examples/snap-policy.md) | Choose sides, centers and nearby candidate limits. | [Grids](/agent-docs/api/grids.md) | | [Minimap policies](/agent-docs/examples/minimap.md) | Compare outlines/fills, optional connections and custom thumbnails, including filled text boxes. | [Canvas](/agent-docs/api/canvas.md) | | [Custom tools](/agent-docs/examples/tools.md) | Register a tool beside built-ins, with cursor/shortcut and shared transaction/preview behavior. | [Configuration](/agent-docs/api/configuration.md) | ## Widgets and host integration | Example | Try it / observable result | API | | --- | --- | --- | | [Controls inside a node](/agent-docs/examples/widgets.md) | Use a slider, compact dropdown and button; drag non-interactive space and resize/rotate the node. | [Widgets](/agent-docs/api/widgets.md) | | [Custom properties](/agent-docs/examples/widget-properties.md) | Edit registered values through the inspector and the widget; observe one canonical value and undo history. | [Widgets](/agent-docs/api/widgets.md) | | [Widget clipping and layering](/agent-docs/examples/widget-layers.md) | Overlap nodes, clip a widget within a frame, and verify input follows visible content after pan/zoom. | [Widgets](/agent-docs/api/widgets.md) | | [Portable widget fallback](/agent-docs/examples/widget-fallback.md) | Compare live controls with their declared portable appearance in output. | [Widgets](/agent-docs/api/widgets.md), [Output](/agent-docs/api/output.md) | | [Events and permissions](/agent-docs/examples/hooks.md) | Inspect semantic changes, deny an edit, and confirm no partial mutation or extra history entry. | [Hooks](/agent-docs/api/hooks.md) | | [Transactions and undo](/agent-docs/examples/history.md) | Preview/commit/cancel a multi-element operation; undo and redo one logical edit. | [Undo and redo](/agent-docs/api/history.md) | | [Copy, paste and duplicate](/agent-docs/examples/clipboard.md) | Duplicate owned contents with fresh identities and retained text/connection relationships. | [Session](/agent-docs/api/session.md) | | [Session lifetime](/agent-docs/examples/lifetime.md) | Unmount, dispose a borrowed session, remount the retained document, and undo an earlier edit. | [Ownership](/api/ownership) | ## Persistence, output and scale | Example | Try it / observable result | API | | --- | --- | --- | | [Save and load](/agent-docs/examples/persistence.md) | Round-trip JSON with rich text and custom values; reject an invalid or stale replacement. | [Persistence](/agent-docs/api/persistence.md) | | [Explicit migrations](/agent-docs/examples/migrations.md) | Upgrade a small older custom schema before canonical import; show failure without partial state. | [Persistence](/agent-docs/api/persistence.md) | | [SVG and PNG](/agent-docs/examples/export.md) | Export the same fixture with text, labels, images, clips and widget fallbacks. | [Output](/agent-docs/api/output.md) | | [Service map and storyboard](/agent-docs/kits/starters.md) | Compare two compositions built from public registrations and hooks, without engine special cases. | [Starter kits](/agent-docs/kits/starters.md) | --- # Session lifetime [Open runnable example](/examples-app/#/integration/lifetime) Move a card, choose **Unmount**, then **Dispose session**. **Mount** creates a new canvas session attached to the same source session. Your content and undo history remain; **Undo** restores the move. **Reset example** disposes both sessions and creates a fresh drawing. The host waits for the canvas to unmount before disposing its session. The source session is owned separately, so disposing its Flutter adapter releases its presentation resources and subscriptions without destroying the document. All resources are disposed when this example leaves the widget tree. The host creates its source session with `FlutterDiagramTextLayoutEngine`, so wrapping or replacing the session retains the same canonical shaped text geometry. This is an ownership example, not independent multi-user collaboration. Borrowed sessions share selection, history and interaction admission. Ordinary hosts can instead use `DrawingSession(document: ...)` and dispose that session when finished. See [Ownership](/api/ownership) and [Collaboration](/agent-docs/api/collaboration.md). ## Application ownership For a normal application, keep one session in your host state. Unmounting the canvas retains that session's content and history. Dispose it when the host ends: ```dart final session = DrawingSession( document: DiagramDocument(id: 'drawing', revision: 0, elements: []), ); // Mount and unmount a DrawingCanvas(session: session) as needed. // When the host is finished and the canvas has unmounted: session.dispose(); ``` To retain a drawing across Flutter adapter replacements, create a source session with the pure Dart package and attach it using `DrawingSession.fromSession(source)`. Dispose the canvas adapter before disposing its source. This uses the same public commands and canonical drawing in both environments. --- # Upgrade a saved document [Open runnable example](/examples-app/#/data/migrations) Try **Load without migration** to see the legacy type rejected, then **Import legacy cards** to upgrade it. The upgrade preserves identities, text, ports and connections. Undo reverses the import; the invalid fixture leaves the drawing unchanged. The host schema version is separate from the diagram JSON format version. Decode first, run the explicit migration chain, then publish the complete candidate through `replaceDocument` with a captured revision. No intermediate document is published. A host stores its schema version alongside the JSON; this example supplies that envelope in memory. The fixture intentionally accepts only its known legacy card type. Production migrations must cover their actual supported schema and preserve or deliberately remap all references. Upgrade callbacks should perform no external side effects. See [Persistence and imports](/agent-docs/api/persistence.md#host-schema-migrations). ## Source --- # Minimap policies [Open runnable example](/examples-app/#/canvas/minimap) Choose **Filled** or **Outlines** to change thumbnails. **Connections** also shows the resolved connector; **Custom cards** installs a painter for the registered card type. The transparent standalone text has a visible thumbnail in both modes. The extension reads the same resolved scene as the canvas. Custom thumbnail paint receives resolved bounds and an outline, with ancestor and shape clipping already applied. It changes minimap presentation, not document geometry or export. Click or drag the minimap to navigate. Policy changes and camera navigation do not add document undo entries. Replace the policy when external thumbnail inputs change so the minimap can invalidate its cache. Reset restores the filled policy. ## Source The complete minimap and zoom controls default to the bottom-left with 12-pixel edge margins. Configure placement when installing the extension: ```dart DiagramMinimapExtension( alignment: Alignment.bottomLeft, margin: const EdgeInsets.only(left: 20, bottom: 24), initiallyExpanded: true, ) ``` Use any `Alignment` to anchor the overlay elsewhere. The same alignment and margins apply when collapsed or expanded. --- # Navigation and camera animation [Open runnable example](/examples-app/#/navigation/camera) **Jump to idea** uses `navigateToElement`. **Animate to result** and **Animate overview** use `animateToElement` and `animateToRectangle`. Their two-second duration makes interruption easy to observe. **Stop camera** calls `stopViewportAnimation`; **Normal scale** uses `navigateToPoint` with `zoom: 1`. Camera operations are presentation state. They require a mounted canvas, do not edit the document, and do not create undo entries. The example awaits animation futures. Reset disposes its old session and ignores late action feedback from it. See [Navigation API](/agent-docs/api/navigation.md) for return values and coordinate spaces, and [Animation API](/agent-docs/api/animation.md) for interruption and lifecycle behavior. ## Source --- # Paragraphs, lists and checklists [Open runnable example](/examples-app/#/text/paragraphs) Double-click the text to edit. Enter continues the current list; Tab nests it and Shift+Tab outdents it. Start a paragraph with `[] ` to create a checklist. Use **Save snapshot**, make an edit, then **Restore snapshot**. The snapshot is local to this example and uses the versioned JSON codec. Restore uses the session's revision-checked import and is undoable. Reset clears the snapshot and returns to the original document with an empty history. The fixture is a canonical `TextElement` containing paragraph, ordered-list, nested bullet, and checked/unchecked checklist blocks. It uses the same text editing pipeline as text inside shapes and cards. See [Text API](/agent-docs/api/text.md) and [Persistence](/agent-docs/api/persistence.md). ## Source --- # Path effects [Open runnable example](/examples-app/#/motion/path-effects) Start an effect, then move either shape. Playback follows the current resolved connection, including its bend and contour clipping. | Control | API property | What changes | | --- | --- | --- | | Particles / Pulse | `kind` | Moving circles or a fading stroke along the route. | | 3 / 9 particles | `count` | Particle spacing, preserving each particle's size. | | Small / Large | `size` | Circle radius of 4 or 8 world units. | | Slow / Fast | `period` | A 3-second or 900-millisecond loop. | | Forward / Reverse | `reverse` | Direction of particle travel. | | Stop | `stopPathAnimation('delivery')` | Removes the effect and restores base-path paint. | Pulse does not consume particle count or size; those choices are retained for switching back to particles. Dense particles can overlap. All playback uses one stable animation ID, so updates replace the effect rather than stacking copies. Controls also start playback if it has stopped. Animation is presentation state: it is excluded from document JSON, undo history, SVG and PNG. Moving a shape remains an ordinary undoable document edit. See [Animation](/agent-docs/api/animation.md) for screen/world sizing and custom effect registration. ## Source --- # Save, restore and revision conflicts [Open runnable example](/examples-app/#/data/persistence) 1. **Save** captures the canonical JSON and current revision in memory. 2. Edit the text or choose **Remove list**. 3. **Try captured revision** rejects the now-stale update without changing the drawing or its undo/redo history. 4. **Restore saved snapshot** explicitly replaces current content. Undo restores the edits you had before replacement; redo reinstates the saved content. The fixture includes nested lists and checklists. Their structure, formatting and checked state are serialized, rather than flattened into display text. Capture a revision before waiting for asynchronous work. A stale agent response must be reviewed or replanned; do not silently retry with the latest revision. The separate Restore button here expresses a deliberate replacement decision. This example stores its snapshot in memory only. Reset or reload clears it. File storage, transport and server-side authorization belong to the host. See [Persistence and imports](/agent-docs/api/persistence.md) for validation, migrations and failure contracts, and [Agents](/agent-docs/agents/index.md) for integration guidance. ## Source --- # Explicit ports [Open runnable example](/examples-app/#/connectors/ports) The sender's ports belong to its shape definition. The receiver's two ports are authored on the element and saved with the document. Both use the same resolved geometry, connection tools and binding model. | API | Role | | --- | --- | | `ShapeConnectionDefinition.fixedPorts(...)` | Declares stable ports for every element of a type. | | `ShapeConnectionDefinition.instancePorts(...)` | Allows each element to supply its own ports. | | `Port` | Stores an ID, side, offset and appearance; geometry is resolved by the framework. | | `BoundEndpoint(elementId: ..., portId: ...)` | Connects to a port by identity. | | `session.reconnect(...)` | Changes one endpoint as an undoable, validated edit. | | `session.setConnectorRouter(...)` | Changes routing while preserving bound port identities. | Choose **Receive below**, then Undo. Change routers, move or rotate either node, and hover over a port to start another connection. The example has no custom port painters, hit regions or routing code. Ports remain attachment intent; visible routes stop at the owner contours. Orthogonal routing negotiates the bound shapes; it does not promise arbitrary scene-wide obstacle avoidance. ## Source --- # Single-line and multiline regions [Open runnable example](/examples-app/#/text/regions) A card owns one canonical story with two named slots. The title is single-line; the body accepts paragraphs and lists. Double-click each region to edit it: Enter finishes the title, while Enter creates another block in the body. **Add body checklist** appends a new block in the body with a fresh identity. **Try title checklist** submits an invalid story through the same public command. The shared grammar rejects it before publication, preserving document, selection and history. The shell displays the error; Undo remains available only for prior successful edits. Reset starts with the original two blocks and empty history. ## Declare the slots The built-in card already declares `title` and `body` using `ShapeTextLayerDefinition`. A custom shape uses those same definitions: | Definition field | Purpose | | --- | --- | | `slotId` | Matches canonical blocks to the named region. | | `singleLine` | Restricts the region to a line and determines Enter behavior. | | `bounds` | Places the region within the shape's resolved geometry. | The story's title block has `role: TextRole.title` and `slotId: 'title'`. Body blocks use `slotId: 'body'`. All regions share the drawing's font catalog and canonical text commands; widgets do not maintain separate text. ```dart final before = session.document.shapeById('card')!.textStory!; final item = TextParagraph( id: DiagramTextCommands.nextBlockId(before), kind: ParagraphKind.checklistItem, slotId: 'body', runs: [TextRun('Remember the blanket')], ); session.text.replace( elementId: 'card', before: before, after: before.copyWith(blocks: [...before.blocks, item]), ).requireAllowed(); ``` JSON retains block identities, roles, slots, marks and list kinds. Use Undo to restore an earlier edit or restore a saved JSON snapshot. Source: `apps/demo/lib/examples/text/regions.dart`. See [paragraph grammar](/agent-docs/api/text.md#grammar-configuration). --- # Anchors and contour routing [Open runnable example](/examples-app/#/connectors/routing) Switch routers and target anchors, then move or rotate either shape. Drag curve handles to adjust bends. Bézier handles snap near quarter intervals of the anchor span, with a tolerance that stays constant on screen as you zoom. An anchor expresses attachment intent. The visible route stops at the occupied shape contour, including when the anchor lies in the center or on a far edge. The example does not draw its own curve, guides, hit regions or arrowheads. | Control / API | Behavior | | --- | --- | | `setConnectorRouter(id, kind)` | Changes between straight, orthogonal, quadratic and Bezier; keeps endpoint identities and bend intent. Router-specific control points are cleared. | | `reconnect(id, atStart: false, endpoint: ...)` | Updates the target's normalized anchor as one undoable edit. | | Center / left / right / top / bottom | Selects `(0.5, 0.5)`, `(0, 0.5)`, `(1, 0.5)`, `(0.5, 0)` or `(0.5, 1)` in target-local coordinates. Rotation is resolved by the framework. | | Select connection | Shows the router's standard editing controls. Drag the bend handle for curved routes. | Try the right or bottom target anchor while the source remains above-left. The connection must terminate when it reaches the target contour. Undo restores each anchor or router edit separately. Reset restores the selected quadratic fixture. The source uses a center anchor in this fixture; the target controls demonstrate all five anchors. Orthogonal routing negotiates the bound owners, not arbitrary unrelated obstacles throughout the diagram. Overlapping owners require different handling and are outside this example's fixed layout. See [Connectors](/agent-docs/api/connectors.md) for attachment and router configuration. ## Source --- # Service map [Open runnable example](/examples-app/#/kits/service-map) Try the registered tools, edit text and undo your changes. This composition uses the same canonical commands and geometry as the other [starter kits](/agent-docs/kits/starters.md). Hover an icon to see its action. --- # Snap guides [Open runnable example](/examples-app/#/canvas/snap-guides) Drag either card near the other. A guide appears when their sides or centers align, and the card snaps into place. Toggle **Snap to shapes** to compare free movement. Release to commit; undo restores the previous position. The optional `DiagramSnapGuidesExtension` draws passive overlays without intercepting pointer input. Guides disappear when the gesture finishes. Alignment applies to selection moves, not resizing. See [Snap guide policy](/agent-docs/examples/snap-policy.md) for feature and proximity controls, or [Grids API](/agent-docs/api/grids.md) for configuration. ## Source ```dart import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:material_ui/material_ui.dart'; import 'package:vyuh_diagram_kit/vyuh_diagram_kit.dart'; import '../connections/cards.dart'; import '../example_canvas.dart'; import 'grids.dart' show configureGuides, guidePolicy; Widget buildExample({bool policyControls = false}) => ExampleCanvas( title: policyControls ? 'Snap guide policy' : 'Snap guides', instructions: 'Drag a card near another card to align their sides or centers. Toggle guides to compare free movement. Change the feature policy and nearby candidate limit to explore alignment.', createSession: () => DrawingSession( document: connectedCardsDocument(), configuration: DrawingConfiguration( extensions: [const DiagramSnapGuidesExtension()], ), ), actionGroups: { if (policyControls) 'Guide features': ['Sides and centers', 'Sides only', 'Centers only'], }, selectedActions: (session) => { if (guidePolicy(session).sides && guidePolicy(session).centers) 'Sides and centers' else if (guidePolicy(session).sides) 'Sides only' else 'Centers only', '${guidePolicy(session).maxCandidates} candidates', if (session.canvasConfiguration.extensions .whereType() .any((extension) => extension.enabled)) 'Guides on' else 'Guides off', session.canvasConfiguration.grid.pattern.label, session.canvasConfiguration.grid.visible ? 'Show' : 'Hide', session.canvasConfiguration.grid.snap ? 'Snap on' : 'Snap off', "${session.canvasConfiguration.grid.size.toInt()} units", }, labeledToggles: {'Guide candidates'}, actionToggles: { if (policyControls) 'Guide candidates': ( on: '24 candidates', off: '8 candidates', icon: FluentIcons.grid_20_regular, activeIcon: FluentIcons.grid_20_filled, ), 'Snap to shapes': ( on: 'Guides on', off: 'Guides off', icon: FluentIcons.align_center_horizontal_20_regular, activeIcon: FluentIcons.align_center_horizontal_20_filled, ), }, actions: { if (policyControls) 'Sides and centers': (s) => configureGuides( s, true, policy: guidePolicy(s).copyWith(sides: true, centers: true), ), if (policyControls) 'Sides only': (s) => configureGuides( s, true, policy: guidePolicy(s).copyWith(sides: true, centers: false), ), if (policyControls) 'Centers only': (s) => configureGuides( s, true, policy: guidePolicy(s).copyWith(sides: false, centers: true), ), if (policyControls) '8 candidates': (s) => configureGuides( s, true, policy: guidePolicy(s).copyWith(maxCandidates: 8), ), if (policyControls) '24 candidates': (s) => configureGuides( s, true, policy: guidePolicy(s).copyWith(maxCandidates: 24), ), 'Guides on': (session) => configureGuides(session, true), 'Guides off': (session) => configureGuides(session, false), }, ); ``` --- # Snap guide policy [Open runnable example](/examples-app/#/canvas/snap-policy) Choose **Sides and centers**, **Sides only**, or **Centers only**, then move a card beside the other. Sides use the left, right, top and bottom of resolved bounds; centers use their horizontal and vertical midpoints. The candidate menu compares limits of **8** and **24** nearby shapes. The spatial index finds shapes intersecting the viewport, then the nearest candidates are ranked and limited before alignment comparisons. A smaller limit reduces comparisons but may skip a useful alignment. It does not cap the spatial query or its ranking cost. The visible viewport defines the candidate pool. Zooming out includes more world space and potentially more candidates; panning changes the pool. The snap tolerance stays at 6 screen pixels as you zoom. The candidate cap defaults to 24; the API accepts 1–256. Policy changes preserve other settings and do not enter document undo history. See [Snap guides](/agent-docs/examples/snap-guides.md), [grid snapping](/agent-docs/examples/grids.md), and [configuration](/agent-docs/api/grids.md#neighbor-snap-guides). Guide appearance is configured with `DiagramSnapGuidesExtension(color: const Color(0xFF5260FF))`. You can supply any Flutter color. The example controls preserve the configured color when changing policy or toggling guides. --- # Storyboard [Open runnable example](/examples-app/#/kits/storyboard) Try the registered tools, edit text and undo your changes. This composition uses the same canonical commands and geometry as the other [starter kits](/agent-docs/kits/starters.md). Hover an icon to see its action. --- # Departmental swimlanes Each department is an ordinary frame. Tasks belong to that frame through their saved `parentId`; connections keep their task bindings when lanes move. Swimlanes use the same parenting, unparenting, movement and optional collapse as frames. Clipping is a separate choice: enable `clipsChildren` on the definition and `clipContent` on a lane to hide overflow while expanded. This example leaves clipping disabled. `arrangeLanes` arranges the frames, not the tasks inside them. [Open runnable example](/examples-app/#/basics/swimlanes) For domain review, choose **Task ownership → Detach review task**, then **Run checks**. Select the finding to locate the task and Quality department. **Assign review to Quality** repairs the ownership; run checks again to clear the finding. Undo restores the draft. This host-defined rule reports incomplete work without blocking edits or adding domain policy to the engine. - **Arrange columns** places departments side by side. Their frames change size; task sizes stay unchanged. - **Arrange rows** restores horizontal lanes in Production, Quality, Release order. Both arrangements leave a 32-unit gap between departments. - **Collapse Quality** shows the department summary and redirects external links. **Expand Quality** restores its task before arranging the lanes again. - **Undo** restores an entire lane arrangement in one step. - **Command-drag a task** to transfer it between departments, or outside all departments to release it. Use Control on other platforms. Its bounds only need to intersect the destination; the destination outline is highlighted. Ordinary dragging keeps its current parent. Escape cancels movement and ownership changes. ```dart session.arrangeLanes( ['production', 'quality', 'release'], direction: DiagramLaneDirection.rows, laneSize: const DiagramSize(720, 180), gap: 32, ); ``` The ID order determines lane order. Omit `laneSize` to preserve existing sizes; `gap` defaults to zero. The arrangement starts at the current lanes' upper-left corner. Lanes must be expanded, unrotated sibling frames. Moving a locked or fixed child rejects the whole operation. Resizing a lane does not automatically fit its contents: hosts choose the lane size and clipping behavior explicitly. Transfer ownership with `session.reparentElements(['task'], parentId: 'quality')`. This preserves world positions; it changes membership without silently laying out the task. Pass no parent to return objects to the root. Transfers, lane movement, deletion, save/load and undo use the ordinary document commands. To move and transfer together, pass a world-coordinate `delta`. The engine carries descendants without resizing them and records the entire operation as one undo: ```dart session.reparentElements( ['task'], parentId: 'quality', delta: const DiagramPoint(32, 48), ).requireAllowed(); ``` A locked descendant rejects the movement without partially changing ownership or positions. Omit `delta` to change membership while preserving world positions. On macOS, **Command+G** groups a selection, **Command+Option+G** frames it, and **Command+Shift+G** removes selected groups or frames while keeping their contents. Use Control instead of Command on other platforms. Removing a collapsed frame releases its children as well; Undo restores the container and membership. ## Source --- # Register a custom tool [Open runnable example](/examples-app/#/canvas/tools) Choose **Checkpoint** or press **K** while the canvas has focus, then click or drag to create a checkpoint. Its label, minimum size and appearance come from the shape definition. Creation returns to selection and adds one undo entry. The ordinary rectangle tool remains available alongside it. The example registers a new tool identity and reuses `DiagramShapeTool`, the same behavior as built-in shape tools. There is no editor switch on the custom type and no second drawing or history implementation. Duplicate identities or shortcuts fail registration rather than silently replacing a default. For a different interaction, implement `DiagramToolBehavior` and use its `DiagramToolContext` mechanics or canonical gesture transactions. This example focuses on reusing existing shape mechanics; it does not demonstrate every custom gesture. Text editing retains ownership of letter shortcuts. `returnToDefault: true` applies to both direct `context.dispatch(...)` tools and gesture tools. An admitted direct dispatch returns after the callback; a gesture returns after its admitted commit. Denied dispatches or commits keep the tool active for retry. Retained callback contexts cannot dispatch after the callback has ended. If the final creation preview is denied, the gesture rolls back and releases the pointer without resetting the active tool or adding an undo entry. See [Configuration](/agent-docs/api/configuration.md) and [Shapes](/agent-docs/api/shapes.md). ## Source --- # Topology graph For a focused before-and-after demonstration of obstacle routing and label arrangement, open [Make a crowded network readable](/agent-docs/examples/dense-network.md). [Open runnable example](/examples-app/#/custom-shapes/topology) A minimal custom shape: an outlined device icon, status dot, title and status line. Straight links bind to the center of each device icon and follow dragging. Hover over an icon for its address and capacity. Both text lines use independent, single-line canonical text regions. Text editing starts disabled. Turn on **Edit text** to edit either line; turning it off keeps nodes movable. Two registered definitions share geometry and content but declare different `textEditable` capabilities. This example switches definitions in one undoable transaction rather than locking the nodes. Choose **5, 10 or 20 nodes** from the dropdown. **Refresh topology** rebuilds that size and simulates a data update to the database status and status dot, including while direct editing is disabled. Manually editing the status text does not infer a new health state: applications own that mapping. The minimap starts expanded and includes connections. Click or drag it to navigate larger topologies. The tooltip is a Flutter widget slot. Its portable image fallback preserves the icon and status dot in exports; transient hover details are not exported. The **Layout** menu compares Circular and native ELK, with downward and left-to-right directional arrangements. These call the shared [graph layout API](/agent-docs/api/layout.md), not example-specific positioning code. ## Network flow Toggle **Network flow** to animate connections and **Particles** to switch between particles and pulses. Choose packet shape, count and speed. Effects follow route changes; refresh and node-count changes preserve settings. Turning flow off leaves document history unchanged. --- # Transform modifiers Transform gestures and API commands share the same canonical geometry and history. [Open runnable example](/examples-app/#/basics/transforms) ## Try it The left rectangle enables `ShapeTransformBehavior(showRotationHandle: true)`. Select the right rectangle to compare the default corner rotation without a separate handle. Hold **Shift** to preserve aspect ratio while resizing or **Alt** to resize around the center. Rotate either shape, then undo. The buttons transform the current selection using move, rotation and centered resize commands. Source: `apps/demo/lib/examples/basics/transforms.dart`. See [Session](/agent-docs/api/session.md) for transform arguments and minimum-size behavior. ## Source --- # View-only diagrams [Open runnable example](/examples-app/#/navigation/view-only) Select shapes and connections, drag to pan, scroll to navigate, or pinch to zoom. Ctrl/Command-scroll zooms around the pointer. Arrow keys pan; Escape clears the selection. None of these operations modifies diagram content. Use `DrawingCanvas(session: session, viewOnly: true)` in either Kit edition. It mounts a separate lightweight renderer without the editor's tools, transform handles, text-input connection, IME or editing shortcuts. The canonical engine, resolved geometry, image/text painting and retained paint caches are shared. The canvas uses the session’s attribution configuration. The view listens to scene, selection, settings and camera changes, so application code can still load or update the diagram through the session. View-only describes canvas interaction, not a restriction on your application's session commands. Custom widget shapes use their portable painted representation in this mode. Editor extensions and editing toolbars are not mounted by the view. ## Source --- # Portable widget content [Open runnable example](/examples-app/#/widgets/fallback) The live CDX button selects both cards. **Show fallback** removes its widget registration, revealing the ordinary text layer declared by the shape definition. **Show live control** restores the interactive button. Both use the same canonical caption; switching presentation does not replace the document or add undo history. | Action | API | Result | | --- | --- | --- | | Show fallback | `session.setConfiguration(configuration.copyWith(widgetRegistry: ShapeWidgetRegistry.empty()))` | Displays registered portable layers instead of native controls. | | Show live control | Configure the widget builder again | Restores the button and its selection action. | | Download SVG / PNG | `session.exportSvg` / `session.exportPng` | Exports portable content in either presentation mode. | The fallback is required in `ShapeWidgetLayerDefinition`. It contains ordinary layer definitions, so layout, clipping and output can resolve it without executing Flutter controls. This example shares a canonical text story between the button and fallback. Arbitrary widget-local state is not captured in output; durable content must be represented by canonical data and declared portable layers. ## Definition and live control ## Session and presentation switch Source: [fallback.dart](https://github.com/vyuh-tech/vyuh_diagram/blob/main/apps/demo/lib/examples/widgets/fallback.dart). See [widgets](/agent-docs/api/widgets.md) and [output](/agent-docs/api/output.md) for the complete contracts. --- # Widget clipping and layering [Open runnable example](/examples-app/#/widgets/layers) The controls card extends beyond its clipping frame. A higher element covers part of the live controls. Hidden and covered controls must not receive pointer input; the visible foreground element owns that space. | Action | Public API | Expected result | | --- | --- | --- | | Move frame | `moveElements(['frame'], delta)` | Moves the child once with its owner; the independent cover stays in place. | | Remove cover | `removeElements(['cover'])` | Reveals controls beneath it. | | Release contents | `removeFrames()` | Removes the selected frame, retaining the widget and its world position. | | Undo | `undo()` | Restores the prior ownership, clipping or cover. | Try dragging the cover, clicking the visible slider, and releasing the contents to reveal the bottom of the widget. Pan and zoom through the normal canvas controls. The example supplies canonical ownership and z-order; the framework owns widget mounting, clipping, hit testing and transforms. The bottom Count button is fully outside the frame and is omitted from the accessibility tree. Releasing the contents exposes it again. Partially visible controls retain their accessibility nodes; clipping follows the resolved path's bounding rectangle for native semantics, while pointer clipping uses the path. ## Source --- # Widget properties [Open runnable example](/examples-app/#/widgets/properties) The selected controls card advertises `progress`, `mode`, and `count` through its shape definition. `DrawingInspector` creates the corresponding property controls; the live widget reads the same values. There is no separate form model to synchronize. | Definition / API | Contract | | --- | --- | | `NumberValueField` | Numeric bounds and integer constraints are validated at canonical publication. | | `TextValueField.allowedValues` | Declares the mode choices. | | `ShapeDefinition.valueFields` | Supplies defaults and field grammar for both commands and inspector. | | `session.updateValues(id, values)` | Changes registered values in one admitted edit. | | `DrawingInspector(session: session)` | Edits selected elements through their declared capabilities and fields. | Change progress or mode in the inspector, then use the widget. **Set ready** updates both fields together; Undo restores both. Locking the card prevents edits through either surface. Text properties are absent because this node has no text capability. The inspector moves below the canvas on narrow example embeds. ## Source The fixture, registry and controls are shared with [Widgets inside shapes](/agent-docs/examples/widgets.md). --- # Widgets inside shapes [Open runnable example](/examples-app/#/widgets/controls) The card contains CDX controls. Drag its non-interactive space to move it, or select it to resize and rotate. The slider, dropdown, and button keep their own interactions. **Set ready** changes two registered values through the session; Undo restores them together. Slider previews commit as one edit. The example composes a `ShapeDefinition`, a `ShapeWidgetLayerDefinition`, and a `ShapeWidgetRegistry`. The values `progress`, `mode`, and `count` are registered fields in the canonical document. There is no separate widget-owned persisted state, and the node has no text story or paragraph controls. The portable output fallback is a simple region, not a screenshot of interactive controls. Hosts must declare an appropriate fallback for their own widget content. See [Widget API](/agent-docs/api/widgets.md) for hit testing, lifecycle, culling, and output contracts. ## Source --- # Diagram kits Diagram kits combine shape definitions, connection rules, custom properties and application adapters into a focused editing experience. They share the canvas, selection, text editing and history APIs. | Kit | Use it to | | --- | --- | | [Workflow](/agent-docs/kits/workflow.md) | Construct workflows using registered operations, human work, decisions, branches, timers and other control nodes. Author input drafts and connect compatible ports. | The host application supplies data, permissions and services. Workflow authoring does not run or simulate workflows. --- # Starter kits Two runnable compositions show how the same diagram API supports different host experiences. Their shared source lives in the unpublished workspace package `apps/starter_examples`. The website loads its gallery as a deferred route; the command-line examples import the same pure Dart compositions. Choose a focused example: [Service map](/agent-docs/examples/service-map.md) or [Storyboard](/agent-docs/examples/storyboard.md). Each page embeds only its own composition. Open it in a new tab to browse the full example collection. Canvas actions use compact icons with tooltips. | | Service map | Storyboard | | --- | --- | --- | | Shapes | A custom service node | A clipping board and owned panels | | Text | Single-line service names | Single-line headings and multiline bodies | | Connections | Named input/output ports with directional policy | No connection affordance on boards or panels | | Tools | Select, Service, Connect | Select, Panel, Text | | Shared behavior | Commands, admission, change events, labels, undo and JSON | Commands, text/list editing, ownership, locks and undo | The examples use public package imports. Shape and tool registries deliberately omit unrelated built-in defaults; custom type and tool IDs use the same grammar and dispatch path as the defaults. ## Run without Flutter From the repository root: ```sh dart --packages=.dart_tool/package_config.json apps/starter_examples/bin/headless.dart services dart --packages=.dart_tool/package_config.json apps/starter_examples/bin/headless.dart storyboard ``` Both produce canonical JSON using the plain Dart VM. Their default text metrics are deterministic bootstrap measurements, not font-accurate server rendering. Restore the JSON with the same registry definitions for its custom shape types. The headless entry point uses `createServiceMapSession()` or `createStoryboardSession()`, then the same `session.document.encode()` and `session.dispose()` API as a Flutter host. Each factory owns its session's registry and document lifetime. ## Embed the same composition The gallery supplies Flutter text layout and mounts a `DrawingCanvas`. For your own Flutter application, create a `DrawingSession` with your document and shape registry, then pass it to `DrawingCanvas(session: session)`. Dispose the session after unmounting its canvas. See [Getting started](/agent-docs/guide/getting-started.md) for a complete host example. ```sh cd apps/demo flutter run -d chrome --target ../starter_examples/lib/main.dart ``` Connection policy governs the service map's connection commands and gestures. Use `beforeChange` for authorization that must also cover advanced raw transactions. Neither example supplies workflow execution, arbitrary embedded Flutter widgets, general schema migrations, SVG/PDF output or complete accessibility; those capabilities should not be inferred from these examples. --- # Workflow building blocks Build workflow diagrams from operations, human tasks and control-flow blocks. [Open runnable example](/examples-app/#/workflow/authoring) Choose a block from the palette, edit its label and inputs, then connect its ports. The example builds workflow drafts; it does not execute them. ## Available blocks | Block | Use it for | | --- | --- | | Start / End | Mark the entry and exit of a workflow. | | Operation | A registered automated task. | | Human work | A task with named responses, such as approve or revise. | | Wait for event | Wait for an external event. | | Sleep / timer | Continue after a delay. | | Decision / switch | Choose a path based on a value. | | Parallel / Race / Quorum | Run branches until all, the first, or a required number finish. | | Branch end | Finish one branch. | | Call / Spawn / Join workflow | Run a child workflow and optionally wait for it. | | Saga | Pair steps with compensating actions. | | For each | Repeat a branch for each item. | | Continue as new | Start another run with new input. | | Query | Read a registered query. | ## Set it up Register the standard blocks and attach the session to a Flutter canvas: ```dart import 'package:vyuh_diagram_kit/vyuh_diagram_kit.dart'; import 'package:vyuh_diagram_workflow/vyuh_diagram_workflow.dart'; final registry = WorkflowAuthoringRegistry([ WorkflowStart(), ...defaultWorkflowPrimitives(), ]); final documentSession = registry.createSession( textLayout: FlutterDiagramTextLayoutEngine(), ); final canvasSession = DrawingSession.fromSession(documentSession); final canvas = DrawingCanvas(session: canvasSession); ``` Use `registry.entries` for your palette. Create a node with its registered shape type: ```dart canvasSession.createShape( type: registry.entries.first.shapeType, worldCenter: const DiagramPoint(100, 100), ); ``` Unmount the canvas, then dispose `canvasSession` followed by `documentSession`. For headless authoring, use `registry.createSession()` without the Flutter adapter. ## Add your own tasks Add `WorkflowOperation` and `WorkflowWork` entries to the registry using your workflow engine contracts. Use `WorkflowResponse` to name the outcomes of human work. The example registers **Send notification** and **Review proposal**. Expand its source for a complete registration example.
Task registration source ```dart import 'package:json_schema_builder/json_schema_builder.dart' as json; import 'package:vyuh_diagram_workflow/vyuh_diagram_workflow.dart'; import 'package:vyuh_workflow_engine/vyuh_workflow_runtime.dart' as runtime; /// Example business contracts compose with the durable control primitives. /// No handlers are installed and no execution service is created. WorkflowAuthoringRegistry exampleWorkflowRegistry() { final attachment = json.Schema.object( properties: {'name': json.Schema.string(), 'url': json.Schema.string()}, required: ['name', 'url'], ); final request = runtime.Schema.identity>( name: 'notification-request', jsonSchema: json.Schema.fromMap({ ...json.Schema.object( properties: { 'recipient': json.Schema.string(), 'message': json.Schema.string(), 'priority': json.Schema.string(enumValues: ['normal', 'urgent']), 'cc': json.Schema.list(items: json.Schema.string()), 'delivery': json.Schema.object( properties: {'sender': json.Schema.string()}, ), 'attachments': json.Schema.list( items: json.Schema.fromMap({r'$ref': r'#/$defs/attachment'}), ), }, required: ['recipient', 'message'], ).value, r'$defs': {'attachment': attachment.value}, }), ); final receipt = runtime.Schema.identity>( name: 'notification-receipt', jsonSchema: json.Schema.object( properties: {'messageId': json.Schema.string()}, required: ['messageId'], ), ); final review = runtime.Schema.identity>( name: 'review-request', jsonSchema: json.Schema.object( properties: { 'title': json.Schema.string(), 'summary': json.Schema.string(), }, required: ['title', 'summary'], ), ); runtime.Schema> response( String name, List decisions, ) => runtime.Schema.identity>( name: name, jsonSchema: json.Schema.object( properties: { 'decision': json.Schema.string(enumValues: decisions), 'comment': json.Schema.string(), }, required: ['decision', 'comment'], ), ); return WorkflowAuthoringRegistry([ WorkflowOperation, Map, Never>( label: 'Send notification', description: 'System work: send a message and return its receipt.', operation: runtime.Operation( name: 'send-notification', input: request, output: receipt, ), ), WorkflowWork, Map>( label: 'Review proposal', description: 'Human work: approve the proposal or request a revision.', work: runtime.Work( name: 'review-proposal', input: review, response: response('review-response', ['approve', 'revise']), ), responses: [ WorkflowResponse( id: 'approve', label: 'Approve', payload: response('approval', ['approve']), ), WorkflowResponse( id: 'revise', label: 'Request revision', payload: response('revision', ['revise']), ), ], ), WorkflowStart(), ...defaultWorkflowPrimitives(), ]); } ```
--- # Workflow kit Build workflow diagrams with a palette of registered tasks, typed connections and editable inputs. The kit supports authoring drafts; it does not execute or simulate workflows. ## Building blocks - **Tasks:** automated operations and human work with named responses. - **Flow:** start/end, decisions, parallel branches, races and quorums. - **Waiting:** events and timers. - **Composition:** child workflows, loops, sagas and queries. See [Workflow building blocks](/agent-docs/kits/workflow-primitives.md) for the complete list, runnable example and setup code. ## Setup 1. Create a `WorkflowAuthoringRegistry` with `WorkflowStart()` and `defaultWorkflowPrimitives()`. 2. Add your own `WorkflowOperation` and `WorkflowWork` entries. 3. Create a session and attach it to `DrawingCanvas`. 4. Use the registry entries for your palette and their schemas for input forms. The session provides selection, connections, save/load and undo/redo. Use `setWorkflowInput` for task inputs and `setWorkflowBranches` for named branches. --- # Capabilities Vyuh Diagram combines editable elements, shared text, connections and extensible tools in one drawing. Use session commands for application actions and definitions to describe custom elements. ## Elements and editing | Capability | What you can do | | --- | --- | | [Shapes](/agent-docs/api/shapes.md) | Create standard or custom shapes; configure geometry, appearance, text regions, properties, ports and transform capabilities. | | [Text](/agent-docs/api/text.md) | Edit standalone text, shape text and connector labels with shared formatting, paragraphs, nested lists and checklists. Use a drawing-wide font catalog. | | [Selection and transforms](/agent-docs/api/session.md) | Select, move, resize, rotate, group, align, distribute, reorder, copy and delete elements. | | [Locks](/agent-docs/api/session.md) | Protect elements and their owned contents from edits while retaining selection. | | [Undo and redo](/agent-docs/api/history.md) | Undo or redo document edits, including text changes, connections and grouped operations. | | [Custom tools](/agent-docs/examples/tools.md) | Register tools that use the same editing and preview contracts as standard tools. | ## Connections and presentation | Capability | What you can do | | --- | --- | | [Connectors](/agent-docs/api/connectors.md) | Connect element anchors or explicit ports with straight, elbow, quadratic or cubic Bézier routes. Apply connection rules and edit route controls. | | [Connector labels](/agent-docs/examples/connector-labels.md) | Add up to two single-line labels by default; edit, style, move and delete them independently. Enter edits the first label or creates one. | | [Navigation](/agent-docs/api/navigation.md) | Pan, zoom, fit content and animate the view toward points, bounds or selected elements. | | [Path effects](/agent-docs/api/animation.md) | Animate particles and pulses along connectors, with configurable appearance and timing. | | [Canvas and grids](/agent-docs/api/grids.md) | Configure canvas bounds, backgrounds, grid patterns and snapping. Compose dot, line and cross layers. | | [Embedded widgets](/agent-docs/api/widgets.md) | Place Flutter controls inside registered elements. Share element values and history while retaining control interaction, transforms and clipping. Supply portable content for export. | | [Accessibility](/agent-docs/api/canvas.md) | Expose element and connector-label selection, text editing and native controls through semantics that follow visible geometry. | ## Integration and output | Capability | What you can do | | --- | --- | | [Events and hooks](/agent-docs/api/hooks.md) | Observe document changes and enforce application permissions or fixed domain invariants. | | [Persistence](/agent-docs/api/persistence.md) | Save versioned JSON and restore documents through validated imports. Register migrations for application-owned schemas. | | [PNG](/agent-docs/api/output.md) | Export a static drawing with transparent or explicit background, chosen bounds and raster scale. Requires Flutter. | | [SVG](/agent-docs/api/output.md) | Export vector geometry, native text, images, clipping and portable widget content. Supply fonts for self-contained text resources. | | [Agents](/agent-docs/agents/index.md) | Generate and update diagrams through commands, validate proposed edits and save JSON or SVG. | | [Diagram kits](/agent-docs/kits/index.md) | Build domain editing experiences such as workflow authoring, service maps and storyboards. | ## Integration boundaries Elbow routing avoids its disjoint endpoint owners; it does not route around every obstacle in a scene. Port labels are not supported. Path effects apply to connectors. PDF export is not available. Widget export uses declared portable content rather than arbitrary native widget pixels. SVG text appearance depends on fonts and the target viewer. Borrowed sessions share interaction and history; they do not provide independent collaborative participants. See [session ownership](/agent-docs/api/collaboration.md) before attaching multiple users or agents.