///|
fn supplied_bounds(node : @model.Node) -> @model.ExactBounds? {
match node.geometry {
Some(g) => g.bounds
None => {
let r = node.bounds
if r.x == 0 && r.y == 0 && r.width == 0 && r.height == 0 {
None
} else {
Some(
exact_bounds({
x: r.x.to_double(),
y: r.y.to_double(),
width: r.width.to_double(),
height: r.height.to_double(),
}),
)
}
}
}
}
///|
fn supplied_points(flow : @model.Flow) -> Array[@model.ExactPoint] {
match flow.geometry {
Some(g) => g.points.iter().to_array()
None =>
flow.points
.iter()
.map(p => exact_point({ x: p.x.to_double(), y: p.y.to_double(), }))
.to_array()
}
}
///|
fn selected_flow(
flow : @model.Flow,
view : @model.Bpmn,
input : Input,
budget : Budget,
) -> (Int32, Int32)? {
if !flow.has_source {
return None
}
let source = input.nodes[flow.source]
let target = input.nodes[flow.target]
if input.visible[source] && input.visible[target] {
return Some((source, target))
}
if flow.kind != MessageFlow {
return None
}
match
(
visible_endpoint(flow.source, view, input, budget),
visible_endpoint(flow.target, view, input, budget),
) {
(Some(s), Some(t)) if s != t => Some((s, t))
_ => {
budget.error(
"bpmn/invalid-reference",
"Message endpoints are hidden within one collapsed scope: \{flow.id}",
)
None
}
}
}
///|
fn charge_bounds(bounds : @model.ExactBounds, budget : Budget) -> Bool {
for value in [bounds.x, bounds.y, bounds.width, bounds.height] {
if !budget.charge(value.token.length()) {
return false
}
}
true
}
///|
fn supplied(
view : @model.Bpmn,
input : Input,
budget : Budget,
) -> (Bool, Array[Shape], Array[Connection]) {
let shapes = []
let connections = []
let mut complete = true
for i, node in view.nodes {
if !input.visible[i] {
continue
}
if node.geometry.bind(g => g.bounds) is Some(r) && !charge_bounds(r, budget) {
break
}
if node.geometry.bind(g => g.label).bind(l => l.bounds) is Some(r) &&
!charge_bounds(r, budget) {
break
}
// Check optional supplied labels even if the required shape bound is absent.
if node.geometry.bind(g => g.label).bind(l => l.bounds) is Some(r) &&
rectangle(r) == None {
budget.error(
"bpmn/invalid-geometry",
"Invalid label bounds on \{node.id}",
)
}
if node.geometry.bind(g => g.label).bind(l => l.font) is Some(font) {
ignore(font_size(font, budget))
}
guard supplied_bounds(node) is Some(bounds) else {
complete = false
continue
}
guard rectangle(bounds) is Some(box) else {
budget.error(
"bpmn/invalid-geometry",
"Invalid shape bounds on \{node.id}",
)
continue
}
if event(node.kind) && (box.width - box.height).abs() > 0.001 {
budget.error(
"bpmn/invalid-geometry",
"Event shape must be circular: \{node.id}",
)
}
shapes.push({
node,
index: i,
bounds,
box,
label: node_label(node, box, true, budget),
})
}
for i, flow in view.flows {
guard selected_flow(flow, view, input, budget) is Some((source, target)) else {
continue
}
if connections.length() >= budget.limits.edges {
budget.error("bpmn/layout-limit", "Visible connection limit exceeded")
break
}
let points = supplied_points(flow)
if !budget.charge(points.length()) {
break
}
let line = []
for p in points {
if !budget.charge(p.x.token.length()) ||
!budget.charge(p.y.token.length()) {
break
}
match point(p) {
Some(p) => line.push(p)
None =>
budget.error(
"bpmn/invalid-geometry",
"Invalid waypoint on \{flow.id}",
)
}
}
if flow.geometry.bind(g => g.label).bind(l => l.bounds) is Some(r) &&
!charge_bounds(r, budget) {
break
}
if flow.geometry.bind(g => g.label).bind(l => l.bounds) is Some(r) &&
rectangle(r) == None {
budget.error(
"bpmn/invalid-geometry",
"Invalid label bounds on \{flow.id}",
)
}
if flow.geometry.bind(g => g.label).bind(l => l.font) is Some(font) {
ignore(font_size(font, budget))
}
if points.length() < 2 {
complete = false
continue
}
if line.length() != points.length() {
continue
}
connections.push({
flow,
index: i,
source: view.nodes[source].id,
target: view.nodes[target].id,
points: ReadOnlyArray::from_array(points),
line: ReadOnlyArray::from_array(line),
label: flow_label(flow, line, true, budget),
})
}
(complete, shapes, connections)
}
///|
fn finish(
view : @model.Bpmn,
shapes : Array[Shape],
connections : Array[Connection],
source : Source,
budget : Budget,
) -> Result {
let mut min_x = 0.0
let mut min_y = 0.0
let mut max_x = 1.0
let mut max_y = 1.0
let mut first = true
fn enclose(r : Rect) -> Unit {
if first {
min_x = r.x
min_y = r.y
max_x = r.x + r.width
max_y = r.y + r.height
first = false
} else {
min_x = Double::min(min_x, r.x)
min_y = Double::min(min_y, r.y)
max_x = Double::max(max_x, r.x + r.width)
max_y = Double::max(max_y, r.y + r.height)
}
}
for shape in shapes {
enclose(shape.box)
if shape.label is Some(l) {
enclose(l.box)
}
}
for edge in connections {
for p in edge.line {
enclose({ x: p.x, y: p.y, width: 0.0, height: 0.0, })
}
if edge.label is Some(l) {
enclose(l.box)
}
}
let translate_x = 32L - min_x.floor().to_int64()
let translate_y = 32L - min_y.floor().to_int64()
let width = (max_x + translate_x.to_double() + 32.0).ceil().to_int64()
let height = (max_y + translate_y.to_double() + 32.0).ceil().to_int64()
{
geometry: if budget.failed {
None
} else {
Some({
view,
shapes: ReadOnlyArray::from_array(shapes),
connections: ReadOnlyArray::from_array(connections),
source,
translate_x,
translate_y,
width,
height,
})
},
diagnostics: ReadOnlyArray::from_array(budget.diagnostics),
}
}
///|
/// Use all supplied coordinates or compute a complete view. Invalid supplied
/// values never become a partial fallback. Automatic explicitly ignores DI.
pub fn layout(
view : @model.Bpmn,
policy~ : @model.GeometryPolicy = PreferDi,
limits~ : Limits = Limits::new(),
) -> Result {
let budget = Budget::{ limits, diagnostics: [], work: 0, failed: false, }
fn failed() -> Result {
{
geometry: None,
diagnostics: ReadOnlyArray::from_array(budget.diagnostics),
}
}
if limits.nodes <= 0 ||
limits.edges <= 0 ||
limits.work <= 0 ||
limits.output_units <= 0 {
budget.error(
"bpmn/layout-limit", "All BPMN geometry limits must be positive",
)
return failed()
}
guard input(view, budget) is Some(input) else { return failed() }
if policy != @model.GeometryPolicy::Automatic {
let (complete, shapes, connections) = supplied(view, input, budget)
if budget.failed {
return failed()
}
if complete {
return finish(view, shapes, connections, Supplied, budget)
}
if policy == RequireDi {
budget.error(
"bpmn/incomplete-di", "The selected view needs complete shape bounds and edge waypoints",
)
return failed()
}
budget.diagnostics.push(
@diagrams.Diagnostic::warning(
"bpmn/incomplete-di", "Supplied geometry is missing or incomplete; computing the whole view",
),
)
}
let (shapes, connections) = automatic(view, input, budget)
finish(view, shapes, connections, Automatic, budget)
}