Controllers, agents and collaboration
Sharing a drawing
A Diagram holds the committed document, its installed extensions and rules, and one history. Each actor that edits it, such as a person at a canvas, an agent or a script, gets its own DiagramController with its own selection, active tool, camera and in-progress edit.
dart
final human = DiagramController(document: document);
final agent = DiagramController(
diagram: human.diagram,
participant: DiagramParticipant.agent(id: 'agent', name: 'Agent'),
);DiagramController(document:, configuration: DiagramConfiguration(extensions:)) creates and owns a diagram; its participant defaults to DiagramParticipant.local (id local, name You). DiagramController(diagram:, participant:) joins an existing one; each participant's id must be unique among the controllers attached to that diagram.
Participants
A DiagramParticipant names one actor:
| Member | Meaning |
|---|---|
DiagramParticipant({required id, required name, kind = human, color}) | A person. id is the stable attribution for edits, history and locks; name is for display. |
DiagramParticipant.agent({required id, required name, color}) | An automated agent (kind is DiagramParticipantKind.agent). |
DiagramParticipant.local | The default participant, id local, name You. |
color | An optional presence color, or null for the host's choice. |
controller.participant is the actor a controller edits as. The diagram resolves ids for presence and attribution:
| Diagram member | Meaning |
|---|---|
diagram.participants | The participants of the attached controllers, in attachment order. |
diagram.participantById(id) | The participant with id, attached now or earlier, or null, so an entry recorded before an actor detached still resolves. |
dart
// Presence: who is on this drawing.
final names = [for (final p in human.diagram.participants) p.name];History entries and document changes stay id-based: they record participantId, which participantById resolves to a name, kind and color. Rules see the whole participant on DiagramChangeProposal.participant.
| Scenario | Behavior |
|---|---|
| User and in-process agent on one diagram | Give each its own controller. Both use the same verbs, admission and history, with independent selections and previews. |
| Undo and redo | One shared history; undo() and redo() revert only that controller's own edits. |
| Simultaneous edits | Allowed on different elements. An edit to an element another controller is previewing is denied. |
| Remote collaborators | No built-in transport or remote conflict reconciliation. Controllers share one diagram in one process; diagram.participants lists the in-process actors. |
| Server-side agent | Use DiagramController from vyuh_diagram_core with an appropriate text backend. Verbs match the Flutter controller. |
A participant records attribution and scopes undo. It is not authentication or authorization.
Element leases
While a controller previews an interaction, such as a drag, a text edit or a beginInteraction lease, the elements that preview changes are leased to it. Another controller's edit touching those elements returns a denial with DiagramDenialCode.conflict; edits to other elements proceed.
dart
final result = agent.setStyle(ids: ['task-1'], fill: fill);
if (result.code == DiagramDenialCode.conflict) {
// The user is editing task-1. Retry after their gesture ends.
}When a controller commits, every other controller's in-progress preview is rebased onto the new committed document, so its own commit keeps the other actor's edits. A preview that the commit changed indirectly is cancelled rather than merged.
Stale revisions
An actor that decides an edit from content it read earlier, such as an agent awaiting a model response, passes the revision it read as expectedRevision. Every verb accepts it; if any controller changed the document meanwhile, the verb is denied with DiagramDenialCode.conflict instead of overwriting that edit. Admission owns this check along with lock checks, so it applies the same way to typed verbs, perform and load.
dart
final revision = agent.document.revision;
final plan = await askModel(agent.document);
final result = agent.perform(['task-1'], 'setText', {
'text': plan.title,
'expectedRevision': revision,
});
if (result.code == DiagramDenialCode.conflict) {
// Read the document again and re-plan.
}See verbs.
Observing every actor
controller.document and controller.scene are that actor's live view, including its own preview. diagram.document, diagram.scene and diagram.snapshot are the committed content every actor shares.
diagram.changes lives on the diagram and reports every controller's completed edits. change.participantId is the id of the actor that made it; resolve it with diagram.participantById:
dart
final changes = human.diagram.changes;
changes.addListener(() {
final actor = human.diagram.participantById(changes.value!.participantId);
if (actor?.kind == DiagramParticipantKind.agent) {
// Highlight or log the agent's edit, attributed to actor.name.
}
});Mounting a Flutter canvas
DiagramCanvas(controller: controller) mounts one controller; a controller supports one mounted canvas at a time. Any controller can be mounted, including one created headlessly; mounting installs the Flutter text layout backend when the diagram has no text layout of its own. Mount a second canvas for another actor by giving it its own controller on the same diagram.
Supported font choices and the default font are part of the diagram's shared configuration (DiagramConfiguration(fonts: ..., defaultFontFamily: ...)), read with controller.configuration and changed with controller.setFonts and controller.setDefaultFontFamily. Every controller and canvas sees the same font choices, and changing them does not edit document content or add undo entries.
Dispose the canvas before its controller. Disposing a controller that joined a diagram releases its selection, previews and listeners and leaves the document and history available. Disposing the controller that created the diagram disposes the diagram and every controller on it.