///|
fn architecture_prefix(
model : @architecture.Architecture,
id_prefix : String?,
theme : @theme.Theme,
mode : Mode,
connectors : ConnectorOptions,
) -> String {
let raw = match id_prefix {
Some(value) => value
None => {
let source = @debug.to_string((model, theme, mode, connectors))
let mut hash : UInt64 = 14695981039346656037
for i in 0.. @writer.Node {
let children = text
.split("\n")
.to_array()
.mapi((i, line) => {
@writer.element(
"tspan",
attrs=[("x", x.to_string()), ("y", (y + i * 18).to_string())],
children=[@writer.text(line.to_owned())],
)
})
@writer.element(
"text",
attrs=[
("class", class),
("x", x.to_string()),
("y", y.to_string()),
("text-anchor", "middle"),
("font-family", font_stack),
("font-size", "13"),
("fill", p.color("text")),
],
children~,
)
}
///|
fn architecture_rect(
box : @architecture_layout.Rect,
fill : String,
stroke : String,
class : String,
) -> @writer.Node {
@writer.element("rect", attrs=[
("class", class),
("x", box.x.to_string()),
("y", box.y.to_string()),
("width", box.width.to_string()),
("height", box.height.to_string()),
("rx", "6"),
("fill", fill),
("stroke", stroke),
("stroke-width", "1.5"),
])
}
///|
fn architecture_path(
points : ReadOnlyArray[@architecture_layout.Point],
) -> String {
let out = StringBuilder()
for i, p in points {
out.write_string(
(if i == 0 { "M" } else { " L" }) +
p.x.to_string() +
" " +
p.y.to_string(),
)
}
out.to_string()
}
///|
fn paint_architecture(
model : @architecture.Architecture,
theme : @theme.Theme,
mode : Mode,
id_prefix : String?,
limits : @architecture_layout.Limits,
geometry? : @architecture_layout.Geometry,
connectors~ : ConnectorOptions = ConnectorOptions::new(),
) -> @render.Rendered {
if !connector_options_valid(connectors) {
return @render.Rendered::failed("image/svg+xml", [
@diagrams.Diagnostic::error(
"connector/invalid-option", "Invalid connector options",
),
])
}
let limits = limits.with_minimum_approach(connectors.minimum_approach)
let result = match geometry {
Some(g) =>
@architecture_layout.LayoutResult::{ geometry: Some(g), diagnostics: [], }
None => @architecture_layout.layout(model, limits~)
}
guard result.geometry is Some(g) else {
return @render.Rendered::failed(
"image/svg+xml",
result.diagnostics.iter().to_array()[:],
)
}
let diagnostics = result.diagnostics.iter().to_array()
let p = Paint::{ theme, mode, }
let prefix = architecture_prefix(model, id_prefix, theme, mode, connectors)
let defs = []
let used : Map[String, Bool] = Map([])
if mode == Adaptive {
defs.push(
@writer.element("style", children=[
@writer.text(style_sheet(theme, selector="svg#" + prefix + "diagram")),
]),
)
}
for
icon in model.groups
.iter()
.map(s => s.icon)
.concat(model.services.iter().map(s => s.icon)) {
if icon is Some(icon) {
if !@symbols.known(icon) {
diagnostics.push(
@diagrams.Diagnostic::warning(
"architecture/unknown-icon",
"Unresolved icon " + icon.pack + ":" + icon.key,
),
)
}
let id = @symbols.asset_id(icon, prefix)
if !used.contains(id) && @symbols.known(icon) {
used[id] = true
defs.push(@symbols.definition_node(icon, prefix))
}
}
}
let groups = []
let nodes = []
let paths = []
// Groups are painted outside-in; their shapes were resolved inside-out.
for i = g.shapes.length() - 1; i >= 0; i = i - 1 {
let shape = g.shapes[i]
if shape.kind != Group {
continue
}
let children = [
architecture_rect(
shape.box,
p.color("external-fill"),
p.color("external-stroke"),
"architecture-group",
),
architecture_label(
p,
shape.label,
shape.box.x + shape.box.width / 2,
shape.box.y + 23,
"group-label",
),
]
for group in model.groups {
if group.id == shape.id {
if group.icon is Some(icon) {
children.push(
@symbols.paint_node(
icon,
prefix,
(shape.box.x + 8).to_double(),
(shape.box.y + 8).to_double(),
24.0,
),
)
if !@symbols.known(icon) {
children.push(
architecture_label(
p,
icon.key,
shape.box.x + shape.box.width / 2,
shape.box.y + 30 + shape.label.split("\n").count() * 18,
"unknown-icon-label",
),
)
}
}
}
}
groups.push(
@writer.element(
"g",
attrs=[("class", "architecture-group"), ("data-id", shape.id)],
children~,
),
)
}
for shape in g.shapes {
if shape.kind == Group {
continue
}
let attrs = [
(
"class",
if shape.kind == Junction {
"architecture-junction"
} else {
"architecture-service"
},
),
("data-id", shape.id),
]
let children = []
if shape.kind == Junction {
children.push(
@writer.element("circle", attrs=[
("cx", (shape.box.x + shape.box.width / 2).to_string()),
("cy", (shape.box.y + shape.box.height / 2).to_string()),
("r", "8"),
("fill", p.color("arrow")),
]),
)
} else {
children.push(
architecture_rect(
shape.box,
p.color("backend-fill"),
p.color("backend-stroke"),
"service-box",
),
)
for service in model.services {
if service.id != shape.id {
continue
}
if service.icon is Some(icon) {
children.push(
@symbols.paint_node(
icon,
prefix,
(shape.box.x + shape.box.width / 2 - 20).to_double(),
(shape.box.y + 12).to_double(),
40.0,
),
)
if !@symbols.known(icon) {
children.push(
architecture_label(
p,
icon.key,
shape.box.x + shape.box.width / 2,
shape.box.y + 65,
"unknown-icon-label",
),
)
}
} else if service.icon_text is Some(text) {
children.push(
architecture_label(
p,
text,
shape.box.x + shape.box.width / 2,
shape.box.y + 38,
"literal-icon",
),
)
}
}
children.push(
architecture_label(
p,
shape.label,
shape.box.x + shape.box.width / 2,
shape.box.y +
shape.box.height -
22 -
(shape.label.split("\n").count() - 1) * 18,
"service-label",
),
)
}
nodes.push(@writer.element("g", attrs~, children~))
}
for i, route in g.connections {
let edge = route.connection
let attrs = [
("class", "architecture-path"),
("d", architecture_path(route.points)),
("fill", "none"),
("stroke", p.color("arrow")),
("stroke-width", "1.5"),
]
for (end, head) in [("start", edge.source_head), ("end", edge.target_head)] {
if head {
let id = prefix + "head-" + i.to_string() + "-" + end
defs.push(marker(id, Arrow, p.color("arrow"), false))
attrs.push(("marker-" + end, "url(#" + id + ")"))
}
}
let children = [@writer.element("path", attrs~)]
if route.label_box is Some(box) {
children.push(
architecture_rect(box, p.color("mask"), "none", "edge-label-mask"),
)
children.push(
architecture_label(
p,
edge.label.unwrap_or(""),
box.x + box.width / 2,
box.y + 14,
"edge-label",
),
)
}
paths.push(
@writer.element(
"g",
attrs=[
("class", "architecture-connection"),
("data-id", edge.id),
("data-from", edge.source.id),
("data-to", edge.target.id),
],
children~,
),
)
}
let children = [
@writer.element("title", children=[
@writer.text(
model.accessibility_title.unwrap_or(
model.title.unwrap_or("Architecture diagram"),
),
),
]),
@writer.element("desc", children=[
@writer.text(
model.accessibility_description.unwrap_or(
"Architecture diagram of authored services, junctions and connections.",
),
),
]),
@writer.element("defs", children=defs),
@writer.element("rect", attrs=[
("class", "bg"),
("x", "0"),
("y", "0"),
("width", g.width.to_string()),
("height", g.height.to_string()),
("fill", p.color("bg")),
]),
@writer.element(
"g",
attrs=[("class", "architecture-groups")],
children=groups,
),
@writer.element(
"g",
attrs=[("class", "architecture-connections")],
children=paths,
),
@writer.element(
"g",
attrs=[("class", "architecture-nodes")],
children=nodes,
),
]
if model.title is Some(title) {
children.push(
architecture_label(p, title, g.width / 2, 24, "diagram-title"),
)
}
let root = @writer.element(
"svg",
attrs=[
("xmlns", "http://www.w3.org/2000/svg"),
("id", prefix + "diagram"),
("viewBox", "0 0 " + g.width.to_string() + " " + g.height.to_string()),
("width", g.width.to_string()),
("height", g.height.to_string()),
("role", "img"),
("data-diagram", "architecture"),
],
children~,
)
let obstacles : Array[ConnectorObstacle] = []
let labels : Array[(String, ConnectorObstacle)] = []
for shape in g.shapes {
if shape.kind != Group {
let r = shape.box
obstacles.push({
owner: shape.id,
x: r.x.to_double(),
y: r.y.to_double(),
width: r.width.to_double(),
height: r.height.to_double(),
})
}
}
for edge in g.connections {
if edge.label_box is Some(r) {
labels.push(
(
"",
{
owner: edge.connection.id,
x: r.x.to_double(),
y: r.y.to_double(),
width: r.width.to_double(),
height: r.height.to_double(),
},
),
)
}
}
for shape in g.shapes {
if shape.kind == Group {
let r = shape.box
labels.push(
(
"",
{
owner: shape.id,
x: r.x.to_double(),
y: r.y.to_double(),
width: r.width.to_double(),
height: shape.header_height.to_double(),
},
),
)
}
}
guard finalize_connectors(
root,
connectors,
diagnostics,
obstacles=obstacles[:],
labels=labels[:],
resource_prefix=prefix,
)
is Some(root) else {
return @render.Rendered::failed("image/svg+xml", diagnostics)
}
let svg = @writer.to_string(root)
if svg.length() > limits.output_units {
return @render.Rendered::failed("image/svg+xml", [
@diagrams.Diagnostic::error(
"architecture/output-limit", "SVG output budget exhausted",
),
])
}
for
palette in (match mode {
Fixed(Dark) => [theme.dark]
Fixed(Light) => [theme.light]
Adaptive => [theme.dark, theme.light]
}) {
for finding in @architecture_layout.check(g, palette~) {
diagnostics.push(
@diagrams.Diagnostic::warning(
"architecture/legibility",
finding.code + ": " + finding.message + " " + finding.element,
),
)
}
}
@render.Rendered::ok(svg, "image/svg+xml", diagnostics=diagnostics[:])
}
///|
pub fn render_architecture(
model : @architecture.Architecture,
theme~ : @theme.Theme = @theme.Theme::classic(),
mode~ : Mode = Adaptive,
id_prefix? : String,
limits~ : @architecture_layout.Limits = @architecture_layout.Limits::new(),
connectors~ : ConnectorOptions = ConnectorOptions::new(),
) -> @render.Rendered {
paint_architecture(model, theme, mode, id_prefix, limits, connectors~)
}