///|
/// Integer convenience path retained for the first motion-plan API.
pub fn bpmn_path(flow : @bpmn.Flow) -> String {
let b = StringBuilder()
for i, p in flow.points {
b.write_string("\{if i == 0 { "M" } else { " L" }}\{p.x} \{p.y}")
}
b.to_string()
}
///|
fn BpmnPaint::shape(
self : BpmnPaint,
shape : @bpmn_layout.Shape,
starts : ArrayView[@bpmn.Node],
) -> @writer.Node {
let node = shape.node
if !self.charge(node.markers.details.length()) ||
!self.charge(node.markers.events.length()) {
return self.element("g")
}
let attrs = self.metadata(node.id, node.source, node.properties)
attrs.push(("id", "node-\{shape.index}"))
attrs.push(("class", "bpmn-shape"))
attrs.push(("data-kind", bpmn_kind(node.kind)))
attrs.push(
(
"data-expanded",
if node.expanded.unwrap_or(false) {
"true"
} else {
"false"
},
),
)
attrs.push(("data-black-box", if node.black_box { "true" } else { "false" }))
if node.ownership.pool is Some(id) {
attrs.push(("data-pool", id))
}
if node.ownership.subprocess is Some(id) {
attrs.push(("data-subprocess", id))
}
if node.ownership.attached_to is Some(id) {
attrs.push(("data-attached-to", id))
}
let events = StringBuilder()
let mut units = 0
for i, event in node.markers.events {
let name = (if i == 0 { "" } else { "," }) + bpmn_event(event.kind)
if name.length() > self.limit - self.used - units {
self.fail("bpmn/render-limit", "Event metadata exceeds the output budget")
break
}
units += name.length()
events.write_string(name)
}
attrs.push(("data-events", events.to_string()))
let r = shape.bounds
let children = [
self.element("rect", attrs=[
("class", "bpmn-bounds"),
("x", r.x.token),
("y", r.y.token),
("width", r.width.token),
("height", r.height.token),
("fill", "none"),
("stroke", "none"),
("pointer-events", "none"),
("aria-hidden", "true"),
]),
self.outline(shape),
]
children.append(self.markers(shape, starts))
self.element("g", attrs~, children~)
}
///|
fn bpmn_background(kind : @bpmn.NodeKind, expanded : Bool) -> Bool {
kind == Participant ||
kind == Lane ||
kind == Group ||
(
expanded &&
(
kind == SubProcess ||
kind == AdHocSubProcess ||
kind == Transaction ||
kind == CallActivity
)
)
}
///|
fn bpmn_prefix(
root : @writer.Node,
raw : String?,
paint : BpmnPaint,
) -> String? {
let prefix = match raw {
Some(raw) => {
let b = StringBuilder()
let mut length = 7
b.write_string("bpmn-u-")
for c in raw {
let value = c.to_int().to_string(radix=16) + "-"
if value.length() > paint.limit - length {
paint.fail(
"bpmn/render-limit", "SVG ID prefix exceeds the output budget",
)
return None
}
length += value.length()
b.write_string(value)
}
b.to_string()
}
None => {
let source = @writer.to_string(root)
let mut hash : UInt64 = 14695981039346656037
for i in 0.. ()
Element(name, attrs, children) => {
if name == "style" {
occurrences += 4
}
for (key, _) in attrs {
if key == "id" || key == "marker-start" || key == "marker-end" {
occurrences += 1
}
}
stack.append(children)
}
}
}
if prefix.length() > (paint.limit - paint.used) / occurrences ||
!paint.reserve(prefix.length() * occurrences + 64) {
paint.fail(
"bpmn/render-limit", "Scoped SVG identifiers exceed the output budget",
)
return None
}
Some(prefix)
}
///|
fn paint_bpmn(
view : @bpmn.Bpmn,
theme : @theme.Theme,
mode : Mode,
id_prefix : String?,
policy : @bpmn.GeometryPolicy,
limits : @bpmn_layout.Limits,
precomputed? : @bpmn_layout.Result,
connectors~ : ConnectorOptions = ConnectorOptions::new(),
) -> @render.Rendered {
if !connector_options_valid(connectors) {
return @render.Rendered::failed("image/svg+xml", [
@diagrams.Diagnostic::error(
"connector/invalid-option", "Invalid connector options",
),
])
}
let limits = limits.with_minimum_approach(connectors.minimum_approach)
let theme = @theme.Theme::{
dark: @bpmn_layout.bpmn_palette(theme.dark),
light: @bpmn_layout.bpmn_palette(theme.light),
}
let result = precomputed.unwrap_or_else(() => {
@bpmn_layout.layout(view, policy~, limits~)
})
let diagnostics = result.diagnostics.iter().to_array()
guard result.geometry is Some(geometry) else {
return @render.Rendered::failed("image/svg+xml", diagnostics)
}
let c = BpmnPaint::{
paint: Paint::{ theme, mode, },
limit: limits.output_units,
diagnostics,
used: 0,
failed: false,
work: 0,
work_limit: limits.work,
}
let starts : Map[String, Array[@bpmn.Node]] = Map([])
let kinds = Map([])
if !c.charge(view.nodes.length()) {
return @render.Rendered::failed("image/svg+xml", diagnostics)
}
for node in view.nodes {
kinds[node.id] = node.kind
if node.kind == StartEvent && node.ownership.subprocess is Some(owner) {
match starts.get(owner) {
Some(events) => events.push(node)
None => starts[owner] = [node]
}
}
}
let background = []
let foreground = []
let labels = []
let containers : Array[@bpmn_layout.Shape] = []
for shape in geometry.shapes {
if c.failed {
break
}
if bpmn_background(shape.node.kind, shape.node.expanded.unwrap_or(false)) {
containers.push(shape)
} else {
foreground.push(c.shape(shape, starts.get(shape.node.id).unwrap_or([])))
}
if shape.label is Some(label) {
labels.push(c.label(label))
}
}
// Outer containers paint first even when XML/model order places a child first.
containers.sort_by((a, b) => {
let area = (b.box.width * b.box.height).compare(a.box.width * a.box.height)
if area == 0 {
a.index.compare(b.index)
} else {
area
}
})
for shape in containers {
background.push(c.shape(shape, starts.get(shape.node.id).unwrap_or([])))
}
let flows = []
for edge in geometry.connections {
if c.failed {
break
}
flows.push(c.connection(edge, kinds.get(edge.source)))
if edge.label is Some(label) {
labels.push(c.label(label))
}
}
let definitions = c.definitions()
if mode == Adaptive {
definitions.push(c.element("style", children=[c.text(style_sheet(theme))]))
}
let root = c.element(
"svg",
attrs=[
("xmlns", "http://www.w3.org/2000/svg"),
("viewBox", "0 0 \{geometry.width} \{geometry.height}"),
("width", geometry.width.to_string()),
("height", geometry.height.to_string()),
("role", "img"),
("data-diagram", "bpmn"),
],
children=[
c.element("title", children=[c.text(view.label)]),
c.element("desc", children=[
c.text(
"BPMN view with \{geometry.shapes.length()} visible shapes and \{geometry.connections.length()} connections. Structural flow is illustrative. Conditions, timers and execution outcomes are not evaluated.",
),
]),
c.element("defs", children=definitions),
c.element("rect", attrs=[
("class", "bg"),
("x", "0"),
("y", "0"),
("width", geometry.width.to_string()),
("height", geometry.height.to_string()),
("fill", c.paint.color("bg")),
]),
c.element(
"g",
attrs=[
(
"transform",
"translate(\{geometry.translate_x} \{geometry.translate_y})",
),
("class", "bpmn-viewport"),
],
children=[
c.element(
"g",
attrs=[("class", "bpmn-backgrounds")],
children=background,
),
c.element("g", attrs=[("class", "bpmn-connections")], children=flows),
c.element("g", attrs=[("class", "bpmn-nodes")], children=foreground),
c.element("g", attrs=[("class", "bpmn-labels")], children=labels),
],
),
],
)
if c.failed {
return @render.Rendered::failed("image/svg+xml", diagnostics)
}
let obstacles : Array[ConnectorObstacle] = []
let labels : Array[(String, ConnectorObstacle)] = []
for shape in geometry.shapes {
if !bpmn_background(shape.node.kind, shape.node.expanded.unwrap_or(false)) {
let r = shape.box
obstacles.push({
owner: shape.node.id,
x: r.x,
y: r.y,
width: r.width,
height: r.height,
})
}
if shape.label is Some(label) {
let r = label.box
labels.push(
(
"",
{
owner: shape.node.id,
x: r.x,
y: r.y,
width: r.width,
height: r.height,
},
),
)
}
}
for edge in geometry.connections {
if edge.label is Some(label) {
let r = label.box
labels.push(
(
"",
{
owner: edge.flow.id,
x: r.x,
y: r.y,
width: r.width,
height: r.height,
},
),
)
}
}
let display_options = if geometry.source == Supplied &&
!connectors.style_supplied_bpmn {
ConnectorOptions::new(crossing=Plain, limits=connectors.limits)
} else {
connectors
}
guard finalize_connectors(
root,
display_options,
diagnostics,
obstacles=obstacles[:],
labels=labels[:],
viewport=(
-geometry.translate_x.to_double(),
-geometry.translate_y.to_double(),
geometry.width.to_double(),
geometry.height.to_double(),
),
supplied=geometry.source == Supplied,
)
is Some(root) else {
return @render.Rendered::failed("image/svg+xml", diagnostics)
}
guard bpmn_prefix(root, id_prefix, c) is Some(prefix) else {
return @render.Rendered::failed("image/svg+xml", diagnostics)
}
let palettes = match mode {
Adaptive => [("dark", theme.dark), ("light", theme.light)]
Fixed(Dark) => [("dark", theme.dark)]
Fixed(Light) => [("light", theme.light)]
}
for (name, palette) in palettes {
for finding in @bpmn_layout.check(geometry, palette, work_limit=limits.work) {
diagnostics.push(
@diagrams.Diagnostic::warning(
"bpmn/legibility",
"\{name}: \{finding.message}",
),
)
}
}
@render.Rendered::ok(
@writer.to_string(scope_svg(root, prefix, theme)),
"image/svg+xml",
diagnostics~,
)
}