///|
pub struct PathBinding {
occurrence : @animation.Occurrence
id : String
path : String
/// The scoped underpass mask of the canonical connector, if any.
mask : String?
}
///|
pub(all) enum NodeBindingKind {
Bpmn(String)
Message(Int32)
Frame(Int32)
Swimlane(String)
Flowchart(String)
Architecture(String)
/// A railroad term group, by term identity.
RailroadTerm(String)
/// A data member row, by member identity.
DataRow(String)
/// A data container or scalar root box, by node identity.
DataBox(String)
} derive(Eq, Debug)
///|
pub extend NodeBindingKind with Eq::{equal, not_equal}
///|
pub extend NodeBindingKind with @debug.Debug::{to_repr}
///|
pub struct NodeBinding {
endpoint : String
kind : NodeBindingKind
}
///|
pub struct Scene {
canonical_svg : String
display_svg : String
catalog : @animation.Catalog
paths : ReadOnlyArray[PathBinding]
nodes : ReadOnlyArray[NodeBinding]
}
///|
pub struct SceneResult {
scene : Scene?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}
///|
fn scene_prefix(canonical : String) -> String {
let mut hash : UInt64 = 14695981039346656037
for i in 0.. Bool {
if sequence.participants.length() > limits.nodes {
return false
}
let mut used = 0
fn text(value : String) -> Bool {
if value.length() > limits.work - used {
return false
}
used += value.length()
true
}
for
value in sequence.title
.iter()
.concat(sequence.accessibility_title.iter())
.concat(sequence.accessibility_description.iter()) {
if !text(value) {
return false
}
}
for participant in sequence.participants {
if !text(participant.id) || !text(participant.label) {
return false
}
if participant.icon is Some(icon) && (!text(icon.pack) || !text(icon.key)) {
return false
}
if participant.link is Some(link) && !text(link.target) {
return false
}
if participant.group is Some(group) &&
(
!text(group.id) ||
!text(group.label) ||
!text(group.colour.unwrap_or(""))
) {
return false
}
if participant.properties is Some(properties) &&
(
!text(properties.sublabel.unwrap_or("")) ||
!text(properties.description.unwrap_or(""))
) {
return false
}
}
let stack = sequence.events.iter().to_array()
while stack.pop() is Some(event) {
if used >= limits.work {
return false
}
used += 1
match event {
Message(message) => {
if !text(message.sender) ||
!text(message.receiver) ||
!text(message.label) {
return false
}
if message.link is Some(link) && !text(link.target) {
return false
}
if message.properties is Some(properties) &&
(
!text(properties.sublabel.unwrap_or("")) ||
!text(properties.description.unwrap_or(""))
) {
return false
}
}
Note(note) => {
if !text(note.text) {
return false
}
for id in note.participants {
if !text(id) {
return false
}
}
}
Activate(id) | Deactivate(id) | Create(id) | Destroy(id) =>
if !text(id) {
return false
}
Frame(frame) => {
if !text(frame.colour.unwrap_or("")) {
return false
}
for section in frame.sections {
if !text(section.label.unwrap_or("")) ||
section.events.length() > limits.work - used {
return false
}
stack.append(section.events)
}
}
Autonumber(_) => ()
}
}
true
}
///|
fn sequence_path(message : @layout.MessageLine, limit : Int32) -> String? {
let result = StringBuilder()
let mut used = 0
for i, segment in message.path {
let part = if i == 0 {
"M\{segment.x1} \{segment.y1} L\{segment.x2} \{segment.y2}"
} else {
" L\{segment.x2} \{segment.y2}"
}
if part.length() > limit - used {
return None
}
used += part.length()
result.write_string(part)
}
Some(result.to_string())
}
///|
/// Both copies and their motion bindings use one measured geometry. Ordinary
/// render output remains the canonical artifact; only the display copy gains
/// invisible, scoped numeric paths. No scripts or active references are added.
pub fn SvgRenderer::scene(
self : SvgRenderer,
diagram : @render.Diagram,
sequence_view_id~ : String = "sequence",
limits~ : @animation.Limits = @animation.Limits::new(),
output_units~ : Int32 = 33554432,
) -> SceneResult {
fn failed(code : String, message : String) -> SceneResult {
{ scene: None, diagnostics: [@diagrams.Diagnostic::error(code, message)], }
}
if output_units <= 0 {
return failed(
"animation/scene-limit", "Scene output limit must be positive",
)
}
if !connector_options_valid(self.connectors) {
return failed(
"connector/invalid-option", "Connector distances, radii and budgets must be valid",
)
}
let catalog_result = match diagram {
Architecture(view) => @animation.Catalog::architecture(view, limits~)
Swimlane(view) => @animation.Catalog::swimlane(view, limits~)
Flowchart(view) => @animation.Catalog::flowchart(view, limits~)
Bpmn(view) => @animation.Catalog::bpmn(view, limits~)
Sequence(sequence) =>
@animation.Catalog::sequence(sequence, view_id=sequence_view_id, limits~)
Regex(model) => @animation.Catalog::regex(model, limits~)
Ebnf(grammar) => @animation.Catalog::ebnf(grammar, limits~)
DataTree(tree) => @animation.Catalog::data_tree(tree, limits~)
}
guard catalog_result.catalog is Some(catalog) else {
return { scene: None, diagnostics: catalog_result.diagnostics, }
}
let paths : Array[(@animation.Occurrence, String)] = []
let nodes : Array[NodeBinding] = []
let (rendered, bpmn) = match diagram {
Architecture(view) => {
let result = @architecture_layout.layout(
view,
limits=self.architecture_limits.with_minimum_approach(
self.connectors.minimum_approach,
),
)
guard result.geometry is Some(geometry) else {
return { scene: None, diagnostics: result.diagnostics, }
}
let rendered = paint_architecture(
view,
self.theme,
self.mode,
self.id_prefix,
self.architecture_limits,
connectors=self.connectors,
geometry~,
)
if rendered.output == None {
return { scene: None, diagnostics: rendered.diagnostics, }
}
let edges = Map([])
for edge in geometry.connections {
edges[edge.connection.id] = edge
}
for occurrence in catalog.occurrences {
guard occurrence.kind is Architecture(connection) &&
edges.get(connection.id) is Some(edge) else {
return failed(
"animation/binding-mismatch", "An architecture connection has no rendered path",
)
}
let path = architecture_path(edge.points)
if path.length() > output_units {
return failed(
"animation/scene-limit", "Architecture scene path exceeds output limit",
)
}
paths.push((occurrence, path))
}
for node in catalog.graph.nodes {
nodes.push({ endpoint: node.id, kind: Architecture(node.id), })
}
(rendered, false)
}
Flowchart(view) => {
let result = @flowchart_layout.layout(
view,
limits=self.flowchart_limits.with_minimum_approach(
self.connectors.minimum_approach,
),
)
guard result.geometry is Some(geometry) else {
return { scene: None, diagnostics: result.diagnostics, }
}
let rendered = paint_flowchart(
view,
self.theme,
self.mode,
self.id_prefix,
self.flowchart_limits,
connectors=self.connectors,
geometry~,
)
if rendered.output == None {
return { scene: None, diagnostics: rendered.diagnostics, }
}
let edges = Map([])
for edge in geometry.connections {
edges[edge.connection.id] = edge
}
for occurrence in catalog.occurrences {
if occurrence.is_visit() {
continue
}
guard occurrence.kind is Flowchart(connection) &&
edges.get(connection.id) is Some(edge) else {
return failed(
"animation/binding-mismatch", "A flowchart connection has no visible rendered path",
)
}
let path = flowchart_path(edge)
if path.length() > output_units {
return failed(
"animation/scene-limit", "Flowchart scene path exceeds output limit",
)
}
paths.push((occurrence, path))
}
let visible = Map(geometry.shapes.map(shape => (shape.id, true)))
for node in catalog.graph.nodes {
let anchor = match node.kind {
FlowchartRegion(_, anchor) =>
if visible.contains(anchor) {
Some(anchor)
} else {
None
}
_ => Some(node.id)
}
if anchor is Some(id) {
nodes.push({ endpoint: node.id, kind: Flowchart(id), })
if node.occurrence is Some(key) {
nodes.push({ endpoint: key, kind: Flowchart(id), })
}
}
}
(rendered, false)
}
Swimlane(view) => {
let geometry = @swimlane_layout.layout(
view,
limits=self.swimlane_limits.with_minimum_approach(
self.connectors.minimum_approach,
),
)
let rendered = paint_swimlane(
view,
self.theme,
self.mode,
self.id_prefix,
self.swimlane_limits,
connectors=self.connectors,
precomputed=geometry,
)
guard geometry.geometry is Some(geometry) && rendered.output is Some(_) else {
return { scene: None, diagnostics: rendered.diagnostics, }
}
let edges = Map([])
for edge in geometry.connections {
edges[edge.connection.id] = edge
}
for occurrence in catalog.occurrences {
guard occurrence.swimlane_connection is Some(connection) &&
edges.get(connection.id) is Some(edge) else {
return failed(
"animation/binding-mismatch", "A swimlane connection has no visible rendered path",
)
}
let path = swimlane_path(edge)
if path.length() > output_units {
return failed(
"animation/scene-limit", "Swimlane scene path exceeds its output limit",
)
}
paths.push((occurrence, path))
}
for node in catalog.graph.nodes {
nodes.push({ endpoint: node.id, kind: Swimlane(node.id), })
}
(rendered, false)
}
Bpmn(view) => {
let geometry = @bpmn_layout.layout(
view,
policy=self.bpmn_policy,
limits=self.bpmn_limits.with_minimum_approach(
self.connectors.minimum_approach,
),
)
let rendered = paint_bpmn(
view,
self.theme,
self.mode,
self.id_prefix,
self.bpmn_policy,
self.bpmn_limits,
connectors=self.connectors,
precomputed=geometry,
)
guard geometry.geometry is Some(geometry) && rendered.output is Some(_) else {
return { scene: None, diagnostics: rendered.diagnostics, }
}
let edges = Map([])
for edge in geometry.connections {
edges[edge.flow.id] = edge
}
for occurrence in catalog.occurrences {
guard occurrence.flow is Some(flow) && edges.get(flow.id) is Some(edge) else {
return failed(
"animation/binding-mismatch", "A catalog connection has no visible rendered path",
)
}
guard bpmn_geometry_path(edge, max_units=output_units) is Some(path) else {
return failed(
"animation/scene-limit", "Scene path exceeds its output limit",
)
}
paths.push((occurrence, path))
}
for node in catalog.graph.nodes {
if node.source_node is Some(source) {
nodes.push({ endpoint: node.id, kind: Bpmn(source.id), })
}
}
(rendered, true)
}
Sequence(sequence) => {
if !sequence_scene_input(sequence, limits) {
return failed(
"animation/scene-limit", "Sequence scene input exceeds its node/text work bound",
)
}
let geometry = @layout.layout(
sequence,
minimum_approach=self.connectors.minimum_approach,
)
let (svg, diagnostics) = paint(
sequence,
self.theme,
self.mode,
self.id_prefix,
connectors=self.connectors,
geometry~,
)
if diagnostics.iter().any(d => d.severity == Error) {
return {
scene: None,
diagnostics: ReadOnlyArray::from_array(diagnostics),
}
}
let messages = Map([])
for message in geometry.messages {
messages[message.index] = message
}
for occurrence in catalog.occurrences {
guard messages.get(occurrence.index) is Some(message) &&
sequence_path(message, output_units) is Some(path) else {
return failed(
"animation/binding-mismatch", "A message occurrence has no rendered path",
)
}
paths.push((occurrence, path))
nodes.push({
endpoint: occurrence.key,
kind: Message(occurrence.index),
})
}
for i, frame in catalog.frames {
nodes.push({ endpoint: frame.key + ":in", kind: Frame(i), })
nodes.push({ endpoint: frame.key + ":out", kind: Frame(i), })
}
(@render.Rendered::ok(svg, "image/svg+xml", diagnostics~), false)
}
Regex(_) | Ebnf(_) | DataTree(_) => {
let parts = match diagram {
DataTree(tree) => data_tree_scene(self, tree, catalog, output_units)
_ => railroad_scene(self, diagram, catalog, output_units)
}
match parts {
Err(diagnostics) => return { scene: None, diagnostics, }
Ok((rendered, scene_paths, scene_nodes)) => {
guard rendered.output is Some(_) else {
return { scene: None, diagnostics: rendered.diagnostics, }
}
paths.append(scene_paths)
nodes.append(scene_nodes)
(rendered, false)
}
}
}
}
let canonical = rendered.output.unwrap()
if canonical.length() > output_units {
return failed(
"animation/scene-limit", "Canonical SVG exceeds the scene output limit",
)
}
// Route strings identify the measured geometry. The finalizer's paths and
// masks are the sole source of display geometry for motion. Repeated equal
// routes are consumed in paint order, retaining parallel-edge occurrences.
let final_paths : Map[String, Array[(String, String?)]] = Map([])
for (route, path, mask) in final_connector_bindings(canonical) {
if final_paths.get(route) is Some(entries) {
entries.push((path, mask))
} else {
final_paths[route] = [(path, mask)]
}
}
let used_paths : Map[String, Int32] = Map([])
let prefix = scene_prefix(canonical)
let bindings : Array[PathBinding] = []
let elements = []
let mut units = canonical.length()
for i, (occurrence, route) in paths {
let used = used_paths.get(route).unwrap_or(0)
let (path, mask) = match
final_paths.get(route).bind(entries => entries.get(used)) {
Some(binding) => {
used_paths[route] = used + 1
binding
}
None =>
return failed(
"animation/binding-mismatch", "A measured route has no final canonical connector path",
)
}
let id = prefix + i.to_string()
let attrs = [
("id", id),
("d", path),
("fill", "none"),
("stroke", "none"),
("pointer-events", "none"),
("aria-hidden", "true"),
]
if mask is Some(value) {
attrs.push(("mask", value))
}
let element = @writer.element("path", attrs~)
let size = path.length() +
id.length() +
mask.map(m => m.length()).unwrap_or(0) +
256
if size > output_units - units {
return failed(
"animation/scene-limit", "Scene bindings exceed their output limit",
)
}
units += size
elements.push(element)
bindings.push({ occurrence, id, path, mask, })
}
let extra = @writer.to_string(
@writer.element(
"g",
attrs=[("data-motion-paths", "true"), ("aria-hidden", "true")],
children=elements,
),
)
if canonical.length() > output_units - canonical.length() - extra.length() {
return failed(
"animation/scene-limit", "Canonical and display copies exceed the scene output limit",
)
}
let closing = if bpmn { "" } else { "" }
guard canonical.rev_find(closing) is Some(cut) else {
return failed(
"animation/binding-mismatch", "Rendered SVG is missing its scene container",
)
}
let display = canonical[:cut].to_owned() + extra + canonical[cut:].to_owned()
{
scene: Some({
canonical_svg: canonical,
display_svg: display,
catalog,
paths: ReadOnlyArray::from_array(bindings),
nodes: ReadOnlyArray::from_array(nodes),
}),
diagnostics: rendered.diagnostics,
}
}