///|
pub struct PathBinding {
occurrence : @animation.Occurrence
id : String
path : String
}
///|
pub(all) enum NodeBindingKind {
Bpmn(String)
Message(Int32)
Frame(Int32)
Swimlane(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.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",
)
}
let catalog_result = match diagram {
Swimlane(view) => @animation.Catalog::swimlane(view, limits~)
Bpmn(view) => @animation.Catalog::bpmn(view, limits~)
Sequence(sequence) =>
@animation.Catalog::sequence(sequence, view_id=sequence_view_id, 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 {
Swimlane(view) => {
let geometry = @swimlane_layout.layout(view, limits=self.swimlane_limits)
let rendered = paint_swimlane(
view,
self.theme,
self.mode,
self.id_prefix,
self.swimlane_limits,
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,
)
let rendered = paint_bpmn(
view,
self.theme,
self.mode,
self.id_prefix,
self.bpmn_policy,
self.bpmn_limits,
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)
let (svg, diagnostics) = paint(
sequence,
self.theme,
self.mode,
self.id_prefix,
geometry~,
)
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)
}
}
let canonical = rendered.output.unwrap()
let prefix = scene_prefix(canonical)
let bindings : Array[PathBinding] = []
let elements = []
let mut units = canonical.length()
for i, (occurrence, path) in paths {
let id = prefix + i.to_string()
let element = @writer.element("path", attrs=[
("id", id),
("d", path),
("fill", "none"),
("stroke", "none"),
("pointer-events", "none"),
("aria-hidden", "true"),
])
let size = path.length() + id.length() + 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, })
}
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,
}
}