///|
/// 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) -> @render.Rendered {
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) }
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~)
}