Skip to content

Shapes, elements and registries ​

Shape elements, Shapes type ids, styles, ports and the rest of the document model are in the main library. Definitions and their parts (ShapeDefinition, ShapeOutline, ShapeText, ShapeColumn, value fields, PortDefinition, ShapeLibrary, ShapeRegistry) are extension authoring types in package:vyuh_diagram_flutter/extension_api.dart (package:vyuh_diagram_core/extension_api.dart in pure Dart).

Describe a shape through these related concepts:

ConceptWhat it describes
OutlineThe shape's own surface: the contour it paints, hit-tests and clips connectors to.
BodyTitle, body and footer regions, arranged using rows, columns, stacks, sizing and padding.
ValuesTyped instance data, such as a path's points, an image or a domain field.
AppearanceFills, stroke styles, widths and colors; text has its own formatting properties.
ConnectionsExplicit named ports or virtual anchors, attachment rules and connection snapping.
BehaviorWhat a person may do on the canvas: select, move, resize, rotate and delete.
Alignment snappingGeometric 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.

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; body 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 anatomy for the parts a body is made of and widgets inside shapes for live Flutter content.

On this page ​

CategoryWhat you can do
Create elementsCreate registered shapes, standalone text or connected elements.
AppearanceSet fills, strokes and other supported styles.
Custom definitionsDeclare a shape's anatomy: outline, body, values, connection and behavior.
Register definitionsMake custom shape types available to the drawing.
Edit and lockUpdate elements while respecting each definition's behavior.
ImagesSet or clear image content.
Custom valuesDeclare and edit fields for controls and domain extensions.

Standard collection ​

See the illustrated element catalog and capability matrix for the built-in elements and their editing support.

The core extensions contribute rectangle, ellipse, polygon, path, card, group, frame, comment, freehand and image. Tables are supplied by the table extension. Standalone text and connectors have their own element definitions. Entities (for example classes) belong to the optional Shapes library (DiagramShapes.entity); comments have a folded top-right corner with fixed yellow fill and outline.

The Connector tool uses L; Path uses P, and Comment uses N. Path covers straight strokes and curves. Click an anchor to select it (Shift-click adds anchors), then Delete removes the selected points. Removing an endpoint from a two-point path removes the path. Double-click an anchor to cycle Corner, Smooth, and Symmetric. All edits support undo/redo.

Use DiagramTools.connector for connections and DiagramTools.path for editable paths.

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. A container declares ShapeContainer(resizesChildren: ...) (default true); the standard frame sets it to false. This policy is shared by controller resize verbs and pointer handles. A group selected around a frame still scales its complete contents as one group transformation. Try frames and ownership.

For rotatable shapes, ShapeBehavior(rotationHandle: true) adds a visible rotation handle. It defaults to false; corner rotation remains available. ShapeBehavior(rotatable: false) disables both. See custom shapes for registration examples.

controller.edits.setStyle({ids, fill, stroke, strokeWidth, cornerRadius, strokeStyle, inkPreset}) performs the setStyle verb, which changes a shape's editable appearance. Only the parts you pass change; the others keep their values. Flutter and pure Dart hosts use this same verb, and the inspector's appearance properties perform it.

Argument / resultContract
idsExisting shape identities; missing IDs and standalone text are denied.
Named partsfill (DiagramFill), stroke (DiagramColor), strokeWidth, cornerRadius, strokeStyle, inkPreset; each optional.
Editable fieldsFill and stroke parts require a shape with an outline; cornerRadius requires an outline that rounds its corners. Other changes are denied.
ReturnEditResult; locks and host admission can deny the edit. Invalid numeric or color values are denied with DiagramDenialCode.unsupportedOperation.
HistoryOne admitted change is one undo unit. Selection, content and geometry remain unchanged.
dart
controller.setStyle(
  ids: [id],
  fill: const DiagramColor(0xFFE4E4E7),
);

Text formatting uses the text verbs; connector appearance has its own model.

Pass several IDs for atomic multi-shape styling in Flutter or Dart. All targets must pass validation and permissions; otherwise none change. A change is one event and one undo step.

dart
controller.setStyle(
  ids: [for (final shape in selectedShapes) shape.id],
  strokeStyle: StrokeStyle.dashed,
);

Initial authored styles and imports retain their separate document-validation contract; the anatomy-derived editable fields govern this verb and the inspector.

Connection declarations ​

ConstructorPort ownership
ShapeConnectionDefinition.none()No connections.
ShapeConnectionDefinition.outline()Connections bind to the shape outline.
ShapeConnectionDefinition.fixedPorts(ports)The definition owns the named ports.
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
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.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 per-controller 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.

Controller APIArgumentsResult
shapeCreationStyle(type)Registered shape type IDCurrent ShapeStyle, falling back to the definition's default
setShapeCreationStyle(type, style)Registered type ID and valid ShapeStyleUpdates 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 controller inserts and changes records through the canonical engine.

TypePackageRole
DiagramElementvyuh_diagram_typesBase authored record with identity, stacking and lock state.
ShapeElementvyuh_diagram_typesA shape instance: type, frame, appearance, values and text.
TextElementvyuh_diagram_typesStandalone text; shared rectangular mechanics without shape appearance.
ConnectorElementvyuh_diagram_typesConnection endpoints, route, markers and labels.
ShapeDefinitionvyuh_diagram_coreA shape's anatomy: outline, body parts, values, connection and behavior.
ShapeRegistryvyuh_diagram_coreDefinition lookup and validation.
DiagramControllervyuh_diagram_coreOne actor's verbs, selection and camera; its diagram holds installed extensions, rules, history and lifetime.

Import package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart in a Flutter host; it exports these types. Pure Dart hosts import package:vyuh_diagram_core/vyuh_diagram_core.dart and use the same controller verbs.

controller.add ​

Use an element directly when you already know its identity, frame and content:

dart
EditResult add(
  DiagramElement element, {
  bool select = true,
})

final result = controller.edits.add(ShapeElement(
  id: 'review-card',
  type: Shapes.card,
  frame: const DiagramRect.fromLTWH(200, 160, 240, 160),
));
Argument / resultContract
elementRequired immutable record. Shapes, text and connectors use this same method.
selectDefaults to true; selects the inserted record. false preserves selection.
ResultEditResult; denied changes return a reason. Call requireAllowed() to throw on denial.
DefaultsPreserves the supplied record; does not invent an ID, story, style, size or parent.
ValidationRegistry, identity, ownership, bindings and the diagram's beforeChange admission still apply. Invalid records throw; no partial insertion occurs.
HistoryOne admitted edit and undo entry. Works before mounting a canvas.

The canCreate rule is not called by raw insertion; a connector inserted this way is still admitted by the canConnect rule. Put permissions that must cover every mutation in the beforeChange rule, which also covers transactions, imports and history.

controller.createShape ​

Use named arguments when the registry should supply defaults.

dart
final cardId = controller.createShape(
  Shapes.card,
  at: const DiagramPoint(320, 240),
  size: const DiagramSize(240, 160),
);

Returns an EditResult; when allowed, id is the created element ID. The type is positional; the rest are named:

ArgumentTypeDefault / behavior
typeStringPositional registered shape type. Standalone text uses createText.
atDiagramPointRequired center in world coordinates.
idString?Allocated when omitted; explicit IDs must be unique.
sizeDiagramSize?Definition's size; requested dimensions are clamped up to its minimum.
styleShapeStyle?Definition's style.
valuesMap<String, Object?>?Overrides for the definition's declared values; the remaining defaults are filled in. A path, drawing or image shape receives its path, ink or image here. See custom values.
textString?Plain Text with generated story and paragraph IDs; line breaks preserve blank and trailing paragraphs. Supply either text or textStory. The registered type must host text; otherwise creation fails without changing content or history.
textStoryTextStory?Materialized from the definition's declared text slots when omitted.
textSizingModeDiagramTextSizingMode?Omit to use the definition’s textCreation.sizingMode; explicit autoWidth opts into intrinsic text width.
portsList<Port>?Empty; admitted by the registered connection grammar.
parentDiagramShapeParentPlacementconst AutoDiagramShapeParent(): deepest accepting owner beneath the center.
selectbooltrue: select the new shape.
zIndexint0.

Use const RootDiagramShapeParent() for explicit root placement, or OwnedDiagramShapeParent(ownerId) for an explicit child-owning shape. Missing or unsuitable explicit owners throw. Registry admission, the canCreate rule and beforeChange run before successful publication. A denial returns a denied result without an id. The operation creates one undo entry and requires no mounted canvas.

controller.createText ​

Creation rules

The canCreate rule 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 diagram's beforeChange rule.

dart
final textId = controller.createText(
  at: const DiagramPoint(320, 240),
  text: 'Your notes',
);

Returns an EditResult whose id is the new ID. Named arguments are at (required), id, size, text, textStory, textSizingMode, parent, select, and zIndex, with the same types/defaults as above. Omitted size and story come from the registered text element, registry.requireText(Shapes.text). There are no fill, border, port, image or path arguments.

Use text: for Plain Text; the controller 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.

controller.createConnected ​

Pass the element you want to insert. The controller adds it and its incoming connection atomically. The element keeps the ID you gave it; the result's id is the connector ID:

dart
final connectorId = controller.createConnected(
  source: BoundEndpoint(elementId: existingShapeId),
  element: ShapeElement(
    id: 'review-card',
    type: Shapes.rectangle,
    frame: const DiagramRect.fromLTWH(420, 190, 200, 100),
  ),
);
ArgumentMeaning
sourceRequired source endpoint: bound to an existing element or a free world point.
elementRequired ShapeElement or TextElement. Its identity, geometry, content, appearance and parent are preserved.
toPortRequired for port-only targets; otherwise omit. Must name a declared target port.
connectorIdOptional explicit ID; allocated when omitted.
routeDefaults to ConnectorRoute.straight.
connectorZIndexDefaults to -1. The connector shares the target's parent.
selectDefaults to true; selects the new element. False preserves selection.
ReturnEditResult; id is the connector ID.

One Undo removes both records; Redo restores their identities and binding. Invalid identities, unsupported targets, missing ports, a canConnect denial or document admission failure insert neither record. No mounted canvas is needed.

Authored elements and defaults

This method preserves an authored element, like add; it does not materialize registry defaults or call the canCreate rule. Use beforeChange for permissions that must cover every insertion. The canConnect rule and canonical document validation still apply. createShape and createText supply registry defaults when you want the controller to construct an element.

ShapeElement ​

Constructor propertyTypeDefault / contract
id, typeStringRequired identity and registry type.
frameDiagramRectRequired authored frame.
rotationdouble0, radians around the frame center.
styleShapeStyleconst ShapeStyle(); raw records do not acquire definition defaults.
valuesMap<String, Object?>Empty immutable value map by default; raw insertion must supply exactly the declared fields. Paths, ink, images and polygon sides live here.
textStoryTextStory?null.
textSizingModeDiagramTextSizingModefixedWidth.
portsList<Port>Empty immutable snapshot; subject to the definition's connection grammar.
parentIdString?null; explicit owner identity when present.
clipContentboolfalse; requires a clipping container (ShapeContainer(clips: true)).
isCollapsedboolfalse; collapsed presentation of a collapsible container.
aspectLockedboolfalse; resizing keeps the width-to-height ratio.
isLockedboolfalse; per-instance editing lock.
zIndexint0; stacking order.

Records are immutable. Change them by performing verbs such as setStyle, setValues or move. Geometry and story data are validated on admission.

Structured values have typed readers. Decode them with the value's own type rather than reading map keys:

dart
final path = DiagramVectorPath.fromValue(shape.values['path']);
final ink = DiagramInk.fromValue(shape.values['ink']);
final image = DiagramImageSource.fromValue(shape.values['image']);

Each type writes itself back with toValue(), for example {'path': DiagramVectorPath.polyline(points).toValue()}.

ShapeDefinition ​

A shape definition is the shape's anatomy: an outline that is its own surface, a body of parts laid out inside or beside it, typed instance values, a connection declaration, optional container and collapse policies, and the behavior a person may exercise on the canvas. Painting, hit testing, text editing, handles and the inspector all follow from these declarations; a definition adds no painter or editor code of its own.

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

final service = ShapeDefinition(
  type: 'acme.service',
  name: 'Service',
  size: const DiagramSize(200, 120),
  outline: const ShapeOutline.rect(),
  style: const ShapeStyle(cornerRadius: 10),
  body: ShapeColumn([
    ShapeSized(
      extent: 36,
      child: ShapeStack([
        const ShapeText(
          'title',
          text: 'Service',
          textType: PlainTextDefinition(maxLines: 1),
          role: TextRole.title,
        ),
        const ShapeDivider(edge: ShapeEdge.bottom),
      ]),
    ),
    const ShapeText(
      'body',
      verticalAlignment: DiagramTextVerticalAlignment.top,
      placeholder: 'Describe the service…',
    ),
  ]),
  values: {
    'replicas': const NumberValueField(defaultValue: 1, minimum: 1, integer: true),
  },
  behavior: const ShapeBehavior(textSizing: ShapeTextSizing.growHeight),
);

Registry admission rejects invalid default appearance, surface 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 propertyTypeDefault / contract
typeStringRequired open registry key.
nameString?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.
outlineShapeOutline?const ShapeOutline.rect(). The shape's surface, hit area and connection boundary; null for a shape that paints no surface of its own.
bodyShapePart?ShapeParts.body: one editable body text slot inset by 12. null for a shape with no text and no parts.
valuesMap<String, ValueField>Empty. Typed declarations of each instance's durable ShapeElement.values.
previewValuesMap<String, Object?>Empty. Values a palette thumbnail shows instead of the defaults.
adjustmentsIterable<ShapeAdjustment>Empty. On-canvas handles bound to numeric values.
connectionShapeConnectionDefinitionconst ShapeConnectionDefinition.outline().
containerShapeContainer?null. Present when the shape owns child elements.
collapseShapeCollapseDefinition?null. A container's compact presentation.
behaviorShapeBehaviorconst ShapeBehavior(): selectable, deletable, movable, resizable and rotatable, fixed text sizing. See shape behavior.
minimumSizeDiagramSize24 × 24, raised to fit a composed body.
sizeDiagramSize160 × 100; the size of a new shape.
styleShapeStyleconst ShapeStyle(); the appearance of a new shape.
shadowShapeShadowDefinition?null. Painted only for a shape with an outline.
fontSizeRange, textCreationText sizing limits and creation defaults.
propertiesIterable<DiagramProperty>Additional inspector properties. See properties.
verbsList<DiagramVerb>The shape's own verbs, such as a card's setTitle, offered beside element verbs and its parts' verbs.
affordancesIterable<ShapeAffordance>On-canvas controls beside the selected shape, such as a card's optional tag.

Outline ​

ShapeOutline is a closed set of contours. Each resolves at the shape's frame from its style and values:

OutlineContourCorner radius
ShapeOutline.rect()Rectangle filling the frame; rect(cornerRadius: false) keeps square corners.Editable
ShapeOutline.ellipse()Ellipse filling the frame.None
ShapeOutline.polygon(points)Fixed closed polygon through unit-coordinate points.None
ShapeOutline.normalized(path)Fixed DiagramVectorPath in unit coordinates, scaled to the frame.Opt in with cornerRadius: true
ShapeOutline.path(builder)Contour computed by (DiagramSize size, Map values) => DiagramVectorPath in shape-local units, so caps and insets keep their size as the frame grows.Opt in with cornerRadius: true
ShapeOutline.regularPolygon()Regular polygon whose side count is the integer value sides, declared with ShapeOutline.sidesField(n).Editable
ShapeOutline.editablePath()The path each instance stores in its path PathValueField, edited point by point.Editable
ShapeOutline.ink()A freehand stroke stored in the instance's ink InkValueField.None
nullNo surface: nothing painted, picked by its frame rectangle.None

A closed, straight-edged path resolves as a precise polygon for hit testing and connector clipping.

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';

final hexagon = ShapeDefinition(
  type: 'acme.hexagon',
  outline: ShapeOutline.regularPolygon(),
  values: {'sides': ShapeOutline.sidesField(6)},
);

What is editable ​

Editing follows from the anatomy rather than from separate flags:

AnatomyDerived editingRead it as
An outlineFill and strokedefinition.paintsSurface
An outline that rounds its cornersCorner radiusdefinition.cornerRadiusEditable
An editable ShapeText, or a content part such as a tableTextdefinition.textEditable (false while collapsed)
Any ShapeText or content partA text storydefinition.hostsText
A PathValueField, sides value or ImageValueFieldPoints, sides or image editorsdefinition.propertyEditors
Any other valueOne control per valuedefinition.customValues

definition.layers lists what the body compiles to, in paint order. Layers are compiled output that renderers, picking and editing read; author shapes with parts, not layers.

A surface or image region's onInteract can name one of the shape's verbs: on the canvas, a tap inside that region performs it with no arguments. Any other onInteract ID goes to the host's region interaction callback.

Shorthand constructors ​

Two shorthand constructors cover the most common definitions:

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

ShapeDefinition.labeled(
  'acme.task',
  name: 'Task',
  outline: const ShapeOutline.rect(),
  connection: const ShapeConnectionDefinition.outline(),
  size: const DiagramSize(180, 100),
);
ShapeDefinition.symbol('acme.start', name: 'Start');

labeled fills the shape with one padded, editable label text slot that starts with the name (or text:). symbol is a small, text-free marker with an ellipse outline by default and an optional glyph part drawn inside it.

TextStory? createTextStory(String elementId) materializes the declared text slots and initial content. It returns null when the shape hosts no text. It does not reset an existing element's story.

Minimum size ​

minimumSize sets the creation and resize floor in world units. size sets the initial size and must meet that floor. Both require positive finite dimensions. Explicit creation sizes below the minimum are clamped. A composed body raises the floor to fit its fixed extents and padding; a body that is a single text part scales with the shape.

DefinitionMinimum width × height
Rectangle, ellipse, polygon, frame24 × 24
Path1 × 1
Line, freehand drawing4 × 4
Card140 × 88
Comment100 × 80
Image80 × 60
Group1 × 1, additionally constrained by its children
Playground controls example280 × 260, to accommodate the native controls

For your own type, declare the minimum with its anatomy:

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

ShapeDefinition(
  type: 'small-marker',
  outline: const ShapeOutline.ellipse(),
  body: null,
  minimumSize: const DiagramSize(8, 8),
  size: const DiagramSize(32, 32),
)

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 ​

A ShapeRegistry is the validated grammar that extensions assemble; it is not constructed directly. Obtain it from the extension assembly:

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';

final registry = DiagramExtensionRegistry(
  extensions: [myShapesExtension()],
).shapeRegistry;
// or, from a diagram:
final installed = controller.diagram.registry.shapeRegistry;

The core extensions always supply the built-in shapes. An extension's ShapeDefinition with a built-in type, such as Shapes.rectangle, replaces the built-in one; two non-core extensions contributing the same type 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. Add creation tools to the same extension's tools, for example with DiagramToolDefinition.forShapes(shapes, group: ...). See custom shapes for a complete example.

Mutation and locking ​

OperationAPIBehavior
Lock / unlockcontroller.edits.setLocked(locked, ids: ids)Returns EditResult; one admitted reversible change.
Deletecontroller.edits.delete(ids: ids)Removes the ownership/dependency closure; locks, behavior.deletable and admission apply.
Movecontroller.edits.move(delta, ids: ids)Translate the specified elements together.
Resizecontroller.edits.resize(size, ids: ids, fromCenter: false, preserveAspectRatio: false)Resize the specified elements with optional centered and proportional sizing.
Rotatecontroller.edits.rotate(ids: ids, pivot: pivot, radians: radians)Rotate the specified elements around a pivot.
Replace textcontroller.edits.perform([id], 'replaceText', {'before': story, 'after': editedStory})Replace a story after validating its expected content.

Each verb publishes and undoes as one operation. For custom atomic edits, see controller.batch. Use beforeChange for permissions; definition behavior and instance locks are not an authorization boundary.

Image content ​

dart
EditResult setImage(String id, DiagramImageSource? image)

Call controller.edits.setImage(id, image) in Flutter or pure Dart; it performs the setImage verb. The verb requires no mounted canvas. This sets the authored source; image fetching and decoding belong to rendering or export.

A shape shows an image by declaring an ImageValueField and placing a ShapeImage(valueKey) part where the image belongs. The built-in image shape is

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';

ShapeDefinition(
  type: 'app.photo',
  outline: null,
  values: const {'image': ImageValueField()},
  body: const ShapeSurface(
    outline: null,
    onInteract: 'choose-image',
    child: ShapeImage('image'),
  ),
)

The stored value is DiagramImageSource.toValue(); read it back with DiagramImageSource.fromValue(shape.values['image']). The choose-image interaction asks the host's image picker for a source.

Argument / outcomeContract
idExisting shape whose definition declares an ImageValueField.
imageImage source to store in that value; null removes it.
Missing shape or no image valueReturns a denied EditResult.
Invalid source URIReturns a result denied with DiagramDenialCode.unsupportedOperation; only HTTP(S) URLs and image data URIs are accepted.
Locked shape or rejected host permissionReturns a denied result without publishing the edit.
Changed contentOne admitted edit and undo entry.
Identical contentAllowed no-op; no content-change event or undo entry.

When the image is chosen after awaiting a file picker or network response, pass expectedRevision to perform so a document that changed meanwhile is denied as a conflict. As with other verbs, 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.

Standalone text is a core element. Extensions contribute text types through textTypes, and every installed text type is also a type of the standalone text element: the element's type is the text type's name, text for the core plain text and richText for RichTextExtension(). The core gives every standalone text element the same standard geometry (placeholder, sizes, font size range, connection and behavior); read it with registry.requireText(type), which returns the TextElementDefinition (an engine type in adapter.dart) the core built for that text type. Do not register a ShapeDefinition with a text type's name or construct ShapeElement(type: Shapes.text).

dart
final controller = DiagramController(
  configuration: DiagramConfiguration(extensions: [RichTextExtension()]),
);
controller.createText(
      type: RichTextDefinition.textType,
      at: DiagramPoint.zero,
      text: 'Notes',
    );

A custom element type name needs a text definition whose type is that name: subclass DiagramTextDefinition and contribute it in textTypes. For text with other geometry, such as a larger box or ports, use a shape with a text slot instead.

ShapeDefinition.textCreation and TextElementDefinition.textCreation use a ShapeTextCreation declaration:

PropertyTypeContract
sizingModeDiagramTextSizingModeDefault for controller creation verbs and drag-created frames; defaults to fixedWidth.
clickSizingModeDiagramTextSizingModeDefault 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 a growing behavior.textSizing 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.

The core's TextElementDefinition for each text type has that textType, an editor-only placeholder (Type something…), standard minimum/default sizes and a behavior. Its frame is filled by one body text slot that is always editable; there is no shape surface, body composition or child ownership. Its default ShapeBehavior(textSizing: ShapeTextSizing.growHeight) grows text height. It does not introduce a second paragraph editor.

Its connection is ShapeConnectionDefinition.outline(). Standalone text accepts center and edge anchors and shares contour clipping with shapes for every route. Text does not declare named ports; use a shape with a text slot when you need ports or no bindings. Its rectangular geometry remains independent of shape fill and border presentation.

Built-in images have no outline, so they expose their pixels across the full frame without fill, border, corner rounding or the corresponding inspector controls. A custom image shape with an outline gains those controls.

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 for the record and persistence for its saved form.

Transform geometry for custom verbs ​

The pure Dart engine exposes geometry operations for custom verbs and tools. They return immutable elements; they do not bypass publication, permissions, ownership closure, or history. Return results from a DiagramVerb.update setter, or submit them from a verb implementation.

APIArgumentsResult
DiagramElementTransform.translateDiagramElement element, DiagramPoint deltaTranslates a shape/text frame or connector free points.
DiagramElementTransform.rotateDiagramElement element, required DiagramPoint pivot, required double radiansRotates placement about a world-space pivot; framed element dimensions remain unchanged.
DiagramResizeTransformRequired DiagramRect fromBounds, DiagramRect toBounds; double rotation = 0Creates one immutable mapping for all elements in a resize selection. Bounds are expressed in its unrotated coordinate frame.
resize.mapPointDiagramPoint pointMaps a world point through the resize coordinate frame.
resize.applyDiagramElement element; optional DiagramRect resolvedFrame, bool fixTextWidth = falseMaps 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; bound connectors follow their endpoints automatically. 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 anatomy guide for how title, body and footer regions, content layout, and widget slots fit inside one shape.

Body containment ​

Surfaces, dividers and images in a body are clipped to their owning shape's resolved outline, including rounded corners and rotated outlines. Interactive surfaces use the same containment: clipped-away corners cannot activate a region. This is a rule of the body, independent of child-frame clipping.

Text is never clipped to the outline; it clips to its own text region, so a slanted or curved edge does not cut through letters. Keep text inside a curved outline with padding, such as ShapePadding.proportional(.18, child: ...). Ports and shadows are separate from the body 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 declares its durable instance data as typed values. The document owns them; Flutter widget state must not become a second source of truth. Mount controls through the widget slot and builder APIs.

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

final definition = ShapeDefinition(
  type: 'control-card',
  values: {
    'progress': const NumberValueField(
      defaultValue: 25, minimum: 0, maximum: 100,
    ),
    'status': ChoiceValueField(
      defaultValue: 'draft', values: ['draft', 'ready'],
    ),
    'enabled': const BooleanValueField(defaultValue: true),
  },
);

// Install the definition in the configuration's extensions first.
final id = controller.createShape(
  'control-card',
  at: const DiagramPoint(200, 150),
  values: {'progress': 50},
);
final result = controller.edits.setValues(id, {'status': 'ready'});

The inspector shows one control per value. Paths, ink, images and a regular polygon's sides have their own editors instead.

APIArgumentsBehavior
ShapeDefinition.valuesMap<String, ValueField>Immutable declarations; unknown fields are rejected.
BooleanValueFieldRequired bool defaultValueAccepts only booleans. A boolean can show or hide a part with ShapeVisibility.value(key).
NumberValueFieldRequired num defaultValue; optional minimum, maximum; integer = falseFinite numbers within inclusive bounds; integer fields require an integral value. Adjustment handles set number values.
ColorValueFieldRequired int defaultValueUnsigned 32-bit ARGB color, such as 0xFF3355FF; uses a color picker. Can be specialized for domain constraints.
ChoiceValueFieldRequired String defaultValue and List<String> valuesRestricts a value to named choices.
TextValueFieldRequired String defaultValue; optional List<String> allowedValuesStrings, optionally restricted to the declared choices.
ObjectValueFieldRequired Map<String, Object?> defaultValueStructured JSON for domain payloads; hosts supply schema-based forms.
PathValueFieldRequired DiagramVectorPath defaultValueAn editable path in unit coordinates, read by ShapeOutline.editablePath() and edited point by point.
InkValueFieldNoneA freehand stroke (DiagramInk), read by ShapeOutline.ink(). Every instance supplies its own ink; there is no default.
ImageValueFieldNoneAn optional image (DiagramImageSource), shown by ShapeImage(valueKey); null until chosen.
definition.createValues(overrides)Optional value mapMaterializes defaults and validates overrides, useful when constructing a ShapeElement directly.
controller.edits.createShape(values: ...)Partial value mapSupplies remaining defaults once during creation.
controller.edits.setValues(id, patch)Shape ID and partial value mapPerforms the setValues verb; returns EditResult; one admitted update is one undo unit.
ShapeElement.valuesRead-only value mapPreserved by copy, duplication, history and JSON.

Direct element insertion must contain every declared field; admission does not silently change supplied records. setValues denies unknown keys and values its field does not accept (unsupportedOperation) without publishing, as it denies host permission and lock rejections. An unchanged patch creates no history entry. Text elements do not accept custom shape values. Use the scalar fields for ordinary editable properties and ObjectValueField only for structured domain payloads.

Custom field validation must be read-only and free of external side effects. During shape creation, grouping, and document admission, attempts to perform 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 ​

In a shape widget, call shape.beginValueEdit() on its widget context, call preview(patch) during a drag, then commit() or cancel(). The returned ShapeValueEdit holds an interaction lease on which each preview performs setValues. Previews start from the original values; commit adds one undo step. Cancellation, denial or owner disposal restores the original values. While the edit is active, other controllers' edits to that shape are denied, and closed edits reject later callbacks.

dart
final edit = shape.beginValueEdit();
edit.preview({'progress': 40});
edit.preview({'progress': 70});
final result = edit.commit();
dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

ShapeValueEdit beginValueEdit() // on ShapeWidgetContext

// Methods and property on the returned edit:
bool get isActive
EditResult preview(Map<String, Object?> patch)
EditResult commit()
void cancel()
MemberContract
beginValueEdit()Requires an enabled context for an existing unlocked shape and an idle controller. Throws StateError when read-only, for a missing or locked shape, or while the controller holds another interaction.
isActiveWhether this edit still owns the interaction. Becomes false after completion, cancellation or controller disposal.
preview(patch)Performs setValues with the patch on the lease, starting from the starting value map. Include all fields intended for the current preview; successive calls do not accumulate patches. Returns the admission result.
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 are denied like rejected admitted operations, so host controls handle one outcome: the result. Cancel an active edit when its control is unmounted or its gesture is abandoned. The widget context adds mounted slot ownership checks. Pure Dart hosts use controller.edits.setValues(id, patch) for discrete edits and controller.beginInteraction(label) for continuous ones, performing setValues on the lease:

dart
final lease = controller.beginInteraction('Edit shape values');
lease.perform([id], 'setValues', {'values': {'progress': 40}});
lease.commit();

Appearance editing limits ​

The inspector bounds shape border width to 32 and corner radius to 200 world units. These limits govern editing controls; they do not normalize imported or programmatically authored appearance values. Connector-label badges declare their own limits through ConnectorLabelingCapability.appearanceEditing.

A clipping container always shows its border so its extent stays visible: when its stroke is fully transparent, the canvas and SVG draw a dark hairline of at least 1.5 units instead. Dividers and image placeholders inside a body use at least a 1-unit stroke. These fallbacks affect the rendered projection only; they do not change the stored ShapeStyle.

Adjustment handles ​

An adjustment is an on-canvas handle bound to a numeric value, such as an arrow's head length. Dragging it sets the value within the field's minimum, maximum and integer bounds, and the outline recomputes from the new value.

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

final arrow = ShapeDefinition(
  type: 'acme.arrow',
  outline: ShapeOutline.path((size, values) {
    final head = (values['head']! as num).toDouble() * size.width;
    return DiagramVectorPath.polyline([
      DiagramPoint(0, size.height * .3),
      DiagramPoint(size.width - head, size.height * .3),
      DiagramPoint(size.width - head, 0),
      DiagramPoint(size.width, size.height / 2),
      DiagramPoint(size.width - head, size.height),
      DiagramPoint(size.width - head, size.height * .7),
      DiagramPoint(0, size.height * .7),
    ], closed: true);
  }),
  values: {
    'head': const NumberValueField(defaultValue: .3, minimum: .1, maximum: .6),
  },
  adjustments: [
    ShapeAdjustment(
      field: 'head',
      label: 'Arrowhead length',
      position: (values) =>
          DiagramPoint(1 - (values['head']! as num).toDouble(), 0),
      direction: (_) => const DiagramPoint(-1, 0),
    ),
  ],
);
ShapeAdjustment argumentContract
field, labelRequired. The NumberValueField the handle sets, and its accessible name.
step, pixelsPerStep.01 per 4 pixels of scrubbing by default.
handleShapeAdjustmentHandle.diamond or circle.
positionThe handle's unit-coordinate position for the current values; null places it on the frame's top edge.
directionThe shape-local direction that increases the value; null scrubs right/up.
dragMaps a unit-coordinate pointer delta to a value, or returns null to scrub.
snapStopsMeaningful values for magnetic snapping, or strict snapping with Shift.

The adjustable shapes use adjustments for arrows, stars, arcs and callouts.

Plain line ​

Shapes.line is a standard shape: an open editable path that starts as a horizontal stroke through its frame center. Drag its endpoints to change its length and direction, or rotate it. It has no text, ports, arrowheads, or connector endpoint bindings. DiagramTools.connector remains the existing connector tool.

dart
controller.edits.createShape(
  Shapes.line,
  at: 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.

A path shape stores its geometry in its path value, a PathValueField that its ShapeOutline.editablePath() outline reads. Read it with DiagramVectorPath.fromValue(shape.values['path']); the path verbs below write it. Declare your own editable path shape the same way:

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

final route = ShapeDefinition(
  type: 'acme.route',
  outline: ShapeOutline.editablePath(),
  body: null,
  values: {
    'path': PathValueField(
      defaultValue: DiagramVectorPath.polyline(
        const [DiagramPoint(0, 1), DiagramPoint(.5, 0), DiagramPoint(1, 1)],
      ),
    ),
  },
  behavior: const ShapeBehavior(resizable: false),
);
dart
final pathId = controller.createPath(
  at: const DiagramPoint(240, 160),
  size: const DiagramSize(240, 160),
  path: DiagramVectorPath.polyline(
    const [DiagramPoint(0, 0), DiagramPoint(1, 0), DiagramPoint(1, 1)],
    closed: true,
  ),
);
controller.edits.setPathSegmentKind(
  pathId, 'node-0', DiagramPathSegmentKind.cubic,
);
controller.edits.setPathHandleMode(
  pathId, 'node-1', DiagramPathHandleMode.symmetric,
);
controller.edits.addPathPoint(pathId, after: 'node-0', id: '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.

The path verbs are setPathClosed, setPathHandleMode, setPathSegmentKind, addPathPoint, removePathNodes, and movePathControl. Each returns an admission result and records one undo entry when allowed. Invalid node counts, IDs or coordinates are rejected without changing the document. addPathPoint 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 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)),
  ],
);
controller.edits.setStyle(ids: [pathId], 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. A custom definition offers corner-radius editing whenever its outline rounds corners: ShapeOutline.rect(), regularPolygon(), editablePath(), or a path/normalized outline declared with cornerRadius: true. Body surfaces also accept DiagramFill.

ShapeStyle.fill is a DiagramFill: a solid DiagramColor, a gradient or no fill. To read a solid color, pattern-match DiagramColor(:final argb); to choose a contrasting color for any fill, use fill.representativeColor.

Simple paths and declarative inspectors ​

Use controller.edits.createLine(start: ..., end: ...) for two world-space points, or controller.edits.createPolyline(points: ..., closed: true) for a polygon. Open paths require two points; closed paths require three. Both return a result whose id is the element ID, and create one undo step. Use controller.createPath when you need explicit normalized nodes, tangents and frame geometry.

controller.edits.addPathPoint(id, after: nodeId) splits a segment without changing its curve; the result's id is the new point ID. setPathHandleMode converts that point to a corner, smooth or symmetric handle.

ShapeDefinition(type: 'card') provides a rectangle with inset text. Its inspector's standard editors always follow its anatomy. Inspector visibility does not change permissions. See common tasks for examples.

The inspector's shape properties follow the anatomy alone: clipping for a clipping container, points for an editable path, fill and stroke for an outline, corner radius for a rounded outline, sides for a polygon, one standard control per declared value, an image and text. Hosts add their own through a DiagramPropertySource. See properties and the inspector.

Compound shapes and libraries ​

Use ShapeDefinition.body to arrange reusable parts with ShapeRow, ShapeColumn, ShapeStack, ShapeSized, ShapePadding and ShapeOutside, around leaves such as ShapeText, ShapeSurface, ShapeDivider, ShapeImage and ShapeWidget. A body contributes minimum dimensions and compiles into the shape's layers. See shape anatomy for layout rules, named text slots, parts, and libraries.

Tables ​

The table extension contributes a standalone table shape (TableExtension.shapeType). Embed the same content in another shape with ShapeTable('members', rows: 2, columns: 1) inside its body. The table fills that declared region; wrapped cell content grows its row and the owning shape. Columns start equally spaced and wrap text at their assigned width.

A story owns TextTable elements with nested DiagramTableRow and DiagramTableCell elements. Cells accept body paragraphs with inline styling; headings, lists, and nested tables are rejected by the grammar.

In the editor, Enter adds a row below the active cell and keeps the same column. Shift+Enter adds a line within the cell. Tab moves across cells and then into the next row; Tab at the final cell appends a row. Shift+Tab moves backward. Drag a vertical divider to resize the column on its left, or a horizontal divider to resize the row above. Other track sizes stay unchanged and the enclosing shape follows the new table size. Minimum sizes come from one measured character plus padding and the cell's text height. The inspector also exposes exact sizes and add/remove row and column controls. At least one row and one column must remain.

Use the table verbs on the controller for the same structural and sizing edits. Outer borders, inner row borders, and inner column borders have separate color, width, and solid/dashed/dotted/none styles. Row and column separator overrides are supported; cell-level borders and merged cells are not part of this initial grammar.