///|
fn bn(value : Double) -> String { @bpmn_layout.svg_number(value).unwrap() }
///|
fn BpmnPaint::event_symbol(self : BpmnPaint, kind : @bpmn.EventKind, filled : Bool, stroke : String) -> Array[@writer.Node] {
let fill=if filled { stroke } else { self.paint.color("bg") }
match kind {
Message => [self.rect("1","4","22","16",stroke,fill~),self.path("M1 4 L12 13 L23 4",if filled { self.paint.color("bg") } else { stroke })]
Timer => {
let parts=[self.circle("12","12","10.5",stroke),self.path("M12 5 V12 L17 15",stroke)]
for (x1,y1,x2,y2) in [(12,2,12,4),(12,20,12,22),(2,12,4,12),(20,12,22,12),(5,5,6,6),(18,18,19,19),(5,19,6,18),(18,6,19,5)] { parts.push(self.path("M\{x1} \{y1} L\{x2} \{y2}",stroke,width="1")) }
parts
}
Conditional => [self.rect("4","1","16","22",stroke,fill~),self.path("M7 6 H17 M7 10 H17 M7 14 H17 M7 18 H17",if filled { self.paint.color("bg") } else { stroke },width="1")]
Link => [self.path("M1 8 H13 V2 L23 12 L13 22 V16 H1 Z",stroke,fill~)]
Signal => [self.path("M12 1 L23 22 H1 Z",stroke,fill~)]
Error => [self.path("M15 1 L4 13 H11 L8 23 L21 9 H14 Z",stroke,fill~)]
Escalation => [self.path("M12 1 L23 22 L12 16 L1 22 Z",stroke,fill~)]
Cancel => [self.path("M4 1 L12 9 L20 1 L23 4 L15 12 L23 20 L20 23 L12 15 L4 23 L1 20 L9 12 L1 4 Z",stroke,fill~)]
Compensation => [self.path("M12 2 L1 12 L12 22 Z M23 2 L12 12 L23 22 Z",stroke,fill~)]
Terminate => [self.circle("12","12","10",stroke,fill=stroke)]
}
}
///|
fn BpmnPaint::event_contents(self : BpmnPaint, markers : @bpmn.Markers, filled : Bool, stroke : String) -> Array[@writer.Node] {
if markers.events.length() > 1 && markers.parallel_multiple { [self.path("M12 2 V22 M2 12 H22",stroke,width="3")] }
else if markers.events.length() > 1 { [self.path("M12 1 L23 9 L19 22 H5 L1 9 Z",stroke,fill=if filled { stroke } else { self.paint.color("bg") })] }
else if markers.events.length() == 1 { self.event_symbol(markers.events[0].kind,filled,stroke) } else { [] }
}
///|
fn multiple_participants(value : String) -> Bool {
let digits=value.trim().trim_start(chars="+").trim_start(chars="0")
digits.length() > 0 && digits.iter().all(c => c >= '0' && c <= '9') && (digits.length() > 1 || digits != "1")
}
///|
fn BpmnPaint::task_symbol(self : BpmnPaint, kind : @bpmn.NodeKind, stroke : String) -> Array[@writer.Node] {
match kind {
UserTask => [self.circle("12","5","4",stroke),self.path("M4 23 V18 Q4 12 12 12 Q20 12 20 18 V23 M8 16 V23 M16 16 V23",stroke)]
ServiceTask => [self.path("M9 1 H15 L16 5 L19 7 L23 7 L24 13 L20 15 L19 18 L19 22 L13 24 L10 20 L7 19 L3 20 L1 14 L5 11 L5 8 L4 4 Z",stroke,fill=self.paint.color("bg")),self.circle("12","12","4",stroke)]
SendTask => self.event_symbol(Message,true,stroke)
ReceiveTask => self.event_symbol(Message,false,stroke)
ManualTask => [self.path("M2 15 L5 12 L8 14 V3 Q8 0 11 3 V11 V2 Q13 -1 15 2 V11 V4 Q17 1 19 4 V12 V7 Q22 5 23 8 V16 Q22 22 17 24 H10 Q7 22 2 17 Z",stroke,fill=self.paint.color("bg"))]
BusinessRuleTask => [self.rect("1","2","22","20",stroke),self.path("M1 8 H23 M8 8 V22 M1 14 H23",stroke),self.rect("1","2","22","6",stroke,fill=stroke)]
ScriptTask => [self.path("M5 1 H21 Q17 4 20 7 H7 V19 Q6 23 1 23 Q5 20 3 17 H17 Q19 20 16 23 H1 M8 10 H16 M8 14 H16",stroke)]
_ => []
}
}
///|
fn BpmnPaint::loop_symbol(self : BpmnPaint, kind : @bpmn.LoopKind, stroke : String) -> Array[@writer.Node] {
match kind {
None => []
Standard => [self.path("M18 6 A9 9 0 1 0 21 16 M13 5 H20 V12",stroke,width="2")]
Sequential => [self.path("M3 5 H21 M3 12 H21 M3 19 H21",stroke,width="2")]
Parallel => [self.path("M5 3 V21 M12 3 V21 M19 3 V21",stroke,width="2")]
}
}
///|
fn BpmnPaint::event_border(self : BpmnPaint, node : @bpmn.Node, stroke : String) -> Array[@writer.Node] {
let dash=if !node.markers.interrupting { Some("4 3") } else { None }
let parts=[self.circle("50","50","50",stroke,fill=self.paint.color("bg"),width=if node.kind == EndEvent { "4" } else { "1.5" },dash?)]
if node.kind == IntermediateCatchEvent || node.kind == IntermediateThrowEvent || node.kind == BoundaryEvent { parts.push(self.circle("50","50","40",stroke,dash?)) }
parts
}
///|
fn BpmnPaint::outline(self : BpmnPaint, shape : @bpmn_layout.Shape) -> @writer.Node {
let node=shape.node; let stroke=self.paint.color("\{@bpmn_layout.family(node.kind).name()}-stroke")
let fill=self.paint.color("\{@bpmn_layout.family(node.kind).name()}-fill")
let mut normalize="scale(0.01 0.0125)"
let parts=match node.kind {
StartEvent | EndEvent | IntermediateCatchEvent | IntermediateThrowEvent | BoundaryEvent => { normalize="scale(0.01 0.01)"; self.event_border(node,stroke) }
ExclusiveGateway | InclusiveGateway | ParallelGateway | ComplexGateway | EventBasedGateway => { normalize="scale(0.01 0.01)"; [self.path("M50 0 L100 50 L50 100 L0 50 Z",stroke,fill=self.paint.color("bg"))] }
Participant | Lane => [self.rect("0","0","100","80",stroke,fill=self.paint.color("bg"))]
Group => [self.rect("0","0","100","80",stroke,radius="10",dash="4 3 1 3")]
TextAnnotation => [self.path("M12 0 H0 V80 H12",stroke)]
DataObject | DataObjectReference | DataInput | DataOutput => [self.path("M0 0 H75 L100 20 V80 H0 Z M75 0 V20 H100",stroke,fill=self.paint.color("bg"))]
DataStore | DataStoreReference => [self.path("M0 12 A50 12 0 0 1 100 12 V68 A50 12 0 0 1 0 68 Z M0 12 A50 12 0 0 0 100 12 M0 18 A50 12 0 0 0 100 18 M0 24 A50 12 0 0 0 100 24",stroke,fill=self.paint.color("bg"))]
Message => [self.rect("0","0","100","80",stroke,fill=self.paint.color("bg")),self.path("M0 0 L50 45 L100 0",stroke)]
Transaction => [self.rect("0","0","100","80",stroke,fill~,radius="10"),self.rect("4","4","92","72",stroke,radius="7")]
_ => [self.rect("0","0","100","80",stroke,fill~,radius="10",weight=if node.kind == CallActivity { "4" } else { "1.5" },dash?=if node.markers.event_subprocess { Some("4 3") } else { None })]
}
if @bpmn_layout.activity(node.kind) { parts.insert(0,self.rect("0","0","100","80","none",fill=self.paint.color("bg"),radius="10")) }
// Stroke widths are user units even when the normalized symbol is resized.
let normalized=parts.map(part => match part {
Element(name,attrs,children) => self.element(name,attrs=attrs.iter().concat([("vector-effect","non-scaling-stroke")].iter()).to_array(),children~)
_ => part
})
let r=shape.bounds
self.element("g",attrs=[("transform","translate(\{r.x.token} \{r.y.token}) scale(\{r.width.token} \{r.height.token})"),("aria-hidden","true")],children=[self.element("g",attrs=[("transform",normalize)],children=normalized)])
}
///|
fn BpmnPaint::markers(self : BpmnPaint, shape : @bpmn_layout.Shape, starts : ArrayView[@bpmn.Node]) -> Array[@writer.Node] {
let node=shape.node; let r=shape.box
let stroke=self.paint.color("\{@bpmn_layout.family(node.kind).name()}-stroke")
let parts=[]
fn place(x : Double, y : Double, scale : Double, icons : ArrayView[@writer.Node]) -> Unit {
parts.push(self.element("g",attrs=[("transform","translate(\{bn(x)} \{bn(y)}) scale(\{bn(scale)})")],children=icons))
}
if @bpmn_layout.activity(node.kind) {
let kind=if node.kind == CallActivity { node.markers.called_kind.unwrap_or(Task) } else { node.kind }
place(10.0,8.0,0.8,self.task_symbol(kind,stroke))
if kind == ReceiveTask && node.markers.instantiate { parts.push(self.circle("20","18","13",stroke)) }
let icons : Array[Array[@writer.Node]]=[]
if (node.kind == SubProcess || node.kind == AdHocSubProcess || node.kind == Transaction || (node.kind == CallActivity && node.markers.called_kind == Some(SubProcess))) && !node.expanded.unwrap_or(false) { icons.push([self.rect("2","2","20","20",stroke,fill=self.paint.color("bg")),self.path("M6 12 H18 M12 6 V18",stroke)]) }
if node.markers.compensation { icons.push(self.event_symbol(Compensation,false,stroke)) }
if node.markers.loop_kind != None { icons.push(self.loop_symbol(node.markers.loop_kind,stroke)) }
if node.kind == AdHocSubProcess { icons.push([self.path("M2 14 Q7 4 12 14 Q17 24 22 14",stroke,width="2")]) }
for i,icon in icons { place(r.width/2.0-icons.length().to_double()*10.0+i.to_double()*20.0,r.height-19.0,0.65,icon) }
if node.markers.event_subprocess && !node.expanded.unwrap_or(false) {
if starts.length() == 1 {
let start=starts[0]
let badge=@bpmn.Node::new("badge",StartEvent,markers=start.markers)
let border=self.event_border(badge,stroke)
parts.push(self.element("g",attrs=[("transform","translate(5 5) scale(0.24)")],children=border))
place(10.0,10.0,0.55,self.event_contents(start.markers,false,stroke))
}
}
}
if node.kind == StartEvent || node.kind == EndEvent || node.kind == IntermediateCatchEvent || node.kind == IntermediateThrowEvent || node.kind == BoundaryEvent {
let filled=node.kind == EndEvent || node.kind == IntermediateThrowEvent
let size=Double::min(r.width,r.height)*0.55
let symbols=self.event_contents(node.markers,filled,stroke)
place((r.width-size)/2.0,(r.height-size)/2.0,size/24.0,symbols)
}
if node.kind == ExclusiveGateway || node.kind == InclusiveGateway || node.kind == ParallelGateway || node.kind == ComplexGateway || node.kind == EventBasedGateway {
let symbols=match node.kind {
ExclusiveGateway => if node.geometry.bind(g => g.marker_visible).unwrap_or(true) { [self.path("M3 3 L21 21 M21 3 L3 21",stroke,width="3")] } else { [] }
InclusiveGateway => [self.circle("12","12","10",stroke,width="2.5")]
ParallelGateway => [self.path("M12 1 V23 M1 12 H23",stroke,width="3")]
ComplexGateway => [self.path("M12 1 V23 M1 12 H23 M4 4 L20 20 M4 20 L20 4",stroke,width="2.5")]
_ => {
let parallel=bpmn_detail(node,"eventGatewayType") == Some("Parallel")
let symbols=[self.circle("12","12","11",stroke)]
if !node.markers.instantiate { symbols.push(self.circle("12","12","8.5",stroke,width="1")) }
if parallel { symbols.push(self.path("M12 5 V19 M5 12 H19",stroke,width="2")) }
else { symbols.push(self.path("M12 5 L19 10 L16 18 H8 L5 10 Z",stroke)) }
symbols
}
}
let size=Double::min(r.width,r.height)*0.55
place((r.width-size)/2.0,(r.height-size)/2.0,size/24.0,symbols)
}
if node.kind == Participant || node.kind == Lane {
if node.geometry.bind(g => g.horizontal).unwrap_or(true) { parts.push(self.path("M30 0 V\{bn(r.height)}",stroke)) }
else { parts.push(self.path("M0 30 H\{bn(r.width)}",stroke)) }
if node.markers.details.iter().any(p => p.0 == "participantMultiplicity/@maximum" && multiple_participants(p.1)) { place(r.width/2.0-8.0,r.height-18.0,0.65,self.loop_symbol(Parallel,stroke)) }
}
if node.kind == DataInput || node.kind == DataOutput { place(6.0,6.0,0.6,[self.path("M2 8 H12 V2 L23 12 L12 22 V16 H2 Z",stroke,fill=if node.kind == DataOutput { stroke } else { self.paint.color("bg") })]) }
if node.markers.collection { place(r.width/2.0-8.0,r.height-17.0,0.6,self.loop_symbol(Parallel,stroke)) }
[self.element("g",attrs=[("transform","translate(\{shape.bounds.x.token} \{shape.bounds.y.token})"),("class","bpmn-markers")],children=parts)]
}