///|
fn external_label(kind : @model.NodeKind) -> Bool {
event(kind) || gateway(kind) || [@model.NodeKind::DataObject,DataObjectReference,DataInput,DataOutput,DataStore,DataStoreReference,Message].contains(kind)
}
///|
fn auto_parent(i : Int, view : @model.Bpmn, input : Input, budget : Budget) -> Int {
let node=view.nodes[i]
let mut parent=node.ownership.lanes.last().bind(id => input.nodes.get(id)).unwrap_or(-1)
if input.containers[i] is Some(sub) {
let mut lane=if parent >= 0 { Some(parent) } else { None }; let mut inside=false
while lane is Some(index) {
if !budget.charge(1) { return -1 }
if input.containers[index] == Some(sub) { inside=true; break }
lane=view.nodes[index].ownership.lanes.last().bind(id => input.nodes.get(id))
}
if !inside { parent=sub }
}
if parent < 0 { parent=node.ownership.pool.bind(id => input.nodes.get(id)).unwrap_or(-1) }
parent
}
///|
fn scope_links(a : Int, b : Int, parents : ArrayView[Int], budget : Budget) -> (Int,Int,Int)? {
let ancestors=Map([]); let mut i=a; let mut child=a
while i >= 0 {
if !budget.charge(1) { return None }
ancestors[i]=child; child=i; i=parents[i]
}
ancestors[-1]=child; i=b; child=b
while !ancestors.contains(i) {
if !budget.charge(1) { return None }
child=i; i=parents[i]
}
let source=ancestors[i]
if source == child || source == i || child == i { None } else { Some((i,source,child)) }
}
///|
fn automatic(view : @model.Bpmn, input : Input, budget : Budget) -> (Array[Shape],Array[Connection]) {
let count=view.nodes.length()
let group_roots=Array::makei(count,i => i)
fn group_root(i : Int) -> Int {
let mut root=i
while group_roots[root] != root { if !budget.charge(1) { return root }; root=group_roots[root] }
root
}
for node in view.nodes {
let mut first : Int?=None
for id in node.ownership.groups {
if !budget.charge(1) { return ([],[]) }
if input.nodes.get(id) is Some(group) {
if first is Some(previous) { let a=group_root(previous); let b=group_root(group); group_roots[Int::max(a,b)]=Int::min(a,b) }
else { first=Some(group) }
}
}
}
let sizes : Array[(Double,Double)]=[]; let captions : Array[(Double,Double)]=[]
let slots : Array[(Double,Double)]=[]; let parents=Array::make(count,-1)
let groups : Map[Int,Array[Int]]=Map([]); let boundaries : Map[Int,Array[Int]]=Map([])
let boxes : Array[Rect?]=Array::make(count,None)
let mut gap_x=96.0; let mut gap_y=80.0
for i,node in view.nodes {
let text=node.label+node.properties.bind(p => p.sublabel).map(s => "\n"+s).unwrap_or("")
let (width,height)=measure(text,default_font(),budget).unwrap_or((0.0,0.0))
captions.push((width,if text.is_empty() { 0.0 } else { height }))
let size=if event(node.kind) { (40.0,40.0) } else if gateway(node.kind) { (56.0,56.0) }
else if [@model.NodeKind::DataObject,DataObjectReference,DataInput,DataOutput].contains(node.kind) { (48.0,64.0) }
else if node.kind == DataStore || node.kind == DataStoreReference { (64.0,64.0) }
else if node.kind == Message { (56.0,36.0) }
else { (Double::max(140.0,width+48.0),Double::max(100.0,height+64.0)) }
sizes.push(size); slots.push(size)
if !input.visible[i] { continue }
parents[i]=auto_parent(i,view,input,budget)
for id in node.ownership.groups {
if !budget.charge(1) { return ([],[]) }
guard input.nodes.get(id) is Some(group) && view.nodes[group].kind == Group else { budget.error("bpmn/invalid-reference","Invalid group membership on \{node.id}"); return ([],[]) }
match groups.get(group) { Some(members) => members.push(i); None => groups[group]=[i] }
}
if node.ownership.attached_to.bind(id => input.nodes.get(id)) is Some(owner) {
match boundaries.get(owner) { Some(events) => events.push(i); None => boundaries[owner]=[i] }
}
}
for flow in view.flows {
let text=flow.label+flow.properties.bind(p => p.sublabel).map(s => "\n"+s).unwrap_or("")
let (width,height)=measure(text,default_font(),budget).unwrap_or((0.0,0.0))
gap_x=Double::max(gap_x,width+64.0); gap_y=Double::max(gap_y,height+48.0)
}
// Boundary captions reserve space on the owning activity's row.
for (owner,events) in boundaries {
let mut width=80.0
for event in events { width=Double::max(width,captions[event].0+32.0) }
sizes[owner]=(Double::max(sizes[owner].0,width*events.length().to_double()+32.0),sizes[owner].1)
for event in events { parents[event]=parents[owner] }
}
let selected : Array[(Int,Int,Int)]=[]
for i,flow in view.flows {
guard selected_flow(flow,view,input,budget) is Some((a,b)) else { continue }
if selected.length() >= budget.limits.edges { budget.error("bpmn/layout-limit","Visible connection limit exceeded"); return ([],[]) }
selected.push((i,a,b))
}
let neighbors : Map[Int,Array[Int]]=Map([])
for (_,a,b) in selected {
for (item,other) in [(a,b),(b,a)] {
if artifact(view.nodes[item].kind) && view.nodes[item].kind != Group && !artifact(view.nodes[other].kind) {
// Artifacts can follow connected flow nodes into lanes, but cannot cross
// a subprocess ownership boundary merely because an association does.
let base=parents[item]; let mut candidate=parents[other]; let mut current=candidate
while current >= 0 && current != base {
if !budget.charge(1) { return ([],[]) }
if view.nodes[current].kind != Lane && view.nodes[current].kind != Participant { candidate=parents[current] }
current=parents[current]
}
match neighbors.get(item) { Some(values) => values.push(candidate); None => neighbors[item]=[candidate] }
}
}
}
for (item,owners) in neighbors {
let mut owner=owners[0]
for other in owners {
let ancestors=Map([(-1,true)]); let mut current=owner
while current >= 0 { if !budget.charge(1) { return ([],[]) }; ancestors[current]=true; current=parents[current] }
current=other
while !ancestors.contains(current) { if !budget.charge(1) { return ([],[]) }; current=parents[current] }
owner=current
}
let base=parents[item]; let mut current=owner
while current >= 0 && current != base { if !budget.charge(1) { return ([],[]) }; current=parents[current] }
if base < 0 || current == base { parents[item]=owner }
}
let children : Map[Int,Array[Int]]=Map([])
for i,node in view.nodes {
if !input.visible[i] || node.ownership.attached_to != None || (node.kind == Group && groups.get(i).map(g => !g.is_empty()).unwrap_or(false)) { continue }
match children.get(parents[i]) { Some(values) => values.push(i); None => children[parents[i]]=[i] }
}
let links : Map[Int,Array[(Int,Int)]]=Map([])
for (_,a,b) in selected {
let a=view.nodes[a].ownership.attached_to.bind(id => input.nodes.get(id)).unwrap_or(a)
let b=view.nodes[b].ownership.attached_to.bind(id => input.nodes.get(id)).unwrap_or(b)
if scope_links(a,b,parents,budget) is Some((scope,a,b)) {
match links.get(scope) { Some(edges) => edges.push((a,b)); None => links[scope]=[(a,b)] }
}
}
let stack=[(-1,false)]
while stack.pop() is Some((scope,leave)) {
if !budget.charge(1) { return ([],[]) }
let members=children.get(scope).unwrap_or([])
if !leave { stack.push((scope,true)); for i in members { if children.contains(i) { stack.push((i,false)) } }; continue }
let bands=members.filter(i => view.nodes[i].kind == Lane || view.nodes[i].kind == Participant)
let ordinary=members.filter(i => view.nodes[i].kind != Lane && view.nodes[i].kind != Participant)
let mut top=64.0; let mut right=128.0
for i in bands {
let (width,height)=sizes[i]; boxes[i]=Some({x:64.0,y:top,width,height}); slots[i]=sizes[i]
top+=height+gap_y; right=Double::max(right,width+128.0)
}
let eligible=Map(ordinary.map(i => (i,true)))
let edges=links.get(scope).unwrap_or([]).filter(p => eligible.contains(p.0) && eligible.contains(p.1))
let rank=ranks(ordinary,edges,budget)
let widths : Map[Int,Double]=Map([]); let rows : Map[Int,Double]=Map([])
let mut last_rank=0
for i in ordinary {
let (width,height)=sizes[i]; let caption=captions[i]
let mut slot_width=if external_label(view.nodes[i].kind) { Double::max(width,caption.0) } else { width }
let mut slot_height=height+(if external_label(view.nodes[i].kind) { caption.1+8.0 } else { 0.0 })
for event in boundaries.get(i).unwrap_or([]) { slot_height=Double::max(slot_height,height+20.0+captions[event].1+8.0) }
for id in view.nodes[i].ownership.groups { if input.nodes.get(id) is Some(group) { slot_width=Double::max(slot_width,captions[group].0+64.0) } }
slots[i]=(slot_width,slot_height)
let r=rank.get(i).unwrap_or(0); widths[r]=Double::max(widths.get(r).unwrap_or(0.0),slot_width); last_rank=Int::max(last_rank,r)
}
let lefts=Map([]); let mut left=64.0
for r in 0..=last_rank { lefts[r]=left; left+=widths.get(r).unwrap_or(0.0)+gap_x }
let grouped : Map[Int,Array[Int]]=Map([]); let bands : Array[Int]=[]
for i in ordinary {
let band=view.nodes[i].ownership.groups.get(0).bind(id => input.nodes.get(id)).map(group_root).unwrap_or(count)
match grouped.get(band) { Some(values) => values.push(i); None => { grouped[band]=[i]; bands.push(band) } }
}
bands.sort_by((a,b) => a.compare(b))
let mut bottom=top
for band in bands {
// A frame and its caption precede the first member, so their measured
// height belongs to the band rather than the preceding scope's padding.
let mut caption_height=0.0
for i in grouped[band] {
for id in view.nodes[i].ownership.groups {
if !budget.charge(1) { return ([],[]) }
if input.nodes.get(id) is Some(group) { caption_height=Double::max(caption_height,captions[group].1) }
}
}
if caption_height > 0.0 { top+=caption_height+28.0 }
rows.clear()
let mut next_top=top
for i in grouped[band] {
let r=rank.get(i).unwrap_or(0); let y=rows.get(r).unwrap_or(top)
let (width,height)=sizes[i]; let slot=slots[i]
boxes[i]=Some({x:lefts[r]+(widths[r]-width)/2.0,y,width,height})
rows[r]=y+slot.1+gap_y; next_top=Double::max(next_top,rows[r]); bottom=Double::max(bottom,y+slot.1+64.0)
}
top=next_top
}
right=Double::max(right,left-gap_x+64.0)
if scope >= 0 { sizes[scope]=(Double::max(sizes[scope].0,right),Double::max(Double::max(sizes[scope].1,bottom),captions[scope].0+48.0)) }
}
let queue=children.get(-1).unwrap_or([]).copy(); let mut cursor=0
while cursor < queue.length() {
if !budget.charge(1) { return ([],[]) }
let i=queue[cursor]; cursor+=1
guard boxes[i] is Some(box) else { continue }
for child in children.get(i).unwrap_or([]) { if boxes[child] is Some(local_box) { boxes[child]=Some({..local_box,x:local_box.x+box.x,y:local_box.y+box.y}) }; queue.push(child) }
}
for (owner,events) in boundaries {
guard boxes[owner] is Some(box) else { continue }
for j,event in events { let fraction=(j+1).to_double()/(events.length()+1).to_double(); boxes[event]=Some({x:box.x+box.width*fraction-20.0,y:box.y+box.height-20.0,width:40.0,height:40.0}) }
}
for i,node in view.nodes {
if node.kind != Group || !input.visible[i] { continue }
let members=groups.get(i).unwrap_or([]); let mut union : Rect?=None
for item in members {
if !budget.charge(1) { return ([],[]) }
if boxes[item] is Some(box) {
let height=box.height+(if external_label(view.nodes[item].kind) { captions[item].1+8.0 } else { 0.0 })
let extra_width=Double::max(box.width,captions[item].0)
let box={..box,x:box.x+(box.width-extra_width)/2.0,width:extra_width,height}
union=Some(match union { None => box; Some(old) => {x:Double::min(old.x,box.x),y:Double::min(old.y,box.y),width:Double::max(old.x+old.width,box.x+box.width)-Double::min(old.x,box.x),height:Double::max(old.y+old.height,box.y+box.height)-Double::min(old.y,box.y)} })
}
}
if union is Some(box) { let width=Double::max(box.width+40.0,captions[i].0+24.0); boxes[i]=Some({x:box.x+(box.width-width)/2.0,y:box.y-20.0,width,height:box.height+40.0}) }
}
let shapes=[]
for i,node in view.nodes {
if !input.visible[i] { continue }
guard boxes[i] is Some(box) else { budget.error("bpmn/invalid-reference","Visible ownership scope is not rooted: \{node.id}"); return ([],[]) }
if box.x.abs()+box.width > 1000000000.0 || box.y.abs()+box.height > 1000000000.0 { budget.error("bpmn/layout-limit","Automatic geometry extent exceeded its safe range"); return ([],[]) }
shapes.push({node,index:i,bounds:exact_bounds(box),box,label:node_label(node,box,false,budget)})
}
(shapes,route_connections(view,selected,shapes,parents,budget))
}