///|
pub struct Limits {
nodes : Int32
edges : Int32
depth : Int32
work : Int32
output_units : Int32
width : Int32
height : Int32
minimum_approach : Double
} derive(Eq, Debug)
///|
pub fn Limits::new(
nodes~ : Int32 = 10000,
edges~ : Int32 = 50000,
depth~ : Int32 = 256,
work~ : Int32 = 1000000,
output_units~ : Int32 = 16777216,
width~ : Int32 = 1000000,
height~ : Int32 = 1000000,
minimum_approach~ : Double = 24.0,
) -> Limits {
{ minimum_approach, nodes, edges, depth, work, output_units, width, height, }
}
///|
pub fn Limits::with_minimum_approach(self : Limits, minimum_approach : Double) -> Limits { { ..self, minimum_approach } }
///|
pub struct Connection {
transition : @model.Transition
source : String
target : String
line : ReadOnlyArray[@vector.Point]
label : @vector.Label?
} derive(Eq, Debug)
///|
pub struct Geometry {
view : @model.StateDiagram
shapes : ReadOnlyArray[@vector.Shape]
connections : ReadOnlyArray[Connection]
width : Int32
height : Int32
priv graph : @graph.Geometry
priv drawing : @vector.Result
boundaries : ReadOnlyArray[(String,String)]
} derive(Eq, Debug)
///|
pub struct Result {
geometry : Geometry?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)
///|
pub fn diagnostics(values : ArrayView[@diagrams.Diagnostic]) -> ReadOnlyArray[@diagrams.Diagnostic] {
ReadOnlyArray::from_array(values.map(d => { ..d, code: d.code.replace_all(old="flowchart/",new="state/").replace_all(old="swimlane/",new="state/") }))
}
///|
/// Region containers are drawing groups only. They never create graph edges.
pub fn layout(view : @model.StateDiagram, limits~ : Limits = Limits::new()) -> Result {
let findings = @model.validate(view, nodes_limit=limits.nodes, regions_limit=limits.nodes, edges_limit=limits.edges, depth_limit=limits.depth, work_limit=limits.work)
if !findings.is_empty() { return { geometry: None, diagnostics: findings } }
for state in view.states {
if state.role is Unsupported(role) { return {geometry:None,diagnostics:[@diagrams.Diagnostic::error("state/unsupported-role","Unsupported state symbol: "+role)]} }
}
let mut adapter_work=0
let mut exhausted=false
fn charge(n : Int32) -> Bool {
if n < 0 || n > limits.work-adapter_work { exhausted=true; false }
else { adapter_work+=n; true }
}
let states=Map(view.states.map(s => (s.id,s)))
let regions=Map(view.regions.map(r => (r.id,r)))
let paths : Map[String,Array[String]] = Map([])
fn path(region : String) -> Array[String] {
if paths.get(region) is Some(p) { if !charge(p.length()) { return [] }; return p }
let reversed=[]
let mut cursor=Some(region)
while cursor is Some(id) {
if !charge(2) { return [] }
let r=regions[id]
if r.owner is Some(owner) {
reversed.push(id)
reversed.push(owner)
cursor=Some(states[owner].region)
} else { cursor=None }
}
let ordered=[]
for i=reversed.length()-1; i>=0; i=i-1 { ordered.push(reversed[i]) }
paths[region]=ordered
ordered
}
let owned : Map[String,Array[@model.Region]] = Map([])
for r in view.regions { if r.owner is Some(owner) { owned.get_or_init(owner,()=>[]).push(r) } }
let ordered=[]
for s in view.states {
let rs=owned.get(s.id).unwrap_or([])
if rs.length()>1 { ordered.push((s.id,if rs[1].separator == Vertical { @flowchart.Direction::LR } else { TB })) }
}
let mut padding=48.0
for e in view.transitions { padding=padding.max(e.label.length().to_double()*8.0+32.0) }
let groups=[]
for s in view.states {
if s.role == Composite {
groups.push(@flowchart.Group::new(s.id,label=s.label + (if s.descriptions.is_empty() { "" } else { "\n" + s.descriptions.join("\n") }),parent?=path(s.region).last(),expanded=s.expanded,direction?=(match s.direction { Some(d) => Some(d); None => { let rs=owned.get(s.id).unwrap_or([]); if rs.length()>1 { Some(if rs[1].separator == Vertical { @flowchart.Direction::LR } else { TB }) } else { None } } })))
}
}
for r in view.regions {
if r.owner is Some(owner) {
groups.push(@flowchart.Group::new(r.id,label="",parent=owner,direction?=r.direction))
}
}
let nodes=[]
for s in view.states {
if s.role == Composite { continue }
let (shape,label) : (String,String) = match s.role {
Initial => ("f-circ", "")
Final => ("fr-circ", "")
Choice => ("diamond", s.label)
Fork | Join => ("fork", "")
ShallowHistory => ("circle", "H")
DeepHistory => ("circle", "H*")
EntryPoint => ("circle", s.label)
ExitPoint => ("cross-circ", s.label)
InputPin => ("rect", "in: " + s.label)
OutputPin => ("rect", "out: " + s.label)
ExpansionInput => ("div-rect", "in[]: " + s.label)
ExpansionOutput => ("div-rect", "out[]: " + s.label)
_ => ("rounded", s.label)
}
let label=label + (if s.descriptions.is_empty() { "" } else { "\n" + s.descriptions.join("\n") })
nodes.push(@flowchart.Node::new(s.id,label~,shape~,groups=path(s.region)[:]))
}
let edges=view.transitions.map(e => @flowchart.Connection::new(e.id,e.source,e.target,label=e.label,style=e.style,source_marker=e.source_marker,target_marker=e.target_marker,min_length=e.min_length,direction_hint?=e.direction_hint,origin?=e.origin))
let notes=view.notes.map(n => @flowchart.Note::new(n.id,n.text,targets=n.targets[:],placement=n.placement,connection_targets=n.connection_targets[:],groups=path(n.region)[:]))
if exhausted { return {geometry:None,diagnostics:[@diagrams.Diagnostic::error("state/work-limit","State layout adaptation work exhausted")]} }
let graph=@flowchart.Flowchart::new(view.id,nodes[:],edges[:],groups=groups[:],notes=notes[:],direction=view.direction,title?=view.title,accessibility_title?=view.accessibility_title,accessibility_description=view.accessibility_description.unwrap_or("State structure and supplied scenarios. Guards, actions and history remain descriptive data."))
let result=@graph.layout(graph,ordered_scopes=ordered[:],region_directions=view.regions.map(r => (r.id,if r.separator == Vertical { @flowchart.Direction::LR } else { TB }))[:],scope_padding=padding,limits=@graph.Limits::new(nodes=limits.nodes,edges=limits.edges,depth=limits.depth,work=limits.work,output_units=limits.output_units,width=limits.width,height=limits.height,minimum_approach=limits.minimum_approach))
guard result.geometry is Some(g) else { return {geometry:None,diagnostics:diagnostics(result.diagnostics[:])} }
let boundaries=view.states.iter().filter_map(s => {
if [@model.Role::EntryPoint,ExitPoint,InputPin,OutputPin,ExpansionInput,ExpansionOutput].contains(s.role) {
regions[s.region].owner.map(owner => (s.id,owner))
} else { None }
}).to_array()
let drawing=if boundaries.is_empty() { g.drawing_result() } else { g.drawing().overlay([],boundaries=boundaries[:],limits=@vector.Limits::new(nodes=limits.nodes,edges=limits.edges,depth=limits.depth,work=limits.work,output_units=limits.output_units,minimum_approach=limits.minimum_approach)) }
guard drawing.geometry is Some(d) else { return {geometry:None,diagnostics:diagnostics(drawing.diagnostics[:])} }
if d.width>limits.width || d.height>limits.height { return {geometry:None,diagnostics:[@diagrams.Diagnostic::error("state/layout-limit","State dimensions exceed configured bounds after boundary routing")]} }
let by_id=Map(view.transitions.map(e => (e.id,e)))
let connections=d.connections.iter().filter_map(e => by_id.get(e.connection.id).map(t => Connection::{transition:t,source:e.source,target:e.target,line:e.line,label:e.label})).to_array()
{ geometry:Some({view,shapes:d.shapes,connections:ReadOnlyArray::from_array(connections),width:d.width,height:d.height,graph:g,drawing,boundaries:ReadOnlyArray::from_array(boundaries.iter().concat(boundaries.iter().map(p => (p.0,states[p.0].region))).to_array())}),diagnostics:diagnostics(result.diagnostics[:]) }
}
///|
pub fn Geometry::drawing(self : Geometry) -> @graph.Geometry { self.graph }
///|
pub fn check(geometry : Geometry, palette : @theme.Palette, work_limit~ : Int32 = 1000000) -> ReadOnlyArray[@vector.Finding] { @vector.check(geometry.drawing.geometry.unwrap(),@vector.palette(palette),work_limit~,intentional_containment=geometry.graph.intentional_containment()[:],boundary_attachments=geometry.boundaries[:]) }
///|
pub fn Geometry::drawing_result(self : Geometry) -> @vector.Result { self.drawing }
///|
pub extend Limits with Eq::{equal, not_equal}
///|
pub extend Limits with @debug.Debug::{to_repr}
///|
pub extend Connection with Eq::{equal, not_equal}
///|
pub extend Connection with @debug.Debug::{to_repr}
///|
pub extend Geometry with Eq::{equal, not_equal}
///|
pub extend Geometry with @debug.Debug::{to_repr}
///|
pub extend Result with Eq::{equal, not_equal}
///|
pub extend Result with @debug.Debug::{to_repr}
///|
pub struct RegionSeparator {
region : String
orientation : @model.Separator
start : @vector.Point
end : @vector.Point
} derive(Eq, Debug)
///|
pub extend RegionSeparator with Eq::{equal, not_equal}
///|
pub extend RegionSeparator with @debug.Debug::{to_repr}
///|
/// The actual drawn compartment lines, independent of source transitions.
pub fn Geometry::separators(self : Geometry) -> ReadOnlyArray[RegionSeparator] {
let shapes=Map(self.shapes.map(s => (s.id,s)))
let previous : Map[String,String] = Map([])
let result=[]
for r in self.view.regions {
if r.owner is Some(owner) && shapes.get(r.id) is Some(shape) && shapes.get(owner) is Some(parent) {
if previous.get(owner) is Some(id) && shapes.get(id) is Some(old) {
let a=old.box; let b=shape.box; let p=parent.box
let (start,end) : (@vector.Point,@vector.Point) = if r.separator == Vertical {
let x=(a.x+a.width+b.x)/2.0
({x,y:(a.y.min(b.y)-24.0).max(p.y+48.0)},{x,y:p.y+p.height})
} else {
let y=(a.y+a.height+b.y)/2.0
({x:(a.x.min(b.x)-24.0).max(p.x),y},{x:p.x+p.width,y})
}
result.push({region:r.id,orientation:r.separator,start,end})
}
previous[owner]=r.id
}
}
ReadOnlyArray::from_array(result)
}