// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Engine API: layout contract shared between layout engines and exporters.
///
/// The layout pipeline is intentionally functional:
/// - engines take an immutable graph
/// - engines return a `@graph.LayoutPatch`
/// - callers apply the patch to obtain a new graph
///|
/// Layout configuration
pub struct LayoutConfig {
/// Horizontal spacing between objects
horizontal_spacing : Double
/// Vertical spacing between objects
vertical_spacing : Double
/// Padding inside containers
container_padding : Double
/// Default object width
default_width : Double
/// Default object height
default_height : Double
/// Edge padding from objects
edge_padding : Double
} derive(Debug)
///|
/// Layout direction
pub(all) enum Direction {
Down
Right
Up
Left
} derive(Eq, Debug)
///|
pub impl Show for Direction with fn output(self, logger) {
let text = match self {
Down => "Down"
Right => "Right"
Up => "Up"
Left => "Left"
}
logger.write_string(text)
}
///|
/// Layout engine capability flags aligned with the reference's layout feature model.
pub(all) enum LayoutFeature {
NearObject
ContainerDimensions
TopLeft
DescendantEdges
} derive(Eq, Debug)
///|
/// Layout error
pub(all) suberror LayoutError {
InvalidGraph(String)
CycleDetected(String)
Unsupported(String)
} derive(Eq, Debug)
///|
pub impl Show for LayoutError with fn output(self, logger) {
match self {
InvalidGraph(message) => {
logger.write_string("InvalidGraph(")
logger.write_object(message)
logger.write_char(')')
}
CycleDetected(message) => {
logger.write_string("CycleDetected(")
logger.write_object(message)
logger.write_char(')')
}
Unsupported(message) => {
logger.write_string("Unsupported(")
logger.write_object(message)
logger.write_char(')')
}
}
}
///|
/// Construct an `engine_api.LayoutError` from outside this package.
pub fn invalid_graph(message : String) -> LayoutError {
InvalidGraph(message)
}
///|
pub fn cycle_detected(message : String) -> LayoutError {
CycleDetected(message)
}
///|
pub fn unsupported(message : String) -> LayoutError {
Unsupported(message)
}
///|
pub fn LayoutConfig::new() -> LayoutConfig {
{
horizontal_spacing: 60.0,
vertical_spacing: 100.0,
container_padding: 50.0,
default_width: 100.0,
default_height: 60.0,
edge_padding: 20.0,
}
}
///|
pub fn LayoutConfig::with_spacing(
horizontal : Double,
vertical : Double,
) -> LayoutConfig {
let config = LayoutConfig::new()
{ ..config, horizontal_spacing: horizontal, vertical_spacing: vertical }
}
///|
/// Layout engine interface.
///
/// Engines receive a graph whose object and edge lookup fields use canonical
/// absolute source syntax. They must not mutate the input graph and return a
/// patch whose keys belong to that graph.
pub(open) trait LayoutEngine {
fn layout(Self, @graph.GraphInput, LayoutConfig, Direction) -> @graph.LayoutPatch raise LayoutError
fn engine_name(Self) -> String
fn features(Self) -> Array[LayoutFeature]
}
///|
fn direction_from_keyword(raw : String) -> Direction? {
match raw.to_lower() {
"down" => Some(Down)
"up" => Some(Up)
"left" => Some(Left)
"right" => Some(Right)
_ => None
}
}
///|
fn graph_layout_direction(
graph : @graph.GraphInput,
fallback : Direction,
) -> Direction {
match graph.root.direction {
Some(raw) =>
match direction_from_keyword(raw) {
Some(direction) => direction
None => fallback
}
None => fallback
}
}
///|
fn has_layout_feature(
features : Array[LayoutFeature],
feature : LayoutFeature,
) -> Bool {
for current in features {
if current == feature {
return true
}
}
false
}
///|
fn build_object_by_abs_id_syntax(
graph : @graph.GraphInput,
) -> Map[String, @graph.ObjectInput] {
let object_by_id : Map[String, @graph.ObjectInput] = Map([])
object_by_id[graph.root.abs_id_syntax] = graph.root
for obj in graph.objects {
object_by_id[obj.abs_id_syntax] = obj
}
object_by_id
}
///|
fn build_parent_id_by_child_abs_id_syntax(
graph : @graph.GraphInput,
) -> Map[String, String] {
let parent_id_by_child_id : Map[String, String] = Map([])
for child_id in graph.root.child_ids {
parent_id_by_child_id[child_id] = graph.root.abs_id_syntax
}
for obj in graph.objects {
for child_id in obj.child_ids {
parent_id_by_child_id[child_id] = obj.abs_id_syntax
}
}
parent_id_by_child_id
}
///|
fn is_grid_diagram_object(obj : @graph.ObjectInput) -> Bool {
obj.grid_rows is Some(_) || obj.grid_columns is Some(_)
}
///|
fn is_container_object(obj : @graph.ObjectInput) -> Bool {
!obj.child_ids.is_empty()
}
///|
fn has_explicit_dimensions(obj : @graph.ObjectInput) -> Bool {
match obj.box {
Some(box) => box.width > 0.0 || box.height > 0.0
None => false
}
}
///|
fn is_descendant_of_id(
obj_id : String,
ancestor_id : String,
parent_id_by_child_id : Map[String, String],
) -> Bool {
let mut current = obj_id
while true {
if current == ancestor_id {
return true
}
match parent_id_by_child_id.get(current) {
Some(parent_id) => current = parent_id
None => break
}
}
false
}
///|
fn is_inside_sequence_diagram(
graph : @graph.GraphInput,
object_by_id : Map[String, @graph.ObjectInput],
parent_id_by_child_id : Map[String, String],
id : String,
) -> Bool {
if graph.root.shape_type == SequenceDiagram {
return true
}
let mut current = id
while true {
match object_by_id.get(current) {
Some(obj) => if obj.shape_type == SequenceDiagram { return true }
None => ()
}
match parent_id_by_child_id.get(current) {
Some(parent_id) => current = parent_id
None => break
}
}
false
}
///|
fn edge_display_id(edge : @graph.EdgeInput) -> String {
let operator = match (edge.src_arrow, edge.dst_arrow) {
(false, false) => "--"
(false, true) => "->"
(true, false) => "<-"
(true, true) => "<->"
}
"(\{edge.src_id_syntax} \{operator} \{edge.dst_id_syntax})[\{edge.index}]"
}
///|
fn check_feature_support(
engine_name : String,
features : Array[LayoutFeature],
graph : @graph.GraphInput,
) -> Unit raise LayoutError {
if graph.uses_latex_labels() {
raise unsupported("LaTeX labels are not supported")
}
if graph.uses_sketch_styles() {
raise unsupported("sketch rendering is not supported")
}
let object_by_id = build_object_by_abs_id_syntax(graph)
let parent_id_by_child_id = build_parent_id_by_child_abs_id_syntax(graph)
for obj in graph.objects {
if (obj.top is Some(_) || obj.left is Some(_)) &&
!has_layout_feature(features, TopLeft) {
raise unsupported(
"Object \"\{obj.abs_id_syntax}\" has attribute \"top\" and/or \"left\" set, but layout engine \"\{engine_name}\" does not support locked positions. See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.",
)
}
if has_explicit_dimensions(obj) &&
is_container_object(obj) &&
!is_grid_diagram_object(obj) &&
!has_layout_feature(features, ContainerDimensions) {
raise unsupported(
"Object \"\{obj.abs_id_syntax}\" has attribute \"width\" and/or \"height\" set, but layout engine \"\{engine_name}\" does not support dimensions set on containers. See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.",
)
}
match obj.near {
Some(near_key) =>
if graph.find_object(near_key) is Some(_) &&
!has_layout_feature(features, NearObject) {
raise unsupported(
"Object \"\{obj.abs_id_syntax}\" has \"near\" set to another object, but layout engine \"\{engine_name}\" only supports constant values for \"near\". See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.",
)
}
None => ()
}
}
if has_layout_feature(features, DescendantEdges) {
return
}
for edge in graph.edges {
if is_inside_sequence_diagram(
graph,
object_by_id,
parent_id_by_child_id,
edge.src_id_syntax,
) ||
is_inside_sequence_diagram(
graph,
object_by_id,
parent_id_by_child_id,
edge.dst_id_syntax,
) {
continue
}
let src = match object_by_id.get(edge.src_id_syntax) {
Some(obj) => obj
None => continue
}
let dst = match object_by_id.get(edge.dst_id_syntax) {
Some(obj) => obj
None => continue
}
if !is_container_object(src) && !is_container_object(dst) {
continue
}
if edge.src_id_syntax == edge.dst_id_syntax {
raise unsupported(
"Connection \"\{edge_display_id(edge)}\" is a self loop on a container, but layout engine \"\{engine_name}\" does not support this. See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.",
)
}
if is_descendant_of_id(
edge.src_id_syntax,
edge.dst_id_syntax,
parent_id_by_child_id,
) ||
is_descendant_of_id(
edge.dst_id_syntax,
edge.src_id_syntax,
parent_id_by_child_id,
) {
raise unsupported(
"Connection \"\{edge_display_id(edge)}\" goes from a container to a descendant, but layout engine \"\{engine_name}\" does not support this. See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.",
)
}
}
}
///|
fn clone_graph_with_parts(
graph : @graph.GraphInput,
root : @graph.ObjectInput,
objects : Array[@graph.ObjectInput],
edges : Array[@graph.EdgeInput],
layers : Array[@graph.LayerInput],
scenarios : Array[@graph.ScenarioInput],
steps : Array[@graph.StepInput],
) -> @graph.GraphInput {
@graph.GraphInput::from_parts(
graph.name,
root,
objects,
edges,
graph.sequence_fragments,
graph.activation_boxes,
graph.sequence_notes,
graph.sequence_fragments_layout,
layers,
scenarios,
steps,
graph.legend,
is_folder_only=graph.is_folder_only,
data=graph.data,
)
}
///|
fn normalize_sequence_root(graph : @graph.GraphInput) -> @graph.GraphInput {
if graph.root.shape_type != SequenceDiagram {
return graph
}
let child_ids : Map[String, Bool] = Map([])
for obj in graph.objects {
for child_id in obj.child_ids {
child_ids[child_id] = true
}
}
let root_child_ids : Array[String] = []
for obj in graph.objects {
if !child_ids.contains(obj.abs_id_syntax) {
root_child_ids.push(obj.abs_id_syntax)
}
}
let root = clone_object_with_parts(graph.root, graph.root.box, root_child_ids)
clone_graph_with_parts(
graph,
root,
graph.objects,
graph.edges,
graph.layers,
graph.scenarios,
graph.steps,
)
}
///|
fn find_sql_column_index(
object_by_id : Map[String, @graph.ObjectInput],
parent_id_by_child_id : Map[String, String],
field_id : String,
) -> Int? {
let field = match object_by_id.get(field_id) {
Some(f) => f
None => return None
}
let parent_id = match parent_id_by_child_id.get(field.abs_id_syntax) {
Some(p) => p
None => return None
}
let parent = match object_by_id.get(parent_id) {
Some(p) => p
None => return None
}
if parent.shape_type != SqlTable {
return None
}
let mut i = 0
for child_id in parent.child_ids {
if child_id == field.abs_id_syntax {
return Some(i)
}
i = i + 1
}
None
}
///|
fn assign_sql_column_indices(graph : @graph.GraphInput) -> @graph.GraphInput {
let object_by_id : Map[String, @graph.ObjectInput] = Map([])
object_by_id[graph.root.abs_id_syntax] = graph.root
for obj in graph.objects {
object_by_id[obj.abs_id_syntax] = obj
}
let parent_id_by_child_id : Map[String, String] = Map([])
for child_id in graph.root.child_ids {
parent_id_by_child_id[child_id] = graph.root.abs_id_syntax
}
for obj in graph.objects {
for child_id in obj.child_ids {
parent_id_by_child_id[child_id] = obj.abs_id_syntax
}
}
let edges : Array[@graph.EdgeInput] = []
for e in graph.edges {
let src_column_index = match e.src_column_index {
Some(i) => Some(i)
None =>
find_sql_column_index(
object_by_id,
parent_id_by_child_id,
e.src_id_syntax,
)
}
let dst_column_index = match e.dst_column_index {
Some(i) => Some(i)
None =>
find_sql_column_index(
object_by_id,
parent_id_by_child_id,
e.dst_id_syntax,
)
}
edges.push(
@graph.EdgeInput::from_parts(
e.index,
e.src_id,
e.dst_id,
e.src_arrow,
e.dst_arrow,
e.src_arrowhead,
e.dst_arrowhead,
e.src_arrowhead_label,
e.dst_arrowhead_label,
e.src_arrowhead_label_color,
e.dst_arrowhead_label_color,
e.src_anchor,
e.dst_anchor,
e.label,
e.style,
e.route,
e.bend_points,
e.is_curve,
e.z_index,
e.reference_count,
e.label_box,
src_column_index,
dst_column_index,
src_id_syntax=e.src_id_syntax,
dst_id_syntax=e.dst_id_syntax,
references=e.references,
icon=e.icon,
icon_position=e.icon_position,
icon_border_radius=e.icon_border_radius,
link=e.link,
classes=e.classes,
),
)
}
clone_graph_with_parts(
graph,
graph.root,
graph.objects,
edges,
graph.layers,
graph.scenarios,
graph.steps,
)
}
///|
fn prepare_graph_for_layout(
graph : @graph.GraphInput,
) -> @graph.GraphInput raise LayoutError {
let prepared = assign_sql_column_indices(normalize_sequence_root(graph))
ignore(compile_layout_problem(prepared))
prepared
}
///|
fn resolve_default_object_positions(
obj : @graph.ObjectInput,
engine_name : String,
) -> @graph.ObjectInput {
let mut icon_position = obj.icon_position
let mut label_position = obj.label_position
let is_grid = obj.grid_rows is Some(_) || obj.grid_columns is Some(_)
if is_grid {
if obj.icon is Some(_) && icon_position is None {
icon_position = Some("INSIDE_TOP_LEFT")
}
if obj.label != "" && label_position is None {
label_position = Some("INSIDE_TOP_CENTER")
}
} else if obj.icon is Some(_) && icon_position is None {
if !obj.child_ids.is_empty() {
icon_position = Some(
if engine_name == "elk" {
"INSIDE_TOP_LEFT"
} else {
"OUTSIDE_TOP_LEFT"
},
)
if label_position is None {
label_position = Some(
if engine_name == "elk" {
"INSIDE_TOP_RIGHT"
} else {
"OUTSIDE_TOP_RIGHT"
},
)
}
} else if obj.shape_type == Class ||
obj.shape_type == SqlTable ||
obj.language is Some(_) {
icon_position = Some("OUTSIDE_TOP_LEFT")
} else {
icon_position = Some("INSIDE_MIDDLE_CENTER")
}
}
if !is_grid && obj.label != "" && label_position is None {
if !obj.child_ids.is_empty() {
label_position = Some(
if engine_name == "elk" {
"INSIDE_TOP_CENTER"
} else {
"OUTSIDE_TOP_CENTER"
},
)
} else if obj.shape_type == Image || obj.shape_type == Person {
label_position = Some("OUTSIDE_BOTTOM_CENTER")
} else if obj.icon is Some(_) {
label_position = Some("INSIDE_TOP_CENTER")
}
}
@graph.ObjectInput::from_parts(
obj.id,
obj.label,
obj.shape_type,
obj.style,
obj.box,
obj.label_box,
obj.child_ids,
obj.z_index,
obj.icon,
obj.tooltip,
obj.link,
obj.classes,
obj.grid_rows,
obj.grid_columns,
obj.grid_gap,
obj.horizontal_gap,
obj.vertical_gap,
obj.grid_column_span,
obj.grid_row_span,
obj.near,
obj.top,
obj.left,
direction=obj.direction,
language=obj.language,
sql_constraints=obj.sql_constraints,
id_val=obj.id_val,
id_syntax=obj.id_syntax,
abs_id_syntax=obj.abs_id_syntax,
references=obj.references,
icon_position~,
tooltip_position=obj.tooltip_position,
label_position~,
grid_row_directed=obj.grid_row_directed,
)
}
///|
fn resolve_default_object_positions_for_engine(
graph : @graph.GraphInput,
engine_name : String,
) -> @graph.GraphInput {
let objects : Array[@graph.ObjectInput] = []
for obj in graph.objects {
objects.push(resolve_default_object_positions(obj, engine_name))
}
clone_graph_with_parts(
graph,
resolve_default_object_positions(graph.root, engine_name),
objects,
graph.edges,
graph.layers,
graph.scenarios,
graph.steps,
)
}
///|
fn compile_layout_problem(
graph : @graph.GraphInput,
) -> @layout_core.LayoutProblem raise LayoutError {
@layout_core.compile(graph) catch {
error => raise invalid_graph(error.message())
}
}
///|
fn validate_layout_patch(
problem : @layout_core.LayoutProblem,
patch : @graph.LayoutPatch,
engine_name : String,
) -> Unit raise LayoutError {
problem.validate_patch(patch) catch {
error =>
raise invalid_graph(
"layout engine \"\{engine_name}\" produced an invalid patch: \{error.message()}",
)
}
}
///|
fn ensure_orthogonal_route(route : Array[@graph.Point]) -> Array[@graph.Point] {
if route.length() != 2 {
return route
}
let p0 = route[0]
let p1 = route[1]
let dx = (p1.x - p0.x).abs()
let dy = (p1.y - p0.y).abs()
if dx < 1.0 || dy < 1.0 {
return route
}
[p0, @graph.Point::new(p0.x, p1.y), p1]
}
///|
fn normalize_routes(graph : @graph.GraphInput) -> @graph.GraphInput {
let edges : Array[@graph.EdgeInput] = []
for e in graph.edges {
let route = if edge_prefers_curved(e) {
e.route
} else {
ensure_orthogonal_route(e.route)
}
edges.push(
@graph.EdgeInput::from_parts(
e.index,
e.src_id,
e.dst_id,
e.src_arrow,
e.dst_arrow,
e.src_arrowhead,
e.dst_arrowhead,
e.src_arrowhead_label,
e.dst_arrowhead_label,
e.src_arrowhead_label_color,
e.dst_arrowhead_label_color,
e.src_anchor,
e.dst_anchor,
e.label,
e.style,
route,
e.bend_points,
e.is_curve,
e.z_index,
e.reference_count,
e.label_box,
e.src_column_index,
e.dst_column_index,
src_id_syntax=e.src_id_syntax,
dst_id_syntax=e.dst_id_syntax,
references=e.references,
icon=e.icon,
icon_position=e.icon_position,
icon_border_radius=e.icon_border_radius,
link=e.link,
classes=e.classes,
),
)
}
clone_graph_with_parts(
graph,
graph.root,
graph.objects,
edges,
graph.layers,
graph.scenarios,
graph.steps,
)
}
///|
fn recompute_final_edge_label_boxes_like_d2(
graph : @graph.GraphInput,
) -> @graph.GraphInput {
let edges : Array[@graph.EdgeInput] = []
for edge in graph.edges {
let label_box = match edge.label_box {
Some(box) if edge.label != "" && !edge.route.is_empty() => {
let center = d2_target_route_point_at_percent_like_d2(edge.route, 0.5)
Some(
@graph.Box::new(
d2_label_chop_precision_like_d2(center.x - box.width / 2.0),
d2_label_chop_precision_like_d2(center.y - box.height / 2.0),
box.width,
box.height,
),
)
}
other => other
}
edges.push(
clone_edge_with_layout(edge, edge.route, edge.bend_points, label_box),
)
}
clone_graph_with_parts(
graph,
graph.root,
graph.objects,
edges,
graph.layers,
graph.scenarios,
graph.steps,
)
}
///|
fn d2_target_route_point_at_percent_like_d2(
route : Array[@graph.Point],
percent : Double,
) -> @graph.Point {
if route.is_empty() {
return @graph.Point::new(0.0, 0.0)
}
if route.length() == 1 {
return d2_target_route_point_like_d2(route[0])
}
let target_route : Array[@graph.Point] = []
for point in route {
target_route.push(d2_target_route_point_like_d2(point))
}
let mut total = 0.0
for i in 1..= target {
let t = (target - walked) / segment
return @graph.Point::new(
p0.x + (p1.x - p0.x) * t,
p0.y + (p1.y - p0.y) * t,
)
}
walked = walked + segment
}
target_route[target_route.length() - 1]
}
///|
fn d2_target_route_point_like_d2(point : @graph.Point) -> @graph.Point {
@graph.Point::new(
Float::from_double((point.x * 1000.0).to_int().to_double() / 1000.0).to_double(),
Float::from_double((point.y * 1000.0).to_int().to_double() / 1000.0).to_double(),
)
}
///|
fn d2_route_point_distance_like_d2(
a : @graph.Point,
b : @graph.Point,
) -> Double {
let dx = b.x - a.x
let dy = b.y - a.y
(dx * dx + dy * dy).sqrt()
}
///|
fn d2_label_chop_precision_like_d2(value : Double) -> Double {
let rounded = (Float::from_double(value * 10000.0).to_double() / 10000.0).round()
if rounded == 0.0 {
0.0
} else {
rounded
}
}
///|
fn edge_prefers_curved(edge : @graph.EdgeInput) -> Bool {
match edge.style.curved {
Some(v) => v.as_bool().unwrap_or(true)
None => true
}
}
///|
priv struct GridChildLayoutMetrics {
layout_width : Double
layout_height : Double
label_position : String?
icon_position : String?
initial_margin : GridMargin
}
///|
priv struct GridMargin {
left : Double
top : Double
right : Double
bottom : Double
}
///|
const D2_GRID_STARTING_THRESHOLD : Double = 1.2
///|
const D2_GRID_THRESHOLD_STEP_SIZE : Double = 0.25
///|
const D2_GRID_MIN_THRESHOLD_ATTEMPTS : Int = 1
///|
const D2_GRID_MAX_THRESHOLD_ATTEMPTS : Int = 3
///|
const D2_GRID_ATTEMPT_LIMIT : Int = 100000
///|
const D2_GRID_SKIP_LIMIT : Int = 10000000
///|
priv struct GridSemanticContext {
groups : Map[String, Array[String]]
child_ids : Map[String, Bool]
}
///|
priv struct GridContainerLayout {
width : Double
height : Double
child_boxes : Map[String, @graph.Box]
}
///|
fn prepare_layout_semantics(
graph : @graph.GraphInput,
config : LayoutConfig,
) -> (@graph.GraphInput, GridSemanticContext) {
let groups = collect_grid_semantic_groups(graph)
if groups.is_empty() {
return (graph, { groups, child_ids: Map([]) })
}
let pre_sized = apply_grid_semantic_parent_boxes(graph, groups, config)
let child_ids : Map[String, Bool] = Map([])
for _, ids in groups {
for id in ids {
child_ids[id] = true
}
}
(detach_grid_children(pre_sized, groups), { groups, child_ids })
}
///|
fn restore_layout_semantics(
original_graph : @graph.GraphInput,
laid_out_graph : @graph.GraphInput,
context : GridSemanticContext,
config : LayoutConfig,
) -> @graph.GraphInput {
if context.groups.is_empty() {
laid_out_graph
} else {
restore_grid_semantics(original_graph, laid_out_graph, context, config)
}
}
///|
fn collect_grid_semantic_groups(
graph : @graph.GraphInput,
) -> Map[String, Array[String]] {
let object_by_id : Map[String, @graph.ObjectInput] = Map([])
object_by_id[graph.root.abs_id_syntax] = graph.root
for obj in graph.objects {
object_by_id[obj.abs_id_syntax] = obj
}
let incident_ids : Map[String, Bool] = Map([])
for edge in graph.edges {
incident_ids[edge.src_id_syntax] = true
incident_ids[edge.dst_id_syntax] = true
}
let groups : Map[String, Array[String]] = Map([])
let parents : Array[@graph.ObjectInput] = [graph.root]
for obj in graph.objects {
parents.push(obj)
}
for parent in parents {
if parent.grid_rows is None && parent.grid_columns is None {
continue
}
let child_ids : Array[String] = []
let mut blocked = false
for child_id in parent.child_ids {
guard object_by_id.contains(child_id) else { continue }
if incident_ids.contains(child_id) {
blocked = true
break
}
child_ids.push(child_id)
}
if blocked || child_ids.is_empty() {
continue
}
groups[parent.abs_id_syntax] = child_ids
}
groups
}
///|
fn apply_grid_semantic_parent_boxes(
graph : @graph.GraphInput,
groups : Map[String, Array[String]],
config : LayoutConfig,
) -> @graph.GraphInput {
let object_by_id : Map[String, @graph.ObjectInput] = Map([])
object_by_id[graph.root.abs_id_syntax] = graph.root
for obj in graph.objects {
object_by_id[obj.abs_id_syntax] = obj
}
let parent_id_by_child_id = build_parent_id_by_child_abs_id_syntax(graph)
let parent_box_by_id : Map[String, @graph.Box] = Map([])
for parent_id, child_ids in groups {
if parent_id == graph.root.abs_id_syntax {
continue
}
let parent = match object_by_id.get(parent_id) {
Some(v) => v
None => continue
}
let child_size_by_id : Map[String, (Double, Double)] = Map([])
for child_id in child_ids {
match object_by_id.get(child_id) {
Some(child) => {
let inside_sequence = is_inside_sequence_diagram(
graph, object_by_id, parent_id_by_child_id, child_id,
)
child_size_by_id[child_id] = semantic_object_size(
child,
config,
inside_sequence~,
)
}
None => ()
}
}
match
estimate_grid_container_layout_like_d2(
parent, child_ids, child_size_by_id,
) {
Some(layout) => {
let (x, y) = match parent.box {
Some(box) => (box.x, box.y)
None => (0.0, 0.0)
}
parent_box_by_id[parent_id] = @graph.Box::new(
x,
y,
layout.width,
layout.height,
)
}
None => ()
}
}
if parent_box_by_id.is_empty() {
return graph
}
let objects : Array[@graph.ObjectInput] = []
for obj in graph.objects {
let box = match parent_box_by_id.get(obj.abs_id_syntax) {
Some(v) => Some(v)
None => obj.box
}
objects.push(clone_object_with_parts(obj, box, obj.child_ids))
}
clone_graph_with_parts(
graph,
graph.root,
objects,
graph.edges,
graph.layers,
graph.scenarios,
graph.steps,
)
}
///|
fn detach_grid_children(
graph : @graph.GraphInput,
groups : Map[String, Array[String]],
) -> @graph.GraphInput {
let group_child_ids : Map[String, Bool] = Map([])
for _, child_ids in groups {
for child_id in child_ids {
group_child_ids[child_id] = true
}
}
let objects : Array[@graph.ObjectInput] = []
for obj in graph.objects {
if group_child_ids.contains(obj.abs_id_syntax) {
continue
}
let child_ids = match groups.get(obj.abs_id_syntax) {
Some(filtered) => {
let removed : Map[String, Bool] = Map([])
for id in filtered {
removed[id] = true
}
let kept : Array[String] = []
for id in obj.child_ids {
if !removed.contains(id) {
kept.push(id)
}
}
kept
}
None => obj.child_ids
}
objects.push(clone_object_with_parts(obj, obj.box, child_ids))
}
let root_child_ids = match groups.get(graph.root.abs_id_syntax) {
Some(filtered) => {
let removed : Map[String, Bool] = Map([])
for id in filtered {
removed[id] = true
}
let kept : Array[String] = []
for id in graph.root.child_ids {
if !removed.contains(id) {
kept.push(id)
}
}
kept
}
None => graph.root.child_ids
}
let root = clone_object_with_parts(graph.root, graph.root.box, root_child_ids)
clone_graph_with_parts(
graph,
root,
objects,
graph.edges,
graph.layers,
graph.scenarios,
graph.steps,
)
}
///|
fn estimate_grid_container_layout_like_d2(
parent : @graph.ObjectInput,
child_ids : Array[String],
child_size_by_id : Map[String, (Double, Double)],
) -> GridContainerLayout? {
match estimate_root_grid_child_boxes(parent, child_ids, child_size_by_id) {
Some(content_child_boxes) => {
let mut content_width = 0.0
let mut content_height = 0.0
for _, box in content_child_boxes {
let right = box.x + box.width
let bottom = box.y + box.height
if right > content_width {
content_width = right
}
if bottom > content_height {
content_height = bottom
}
}
let (hgap, vgap) = grid_semantic_gaps(parent)
let padding = grid_semantic_padding_like_d2(
parent, content_width, content_height, hgap, vgap,
)
let child_boxes : Map[String, @graph.Box] = Map([])
for child_id, box in content_child_boxes {
child_boxes[child_id] = @graph.Box::new(
box.x + padding.left,
box.y + padding.top,
box.width,
box.height,
)
}
Some({
width: content_width + padding.left + padding.right,
height: content_height + padding.top + padding.bottom,
child_boxes,
})
}
None => None
}
}
///|
/// Estimate a real nested grid container, including D2's outside-label
/// margins and container padding, from backend-produced child sizes.
pub fn estimate_nested_grid_layout_like_d2(
parent : @graph.ObjectInput,
child_ids : Array[String],
child_size_by_id : Map[String, (Double, Double)],
object_by_id : Map[String, @graph.ObjectInput],
) -> (Double, Double, Map[String, @graph.Box])? {
let metrics_by_id : Map[String, GridChildLayoutMetrics] = Map([])
let layout_size_by_id : Map[String, (Double, Double)] = Map([])
for child_id in child_ids {
let child = match object_by_id.get(child_id) {
Some(value) => value
None => return None
}
let (width, height) = match child_size_by_id.get(child_id) {
Some(value) => value
None => return None
}
let (label_position, icon_position) = grid_child_effective_positions_like_d2(
child,
)
let margin = grid_child_margin_like_d2(
child, width, height, label_position, icon_position,
)
let metrics : GridChildLayoutMetrics = {
layout_width: width + margin.left + margin.right,
layout_height: height + margin.top + margin.bottom,
label_position,
icon_position,
initial_margin: margin,
}
metrics_by_id[child_id] = metrics
layout_size_by_id[child_id] = (metrics.layout_width, metrics.layout_height)
}
let layout = match
estimate_grid_container_layout_like_d2(parent, child_ids, layout_size_by_id) {
Some(value) => value
None => return None
}
let boxes : Map[String, @graph.Box] = Map([])
for child_id in child_ids {
let child = object_by_id.get(child_id).unwrap()
let metrics = metrics_by_id.get(child_id).unwrap()
let cell = layout.child_boxes.get(child_id).unwrap()
boxes[child_id] = grid_child_restored_box_like_d2(
child,
metrics,
cell.x,
cell.y,
cell.width,
cell.height,
)
}
Some((layout.width, layout.height, boxes))
}
///|
fn grid_child_layout_metrics_like_d2(
obj : @graph.ObjectInput,
config : LayoutConfig,
) -> GridChildLayoutMetrics {
let (base_width, base_height) = grid_child_base_dimensions_like_d2(
obj, config,
)
let (label_position, icon_position) = grid_child_effective_positions_like_d2(
obj,
)
let initial_margin = grid_child_margin_like_d2(
obj, base_width, base_height, label_position, icon_position,
)
{
layout_width: base_width + initial_margin.left + initial_margin.right,
layout_height: base_height + initial_margin.top + initial_margin.bottom,
label_position,
icon_position,
initial_margin,
}
}
///|
fn grid_child_base_dimensions_like_d2(
obj : @graph.ObjectInput,
config : LayoutConfig,
) -> (Double, Double) {
match obj.box {
Some(box) =>
if box.width > 0.0 && box.height > 0.0 {
(box.width, box.height)
} else {
semantic_object_size(obj, config)
}
None => semantic_object_size(obj, config)
}
}
///|
fn inferred_label_position_from_layout_like_d2(
obj : @graph.ObjectInput,
) -> String? {
match (obj.box, obj.label_box) {
(Some(box), Some(label_box)) => {
let label_bottom = label_box.y + label_box.height
let box_bottom = box.y + box.height
let label_center_x = label_box.x + label_box.width / 2.0
let box_center_x = box.x + box.width / 2.0
let centered_x = (label_center_x - box_center_x).abs() <= 1.0
if label_bottom <= box.y {
if centered_x {
Some("OUTSIDE_TOP_CENTER")
} else {
Some("OUTSIDE_TOP_LEFT")
}
} else if label_box.y >= box_bottom {
if centered_x {
Some("OUTSIDE_BOTTOM_CENTER")
} else {
Some("OUTSIDE_BOTTOM_LEFT")
}
} else if label_box.y >= box.y && label_bottom <= box_bottom {
if label_box.y <= box.y + label_box.height + D2_LABEL_PADDING {
if centered_x {
Some("INSIDE_TOP_CENTER")
} else {
Some("INSIDE_TOP_LEFT")
}
} else if centered_x {
Some("INSIDE_MIDDLE_CENTER")
} else {
Some("INSIDE_MIDDLE_LEFT")
}
} else {
None
}
}
_ => None
}
}
///|
fn grid_child_effective_positions_like_d2(
obj : @graph.ObjectInput,
) -> (String?, String?) {
let mut label_position = match obj.label_position {
Some(position) => Some(position)
None => inferred_label_position_from_layout_like_d2(obj)
}
let mut icon_position = obj.icon_position
let mut positioned_label = false
if obj.icon is Some(_) && icon_position is None {
if !obj.child_ids.is_empty() {
icon_position = Some("OUTSIDE_TOP_LEFT")
if label_position is None {
label_position = Some("OUTSIDE_TOP_RIGHT")
positioned_label = true
}
} else {
icon_position = Some("INSIDE_MIDDLE_CENTER")
}
}
if !positioned_label &&
grid_child_has_label_like_d2(obj) &&
label_position is None {
if !obj.child_ids.is_empty() {
label_position = Some("OUTSIDE_TOP_CENTER")
} else if grid_child_has_outside_bottom_label_like_d2(obj) {
label_position = Some("OUTSIDE_BOTTOM_CENTER")
} else if obj.icon is Some(_) {
label_position = Some("INSIDE_TOP_CENTER")
} else {
label_position = Some("INSIDE_MIDDLE_CENTER")
}
}
(label_position, icon_position)
}
///|
fn grid_child_has_label_like_d2(obj : @graph.ObjectInput) -> Bool {
match obj.shape_type {
Text => false
Class => false
SqlTable => false
Code => false
_ => obj.label != ""
}
}
///|
fn grid_child_has_outside_bottom_label_like_d2(
obj : @graph.ObjectInput,
) -> Bool {
match obj.shape_type {
Image => true
Person => true
_ => false
}
}
///|
fn grid_child_margin_like_d2(
obj : @graph.ObjectInput,
width : Double,
height : Double,
label_position : String?,
icon_position : String?,
) -> GridMargin {
let mut left = 0.0
let mut top = 0.0
let mut right = 0.0
let mut bottom = 0.0
if grid_child_has_label_like_d2(obj) {
match label_position {
Some(position) => {
let dims = grid_child_label_dimensions_like_d2(obj)
let label_width = dims.0
let label_height = dims.1
let upper = position.to_upper()
if upper.contains("OUTSIDE_TOP_") {
top = label_height
} else if upper.contains("OUTSIDE_BOTTOM_") {
bottom = label_height
} else if upper.contains("OUTSIDE_LEFT_") {
left = label_width
} else if upper.contains("OUTSIDE_RIGHT_") {
right = label_width
}
if label_width > width {
let dx = label_width - width
if upper == "OUTSIDE_TOP_LEFT" || upper == "OUTSIDE_BOTTOM_LEFT" {
right = dx
} else if upper == "OUTSIDE_TOP_CENTER" ||
upper == "OUTSIDE_BOTTOM_CENTER" {
let half = (dx / 2.0).ceil()
left = half
right = half
} else if upper == "OUTSIDE_TOP_RIGHT" ||
upper == "OUTSIDE_BOTTOM_RIGHT" {
left = dx
}
}
if label_height > height {
let dy = label_height - height
if upper == "OUTSIDE_LEFT_TOP" || upper == "OUTSIDE_RIGHT_TOP" {
bottom = dy
} else if upper == "OUTSIDE_LEFT_MIDDLE" ||
upper == "OUTSIDE_RIGHT_MIDDLE" {
let half = (dy / 2.0).ceil()
top = half
bottom = half
} else if upper == "OUTSIDE_LEFT_BOTTOM" ||
upper == "OUTSIDE_RIGHT_BOTTOM" {
top = dy
}
}
}
None => ()
}
}
if obj.icon is Some(_) {
match icon_position {
Some(position) => {
let upper = position.to_upper()
let icon_size = D2_MAX_ICON_SIZE + D2_LABEL_PADDING
if upper.contains("OUTSIDE_TOP_") {
top = if top > icon_size { top } else { icon_size }
} else if upper.contains("OUTSIDE_BOTTOM_") {
bottom = if bottom > icon_size { bottom } else { icon_size }
} else if upper.contains("OUTSIDE_LEFT_") {
left = if left > icon_size { left } else { icon_size }
} else if upper.contains("OUTSIDE_RIGHT_") {
right = if right > icon_size { right } else { icon_size }
}
}
None => ()
}
}
{ left, top, right, bottom }
}
///|
fn grid_child_label_dimensions_like_d2(
obj : @graph.ObjectInput,
) -> (Double, Double) {
let font_size = match obj.style.font_size {
Some(v) => v.as_int().unwrap_or(16)
None => 16
}
let is_bold = object_label_bold_like_d2(obj, false)
let is_italic = style_bool(obj.style.italic, false)
let is_mono = obj.shape_type == Code || style_is_mono(obj.style.font)
let dims = if obj.shape_type == Code {
@text_metrics.measure_label_code(obj.label, font_size, is_bold, is_italic)
} else {
@text_metrics.measure_label(
obj.label,
font_size,
is_mono,
is_bold,
is_italic,
)
}
(dims.width + D2_LABEL_PADDING, dims.height + D2_LABEL_PADDING)
}
///|
fn grid_child_restored_box_like_d2(
obj : @graph.ObjectInput,
metrics : GridChildLayoutMetrics,
x : Double,
y : Double,
cell_w : Double,
cell_h : Double,
) -> @graph.Box {
let initial_dx = metrics.initial_margin.left + metrics.initial_margin.right
let initial_dy = metrics.initial_margin.top + metrics.initial_margin.bottom
let mut width = cell_w - initial_dx
let mut height = cell_h - initial_dy
if width < 0.0 {
width = 0.0
}
if height < 0.0 {
height = 0.0
}
let new_margin = grid_child_margin_like_d2(
obj,
width,
height,
metrics.label_position,
metrics.icon_position,
)
let new_dx = new_margin.left + new_margin.right
let new_dy = new_margin.top + new_margin.bottom
if new_dx < initial_dx {
width += initial_dx - new_dx
}
if new_dy < initial_dy {
height += initial_dy - new_dy
}
@graph.Box::new(x + new_margin.left, y + new_margin.top, width, height)
}
///|
fn grid_semantic_gaps(parent : @graph.ObjectInput) -> (Double, Double) {
let mut hgap = 40.0
let mut vgap = 40.0
match parent.grid_gap {
Some(v) => {
hgap = v
vgap = v
}
None => ()
}
match parent.horizontal_gap {
Some(v) => hgap = v
None => ()
}
match parent.vertical_gap {
Some(v) => vgap = v
None => ()
}
(hgap, vgap)
}
///|
fn grid_semantic_padding_like_d2(
parent : @graph.ObjectInput,
content_width : Double,
content_height : Double,
hgap : Double,
vgap : Double,
) -> GridMargin {
let (label_position, icon_position) = grid_container_effective_positions_like_d2(
parent,
)
let spacing_padding = grid_container_spacing_padding_like_d2(
parent, label_position, icon_position,
)
let mut left = spacing_padding.left
let mut top = spacing_padding.top
let mut right = spacing_padding.right
let mut bottom = spacing_padding.bottom
let (label_width, label_height) = if grid_child_has_label_like_d2(parent) {
object_label_dimensions_like_d2(parent, 2.0 * D2_LABEL_PADDING)
} else {
(0.0, 0.0)
}
match label_position {
Some(position) => {
let upper = position.to_upper()
if label_width > 0.0 &&
(
upper.contains("OUTSIDE_TOP_") ||
upper.contains("INSIDE_TOP_") ||
upper.contains("INSIDE_BOTTOM_") ||
upper.contains("OUTSIDE_BOTTOM_")
) {
let overflow = label_width - content_width
if overflow > 0.0 {
let half = overflow / 2.0
left = left + half
right = right + half
}
}
if label_height > 0.0 &&
(
upper.contains("OUTSIDE_LEFT_") ||
upper.contains("INSIDE_MIDDLE_") ||
upper.contains("OUTSIDE_RIGHT_")
) {
let overflow = label_height - content_height
if overflow > 0.0 {
let half = overflow / 2.0
top = top + half
bottom = bottom + half
}
}
}
None => ()
}
if icon_position == Some("INSIDE_TOP_LEFT") &&
label_position == Some("INSIDE_TOP_CENTER") {
let icon_size = D2_MAX_ICON_SIZE + 2.0 * D2_LABEL_PADDING
if left < icon_size {
left = icon_size
}
if right < icon_size {
right = icon_size
}
let min_width = 2.0 * icon_size + label_width
let overflow = min_width - content_width
if overflow > 0.0 {
let half = overflow / 2.0
if left < half {
left = half
}
if right < half {
right = half
}
}
}
let horizontal_padding = if parent.grid_gap is Some(_) ||
parent.horizontal_gap is Some(_) {
hgap
} else {
60.0
}
let vertical_padding = if parent.grid_gap is Some(_) ||
parent.vertical_gap is Some(_) {
vgap
} else {
60.0
}
if top < vertical_padding {
top = vertical_padding
}
if bottom < vertical_padding {
bottom = vertical_padding
}
if left < horizontal_padding {
left = horizontal_padding
}
if right < horizontal_padding {
right = horizontal_padding
}
{ left, top, right, bottom }
}
///|
fn grid_container_effective_positions_like_d2(
obj : @graph.ObjectInput,
) -> (String?, String?) {
let label_position = if obj.label_position is Some(_) {
obj.label_position
} else if grid_child_has_label_like_d2(obj) {
Some("INSIDE_TOP_CENTER")
} else {
None
}
let icon_position = if obj.icon_position is Some(_) {
obj.icon_position
} else if obj.icon is Some(_) {
Some("INSIDE_TOP_LEFT")
} else {
None
}
(label_position, icon_position)
}
///|
fn grid_container_spacing_padding_like_d2(
obj : @graph.ObjectInput,
label_position : String?,
icon_position : String?,
) -> GridMargin {
let mut left = 0.0
let mut top = 0.0
let mut right = 0.0
let mut bottom = 0.0
if grid_child_has_label_like_d2(obj) {
let (label_width, label_height) = object_label_dimensions_like_d2(
obj,
2.0 * D2_LABEL_PADDING,
)
match label_position {
Some(position) => {
let upper = position.to_upper()
if upper.contains("INSIDE_TOP_") {
top = label_height
} else if upper.contains("INSIDE_BOTTOM_") {
bottom = label_height
} else if upper == "INSIDE_MIDDLE_LEFT" {
left = label_width
} else if upper == "INSIDE_MIDDLE_RIGHT" {
right = label_width
}
}
None => ()
}
}
if obj.icon is Some(_) {
let icon_size = D2_MAX_ICON_SIZE + 2.0 * D2_LABEL_PADDING
match icon_position {
Some(position) => {
let upper = position.to_upper()
if upper.contains("INSIDE_TOP_") {
if top < icon_size {
top = icon_size
}
} else if upper.contains("INSIDE_BOTTOM_") {
if bottom < icon_size {
bottom = icon_size
}
} else if upper == "INSIDE_MIDDLE_LEFT" {
if left < icon_size {
left = icon_size
}
} else if upper == "INSIDE_MIDDLE_RIGHT" {
if right < icon_size {
right = icon_size
}
}
}
None => ()
}
}
{ left, top, right, bottom }
}
///|
fn object_label_dimensions_like_d2(
obj : @graph.ObjectInput,
padding : Double,
) -> (Double, Double) {
let font_size = match obj.style.font_size {
Some(v) => v.as_int().unwrap_or(16)
None => 16
}
let is_bold = object_label_bold_like_d2(obj, false)
let is_italic = style_bool(obj.style.italic, false)
let is_mono = obj.shape_type == Code || style_is_mono(obj.style.font)
let dims = if obj.shape_type == Code {
@text_metrics.measure_label_code(obj.label, font_size, is_bold, is_italic)
} else {
@text_metrics.measure_label(
obj.label,
font_size,
is_mono,
is_bold,
is_italic,
)
}
(dims.width + padding, dims.height + padding)
}
///|
fn resize_label_box_for_position_like_d2(
box : @graph.Box,
width : Double,
height : Double,
position : String?,
) -> @graph.Box {
let upper = position.unwrap_or("INSIDE_MIDDLE_CENTER").to_upper()
let x = if upper.has_suffix("_RIGHT") {
box.x + box.width - width
} else if upper.has_suffix("_CENTER") {
box.x + (box.width - width) / 2.0
} else {
box.x
}
let y = if upper.contains("_BOTTOM_") {
box.y + box.height - height
} else if upper.contains("_MIDDLE_") {
box.y + (box.height - height) / 2.0
} else {
box.y
}
@graph.Box::new(x, y, width, height)
}
///|
fn label_box_for_position_like_d2(
box : @graph.Box,
width : Double,
height : Double,
position : String?,
) -> @graph.Box {
let upper = position.unwrap_or("INSIDE_MIDDLE_CENTER").to_upper()
let outside_left = upper.has_prefix("OUTSIDE_LEFT_")
let outside_right = upper.has_prefix("OUTSIDE_RIGHT_")
let outside_top = upper.has_prefix("OUTSIDE_TOP_")
let outside_bottom = upper.has_prefix("OUTSIDE_BOTTOM_")
let x = if outside_left {
box.x - D2_LABEL_PADDING - width
} else if outside_right {
box.x + box.width + D2_LABEL_PADDING
} else if upper.has_suffix("_LEFT") {
box.x + D2_LABEL_PADDING
} else if upper.has_suffix("_RIGHT") {
box.x + box.width - D2_LABEL_PADDING - width
} else {
box.x + (box.width - width) / 2.0
}
let y = if outside_top {
box.y - D2_LABEL_PADDING - height
} else if outside_bottom {
box.y + box.height + D2_LABEL_PADDING
} else if upper.has_suffix("_TOP") || upper.contains("_TOP_") {
box.y + D2_LABEL_PADDING
} else if upper.has_suffix("_BOTTOM") || upper.contains("_BOTTOM_") {
box.y + box.height - D2_LABEL_PADDING - height
} else {
box.y + (box.height - height) / 2.0
}
@graph.Box::new(x, y, width, height)
}
///|
fn root_grid_gaps(parent : @graph.ObjectInput) -> (Double, Double) {
let default_gap = if parent.grid_rows is Some(_) ||
parent.grid_columns is Some(_) {
40.0
} else {
20.0
}
let mut vertical_gap = default_gap
let mut horizontal_gap = default_gap
match parent.grid_gap {
Some(v) => {
vertical_gap = v
horizontal_gap = v
}
None => ()
}
match parent.vertical_gap {
Some(v) => vertical_gap = v
None => ()
}
match parent.horizontal_gap {
Some(v) => horizontal_gap = v
None => ()
}
(horizontal_gap, vertical_gap)
}
///|
fn root_grid_segment_size(
segment : Array[Int],
sizes : Array[Double],
gap : Double,
) -> Double {
let mut out = 0.0
for i in segment {
out = out + sizes[i]
}
if segment.length() > 1 {
out = out + gap * (segment.length() - 1).to_double()
}
out
}
///|
fn root_grid_layout_distance(
layout : Array[Array[Int]],
target_size : Double,
sizes : Array[Double],
gap : Double,
) -> Double {
let mut total = 0.0
for segment in layout {
let segment_size = root_grid_segment_size(segment, sizes, gap)
total = total + (segment_size - target_size).abs()
}
total
}
///|
fn root_grid_gen_layout(
count : Int,
cut_indices : Array[Int],
) -> Array[Array[Int]] {
let layout : Array[Array[Int]] = []
let mut obj_index = 0
let mut i = 0
while i <= cut_indices.length() {
let stop = if i < cut_indices.length() { cut_indices[i] } else { count - 1 }
let row : Array[Int] = []
while obj_index <= stop && obj_index < count {
row.push(obj_index)
obj_index = obj_index + 1
}
layout.push(row)
i = i + 1
}
layout
}
///|
fn root_grid_cut_combinations(end : Int, cuts_left : Int) -> Array[Array[Int]] {
let out : Array[Array[Int]] = []
if end < 2 || cuts_left <= 0 {
return out
}
let mut index = end - 1
while index >= cuts_left {
if cuts_left > 1 {
let inner = root_grid_cut_combinations(index, cuts_left - 1)
for cut_list in inner {
let combined = cut_list.copy()
combined.push(index - 1)
out.push(combined)
}
} else {
out.push([index - 1])
}
index = index - 1
}
out
}
///|
fn root_grid_fast_layout(
sizes : Array[Double],
target_size : Double,
n_cuts : Int,
gap : Double,
) -> Array[Array[Int]]? {
if n_cuts <= 0 {
return Some(root_grid_gen_layout(sizes.length(), []))
}
let mut debt = 0.0
let division : Array[Int] = []
let mut row_size = 0.0
for i, size in sizes {
if row_size == 0.0 {
if size > target_size - debt {
division.push(i)
debt = debt + (size - target_size)
} else {
row_size = row_size + size
}
continue
}
if row_size + gap + size / 2.0 > target_size - debt {
division.push(i - 1)
debt = debt + (row_size - target_size)
row_size = size
} else {
row_size = row_size + gap + size
}
}
if division.length() == n_cuts {
return Some(root_grid_gen_layout(sizes.length(), division))
}
None
}
///|
fn root_grid_sum(values : Array[Double]) -> Double {
let mut s = 0.0
for v in values {
s = s + v
}
s
}
///|
fn root_grid_avg(values : Array[Double]) -> Double {
if values.is_empty() {
return 0.0
}
root_grid_sum(values) / values.length().to_double()
}
///|
fn root_grid_variance(values : Array[Double]) -> Double {
if values.is_empty() {
return 0.0
}
let mean = root_grid_avg(values)
let mut total = 0.0
for value in values {
let dev = mean - value
total = total + dev * dev
}
total / values.length().to_double()
}
///|
fn root_grid_stddev(values : Array[Double]) -> Double {
root_grid_variance(values).sqrt()
}
///|
fn root_grid_best_layout(
sizes : Array[Double],
target_size : Double,
n_cuts : Int,
gap : Double,
) -> Array[Array[Int]] {
if n_cuts <= 0 {
return root_grid_gen_layout(sizes.length(), [])
}
let mut best_layout : Array[Array[Int]] = []
let mut best_dist = 1000000000000.0
let mut has_best = false
let mut fast_is_best = false
match root_grid_fast_layout(sizes, target_size, n_cuts, gap) {
Some(fast_layout) => {
let dist = root_grid_layout_distance(fast_layout, target_size, sizes, gap)
if dist == 0.0 {
return fast_layout
}
best_layout = fast_layout
best_dist = dist
has_best = true
fast_is_best = true
}
None => ()
}
let sd = root_grid_stddev(sizes)
let mut threshold_attempts = sd.ceil().to_int()
if threshold_attempts < D2_GRID_MIN_THRESHOLD_ATTEMPTS {
threshold_attempts = D2_GRID_MIN_THRESHOLD_ATTEMPTS
} else if threshold_attempts > D2_GRID_MAX_THRESHOLD_ATTEMPTS {
threshold_attempts = D2_GRID_MAX_THRESHOLD_ATTEMPTS
}
let combinations = root_grid_cut_combinations(sizes.length(), n_cuts)
let mut ok_threshold = D2_GRID_STARTING_THRESHOLD
let mut attempt = 0
while attempt < threshold_attempts || !has_best {
let starting_cache : Map[Int, Bool] = Map([])
let mut count = 0
let mut skip_count = 0
for division in combinations {
if count >= D2_GRID_ATTEMPT_LIMIT || skip_count >= D2_GRID_SKIP_LIMIT {
break
}
let layout = root_grid_gen_layout(sizes.length(), division)
let mut valid = true
for i, segment in layout {
let starting = i == 0
if starting {
let cached = starting_cache.get(segment.length())
match cached {
Some(ok) =>
if !ok {
valid = false
break
}
None => {
let segment_size = root_grid_segment_size(segment, sizes, gap)
let mut ok = true
if segment.length() > 1 &&
segment_size > ok_threshold * target_size {
skip_count = skip_count + 1
ok = skip_count >= D2_GRID_SKIP_LIMIT
}
if ok && segment_size < target_size / ok_threshold {
skip_count = skip_count + 1
ok = skip_count >= D2_GRID_SKIP_LIMIT
}
starting_cache[segment.length()] = ok
if !ok {
valid = false
break
}
}
}
} else {
let segment_size = root_grid_segment_size(segment, sizes, gap)
if segment.length() > 1 && segment_size > ok_threshold * target_size {
skip_count = skip_count + 1
if skip_count < D2_GRID_SKIP_LIMIT {
valid = false
break
}
}
if segment_size < target_size / ok_threshold {
skip_count = skip_count + 1
if skip_count < D2_GRID_SKIP_LIMIT {
valid = false
break
}
}
}
}
if !valid {
continue
}
let dist = root_grid_layout_distance(layout, target_size, sizes, gap)
if dist < best_dist {
best_layout = layout
best_dist = dist
has_best = true
fast_is_best = false
} else if fast_is_best && dist == best_dist {
best_layout = layout
fast_is_best = false
}
count = count + 1
}
ok_threshold = ok_threshold + D2_GRID_THRESHOLD_STEP_SIZE
if skip_count == 0 {
break
}
if count == 0 && threshold_attempts < D2_GRID_MAX_THRESHOLD_ATTEMPTS {
threshold_attempts = threshold_attempts + 1
}
attempt = attempt + 1
}
if has_best {
best_layout
} else {
root_grid_gen_layout(sizes.length(), [])
}
}
///|
pub fn estimate_root_grid_child_boxes(
parent : @graph.ObjectInput,
child_ids : Array[String],
child_size_by_id : Map[String, (Double, Double)],
) -> Map[String, @graph.Box]? {
if child_ids.is_empty() {
return None
}
let n = child_ids.length()
let mut rows = match parent.grid_rows {
Some(v) => if v > 0 { v } else { 0 }
None => 0
}
let mut columns = match parent.grid_columns {
Some(v) => if v > 0 { v } else { 0 }
None => 0
}
if rows == 0 && columns == 0 {
return None
}
let mut row_directed = false
if rows != 0 && columns != 0 {
row_directed = match parent.grid_row_directed {
Some(value) => value
None => true
}
let mut capacity = rows * columns
while capacity < n {
if row_directed {
rows = rows + 1
capacity = capacity + columns
} else {
columns = columns + 1
capacity = capacity + rows
}
}
} else if columns == 0 {
row_directed = true
if n < rows {
rows = n
}
} else if n < columns {
columns = n
}
let (horizontal_gap, vertical_gap) = root_grid_gaps(parent)
let widths : Array[Double] = []
let heights : Array[Double] = []
for id in child_ids {
match child_size_by_id.get(id) {
Some((w, h)) => {
widths.push(w)
heights.push(h)
}
None => {
widths.push(0.0)
heights.push(0.0)
}
}
}
let final_widths = widths.copy()
let final_heights = heights.copy()
let final_x : Array[Double] = []
let final_y : Array[Double] = []
for _ in 0.. {
if row_directed {
let index = row_index * columns + column_index
if index < n {
Some(index)
} else {
None
}
} else {
let index = column_index * rows + row_index
if index < n {
Some(index)
} else {
None
}
}
}
for i in 0..
if final_heights[index] > row_height {
row_height = final_heights[index]
}
None => break
}
}
row_heights[i] = row_height
}
for j in 0..
if final_widths[index] > column_width {
column_width = final_widths[index]
}
None => break
}
}
col_widths[j] = column_width
}
if row_directed {
let mut cursor_y = 0.0
for i in 0.. {
final_widths[index] = col_widths[j]
final_heights[index] = row_heights[i]
final_x[index] = cursor_x
final_y[index] = cursor_y
cursor_x = cursor_x + col_widths[j] + horizontal_gap
}
None => break
}
}
cursor_y = cursor_y + row_heights[i] + vertical_gap
}
} else {
let mut cursor_x = 0.0
for j in 0.. {
final_widths[index] = col_widths[j]
final_heights[index] = row_heights[i]
final_x[index] = cursor_x
final_y[index] = cursor_y
cursor_y = cursor_y + row_heights[i] + vertical_gap
}
None => break
}
}
cursor_x = cursor_x + col_widths[j] + horizontal_gap
}
}
for w in col_widths {
total_width = total_width + w + horizontal_gap
}
for h in row_heights {
total_height = total_height + h + vertical_gap
}
total_width = total_width - horizontal_gap
total_height = total_height - vertical_gap
} else if row_directed {
let mut sum_width = 0.0
for w in final_widths {
sum_width = sum_width + w
}
let target_width = (sum_width + horizontal_gap * (n - rows).to_double()) /
rows.to_double()
let layout = root_grid_best_layout(
final_widths,
target_width,
rows - 1,
horizontal_gap,
)
let row_widths : Array[Double] = []
let mut max_x = 0.0
for row in layout {
let row_width = root_grid_segment_size(row, final_widths, horizontal_gap)
row_widths.push(row_width)
if row_width > max_x {
max_x = row_width
}
}
for i, row in layout {
let row_width = row_widths[i]
if row_width == max_x {
continue
}
let delta = max_x - row_width
let mut widest = 0.0
for index in row {
if final_widths[index] > widest {
widest = final_widths[index]
}
}
let diffs : Array[Double] = []
let mut total_diff = 0.0
for index in row {
let diff = widest - final_widths[index]
diffs.push(diff)
total_diff = total_diff + diff
}
if total_diff > 0.0 {
let growth = if delta < total_diff { delta } else { total_diff }
for j, index in row {
final_widths[index] = final_widths[index] +
diffs[j] / total_diff * growth
}
}
if delta > total_diff {
let growth = (delta - total_diff) / row.length().to_double()
for index in row {
final_widths[index] = final_widths[index] + growth
}
}
}
let mut cursor_y = 0.0
for row in layout {
let mut row_height = 0.0
let mut cursor_x = 0.0
for index in row {
final_x[index] = cursor_x
final_y[index] = cursor_y
cursor_x = cursor_x + final_widths[index] + horizontal_gap
if final_heights[index] > row_height {
row_height = final_heights[index]
}
}
for index in row {
final_heights[index] = row_height
}
cursor_y = cursor_y + row_height + vertical_gap
}
total_width = max_x
total_height = cursor_y - vertical_gap
} else {
let mut sum_height = 0.0
for h in final_heights {
sum_height = sum_height + h
}
let target_height = (sum_height + vertical_gap * (n - columns).to_double()) /
columns.to_double()
let layout = root_grid_best_layout(
final_heights,
target_height,
columns - 1,
vertical_gap,
)
let column_heights : Array[Double] = []
let mut max_y = 0.0
for column in layout {
let column_height = root_grid_segment_size(
column, final_heights, vertical_gap,
)
column_heights.push(column_height)
if column_height > max_y {
max_y = column_height
}
}
for i, column in layout {
let column_height = column_heights[i]
if column_height == max_y {
continue
}
let delta = max_y - column_height
let mut tallest = 0.0
for index in column {
if final_heights[index] > tallest {
tallest = final_heights[index]
}
}
let diffs : Array[Double] = []
let mut total_diff = 0.0
for index in column {
let diff = tallest - final_heights[index]
diffs.push(diff)
total_diff = total_diff + diff
}
if total_diff > 0.0 {
let growth = if delta < total_diff { delta } else { total_diff }
for j, index in column {
final_heights[index] = final_heights[index] +
diffs[j] / total_diff * growth
}
}
if delta > total_diff {
let growth = (delta - total_diff) / column.length().to_double()
for index in column {
final_heights[index] = final_heights[index] + growth
}
}
}
let mut cursor_x = 0.0
for column in layout {
let mut column_width = 0.0
let mut cursor_y = 0.0
for index in column {
final_x[index] = cursor_x
final_y[index] = cursor_y
cursor_y = cursor_y + final_heights[index] + vertical_gap
if final_widths[index] > column_width {
column_width = final_widths[index]
}
}
for index in column {
final_widths[index] = column_width
}
cursor_x = cursor_x + column_width + horizontal_gap
}
total_width = cursor_x - horizontal_gap
total_height = max_y
}
let child_boxes : Map[String, @graph.Box] = Map([])
for i, id in child_ids {
child_boxes[id] = @graph.Box::new(
final_x[i],
final_y[i],
final_widths[i],
final_heights[i],
)
}
ignore(total_width)
ignore(total_height)
Some(child_boxes)
}
///|
fn restore_grid_semantics(
original_graph : @graph.GraphInput,
laid_out_graph : @graph.GraphInput,
context : GridSemanticContext,
config : LayoutConfig,
) -> @graph.GraphInput {
let original_by_id : Map[String, @graph.ObjectInput] = Map([])
original_by_id[original_graph.root.abs_id_syntax] = original_graph.root
for obj in original_graph.objects {
original_by_id[obj.abs_id_syntax] = obj
}
let laid_out_by_id : Map[String, @graph.ObjectInput] = Map([])
laid_out_by_id[laid_out_graph.root.abs_id_syntax] = laid_out_graph.root
for obj in laid_out_graph.objects {
laid_out_by_id[obj.abs_id_syntax] = obj
}
let override_boxes : Map[String, @graph.Box] = Map([])
for parent_id, child_ids in context.groups {
if parent_id == original_graph.root.abs_id_syntax {
let parent = original_graph.root
let child_metrics_by_id : Map[String, GridChildLayoutMetrics] = Map([])
let child_size_by_id : Map[String, (Double, Double)] = Map([])
for child_id in child_ids {
let child = match laid_out_by_id.get(child_id) {
Some(v) => v
None =>
match original_by_id.get(child_id) {
Some(v) => v
None => continue
}
}
let metrics = grid_child_layout_metrics_like_d2(child, config)
child_metrics_by_id[child_id] = metrics
child_size_by_id[child_id] = (
metrics.layout_width,
metrics.layout_height,
)
}
if root_uses_container_grid_semantics(parent) {
match
estimate_grid_container_layout_like_d2(
parent, child_ids, child_size_by_id,
) {
Some(layout) => {
override_boxes[parent.abs_id_syntax] = @graph.Box::new(
0.0,
0.0,
layout.width,
layout.height,
)
for child_id in child_ids {
let content_box = match layout.child_boxes.get(child_id) {
Some(value) => value
None => continue
}
let child = match original_by_id.get(child_id) {
Some(value) =>
match laid_out_by_id.get(child_id) {
Some(laid_out_value) => laid_out_value
None => value
}
None => {
override_boxes[child_id] = content_box
continue
}
}
let metrics = match child_metrics_by_id.get(child_id) {
Some(value) => value
None =>
{
layout_width: content_box.width,
layout_height: content_box.height,
label_position: None,
icon_position: None,
initial_margin: {
left: 0.0,
top: 0.0,
right: 0.0,
bottom: 0.0,
},
}
}
override_boxes[child_id] = grid_child_restored_box_like_d2(
child,
metrics,
content_box.x,
content_box.y,
content_box.width,
content_box.height,
)
}
}
None => ()
}
} else {
match
estimate_root_grid_child_boxes(parent, child_ids, child_size_by_id) {
Some(child_boxes) =>
for child_id, content_box in child_boxes {
let metrics = match child_metrics_by_id.get(child_id) {
Some(value) => value
None => continue
}
let child = match laid_out_by_id.get(child_id) {
Some(value) => value
None =>
match original_by_id.get(child_id) {
Some(value) => value
None => continue
}
}
override_boxes[child_id] = grid_child_restored_box_like_d2(
child,
metrics,
content_box.x,
content_box.y,
content_box.width,
content_box.height,
)
}
None => ()
}
}
continue
}
let parent = match laid_out_by_id.get(parent_id) {
Some(v) => v
None => continue
}
let parent_box = match parent.box {
Some(v) => v
None => continue
}
let child_metrics_by_id : Map[String, GridChildLayoutMetrics] = Map([])
let child_size_by_id : Map[String, (Double, Double)] = Map([])
for child_id in child_ids {
let child = match laid_out_by_id.get(child_id) {
Some(v) => v
None =>
match original_by_id.get(child_id) {
Some(v) => v
None => continue
}
}
let metrics = grid_child_layout_metrics_like_d2(child, config)
child_metrics_by_id[child_id] = metrics
child_size_by_id[child_id] = (metrics.layout_width, metrics.layout_height)
}
let layout = match
estimate_grid_container_layout_like_d2(
parent, child_ids, child_size_by_id,
) {
Some(v) => v
None => continue
}
let parent_new = parent_box
override_boxes[parent_id] = parent_new
for child_id in child_ids {
let content_box = match layout.child_boxes.get(child_id) {
Some(value) => value
None => continue
}
let metrics = match child_metrics_by_id.get(child_id) {
Some(value) => value
None =>
{
layout_width: content_box.width,
layout_height: content_box.height,
label_position: None,
icon_position: None,
initial_margin: { left: 0.0, top: 0.0, right: 0.0, bottom: 0.0 },
}
}
let child = match laid_out_by_id.get(child_id) {
Some(value) => value
None =>
match original_by_id.get(child_id) {
Some(value) => value
None => {
override_boxes[child_id] = @graph.Box::new(
parent_new.x + content_box.x,
parent_new.y + content_box.y,
content_box.width,
content_box.height,
)
continue
}
}
}
override_boxes[child_id] = grid_child_restored_box_like_d2(
child,
metrics,
parent_new.x + content_box.x,
parent_new.y + content_box.y,
content_box.width,
content_box.height,
)
}
}
normalize_override_boxes_origin(override_boxes)
let objects : Array[@graph.ObjectInput] = []
for obj in original_graph.objects {
if context.child_ids.contains(obj.abs_id_syntax) {
let laid_out_obj = match laid_out_by_id.get(obj.abs_id_syntax) {
Some(v) => v
None => obj
}
let override_box = override_boxes.get(obj.abs_id_syntax)
let box = match override_box {
Some(v) => Some(v)
None => obj.box
}
let label_box = match override_box {
Some(override_value) =>
match laid_out_obj.label_box {
Some(current) => {
let (width, height) = object_label_dimensions_like_d2(obj, 0.0)
let (position, _) = grid_child_effective_positions_like_d2(obj)
Some(
resize_label_box_for_position_like_d2(
current, width, height, position,
),
)
}
None =>
if grid_child_has_label_like_d2(obj) {
let (width, height) = object_label_dimensions_like_d2(obj, 0.0)
let (position, _) = grid_child_effective_positions_like_d2(obj)
Some(
label_box_for_position_like_d2(
override_value, width, height, position,
),
)
} else {
None
}
}
None => obj.label_box
}
objects.push(
clone_object_with_parts_and_label(
laid_out_obj,
box,
label_box,
obj.child_ids,
),
)
continue
}
let laid_out_obj = match laid_out_by_id.get(obj.abs_id_syntax) {
Some(v) => v
None => obj
}
let override_box = override_boxes.get(obj.abs_id_syntax)
let box = match override_box {
Some(v) => Some(v)
None => laid_out_obj.box
}
let label_box = match override_box {
Some(_) =>
if context.groups.contains(obj.abs_id_syntax) {
None
} else {
laid_out_obj.label_box
}
None => laid_out_obj.label_box
}
let child_ids = if context.groups.contains(obj.abs_id_syntax) {
obj.child_ids
} else {
laid_out_obj.child_ids
}
objects.push(
clone_object_with_parts_and_label(laid_out_obj, box, label_box, child_ids),
)
}
let root_has_group = context.groups.contains(
original_graph.root.abs_id_syntax,
)
let root_child_ids = if root_has_group {
original_graph.root.child_ids
} else {
laid_out_graph.root.child_ids
}
let root_box = match override_boxes.get(original_graph.root.abs_id_syntax) {
Some(box) => Some(box)
None => laid_out_graph.root.box
}
let root_label_box = match
override_boxes.get(original_graph.root.abs_id_syntax) {
Some(_) => if root_has_group { None } else { laid_out_graph.root.label_box }
None => laid_out_graph.root.label_box
}
let root = clone_object_with_parts_and_label(
laid_out_graph.root,
root_box,
root_label_box,
root_child_ids,
)
clone_graph_with_parts(
laid_out_graph,
root,
objects,
laid_out_graph.edges,
laid_out_graph.layers,
laid_out_graph.scenarios,
laid_out_graph.steps,
)
}
///|
fn root_uses_container_grid_semantics(parent : @graph.ObjectInput) -> Bool {
!(parent.id == "root" &&
parent.abs_id_syntax == "root" &&
parent.references.is_empty())
}
///|
fn normalize_override_boxes_origin(
override_boxes : Map[String, @graph.Box],
) -> Unit {
if override_boxes.is_empty() {
return
}
let mut min_x = 1000000000.0
let mut min_y = 1000000000.0
for _, box in override_boxes {
if box.x < min_x {
min_x = box.x
}
if box.y < min_y {
min_y = box.y
}
}
let shift_x = if min_x < 0.0 { -min_x } else { 0.0 }
let shift_y = if min_y < 0.0 { -min_y } else { 0.0 }
if shift_x == 0.0 && shift_y == 0.0 {
return
}
let ids : Array[String] = []
for id, _ in override_boxes {
ids.push(id)
}
for id in ids {
if override_boxes.get(id) is Some(box) {
override_boxes[id] = @graph.Box::new(
box.x + shift_x,
box.y + shift_y,
box.width,
box.height,
)
}
}
}
///|
fn clone_object_with_parts(
obj : @graph.ObjectInput,
box : @graph.Box?,
child_ids : Array[String],
) -> @graph.ObjectInput {
clone_object_with_parts_and_label(obj, box, obj.label_box, child_ids)
}
///|
fn clone_object_with_parts_and_label(
obj : @graph.ObjectInput,
box : @graph.Box?,
label_box : @graph.Box?,
child_ids : Array[String],
) -> @graph.ObjectInput {
@graph.ObjectInput::from_parts(
obj.id,
obj.label,
obj.shape_type,
obj.style,
box,
label_box,
child_ids,
obj.z_index,
obj.icon,
obj.tooltip,
obj.link,
obj.classes,
obj.grid_rows,
obj.grid_columns,
obj.grid_gap,
obj.horizontal_gap,
obj.vertical_gap,
obj.grid_column_span,
obj.grid_row_span,
obj.near,
obj.top,
obj.left,
direction=obj.direction,
language=obj.language,
sql_constraints=obj.sql_constraints,
id_val=obj.id_val,
id_syntax=obj.id_syntax,
abs_id_syntax=obj.abs_id_syntax,
references=obj.references,
icon_position=obj.icon_position,
tooltip_position=obj.tooltip_position,
label_position=obj.label_position,
grid_row_directed=obj.grid_row_directed,
)
}
///|
fn semantic_object_size(
obj : @graph.ObjectInput,
config : LayoutConfig,
inside_sequence? : Bool = false,
) -> (Double, Double) {
if obj.label == "" &&
obj.shape_type != Image &&
obj.shape_type != SqlTable &&
obj.shape_type != Class {
let (desired_width, desired_height) = match obj.box {
Some(box) => (box.width, box.height)
None => (0.0, 0.0)
}
if obj.shape_type == Circle || obj.shape_type == Square {
let side = if desired_width > 0.0 || desired_height > 0.0 {
if desired_width > desired_height {
desired_width
} else {
desired_height
}
} else {
100.0
}
return (side, side)
}
return (
if desired_width > 0.0 {
desired_width
} else {
100.0
},
if desired_height > 0.0 {
desired_height
} else {
100.0
},
)
}
match obj.box {
Some(box) => {
let (auto_width, auto_height) = estimate_semantic_object_size(
obj,
config,
inside_sequence~,
)
let width = if box.width > 0.0 { box.width } else { auto_width }
let height = if box.height > 0.0 { box.height } else { auto_height }
let (width, height) = if obj.shape_type == Circle ||
obj.shape_type == Square {
let side = if width > height { width } else { height }
(side, side)
} else if box.width <= 0.0 || box.height <= 0.0 {
if obj.shape_type == Person {
limit_ar(width, height, D2_PERSON_AR_LIMIT)
} else if obj.shape_type == Oval {
limit_ar(width, height, D2_OVAL_AR_LIMIT)
} else {
(width, height)
}
} else {
(width, height)
}
(
positive_or(width, config.default_width),
positive_or(height, config.default_height),
)
}
None => estimate_semantic_object_size(obj, config, inside_sequence~)
}
}
///|
fn estimate_semantic_object_size(
obj : @graph.ObjectInput,
config : LayoutConfig,
inside_sequence? : Bool = false,
) -> (Double, Double) {
let has_explicit_width = match obj.box {
Some(box) => box.width > 0.0
None => false
}
let has_explicit_height = match obj.box {
Some(box) => box.height > 0.0
None => false
}
let with_inner_label_padding = !(has_explicit_width || has_explicit_height)
let font_size = object_label_font_size_like_d2(obj, inside_sequence)
let is_bold = object_label_bold_like_d2(obj, inside_sequence)
let is_italic = style_bool(obj.style.italic, false)
let is_mono = obj.shape_type == Code || style_is_mono(obj.style.font)
let label_dims = if obj.shape_type == Code {
@text_metrics.measure_label_code(obj.label, font_size, is_bold, is_italic)
} else if obj.language == Some("markdown") {
@text_metrics.measure_label_markdown(obj.label, font_size)
} else {
@text_metrics.measure_label(
obj.label,
font_size,
is_mono,
is_bold,
is_italic,
)
}
let mut content_width = label_dims.width
let mut content_height = label_dims.height
if obj.shape_type == Code {
content_width = content_width + font_size.to_double()
content_height = content_height + font_size.to_double()
// reference behavior: code shape size is measured text + 0.5em padding each side only.
return (content_width, content_height)
} else if with_inner_label_padding &&
obj.label != "" &&
obj.shape_type != Text {
content_width = content_width + D2_INNER_LABEL_PADDING
content_height = content_height + D2_INNER_LABEL_PADDING
}
if obj.shape_type == Text {
if content_width < D2_MIN_SHAPE_SIZE {
content_width = D2_MIN_SHAPE_SIZE
}
if content_height < D2_MIN_SHAPE_SIZE {
content_height = D2_MIN_SHAPE_SIZE
}
return (content_width, content_height)
}
if obj.shape_type == Image {
return (128.0, 128.0)
}
let base_padding = semantic_shape_default_padding(obj.shape_type)
let mut padding_x = base_padding.0
let mut padding_y = base_padding.1
if has_explicit_width {
padding_x = 0.0
}
if has_explicit_height {
padding_y = 0.0
}
if obj.icon is Some(_) &&
obj.shape_type != SqlTable &&
obj.shape_type != Class &&
obj.shape_type != Code &&
obj.shape_type != Text {
let label_height = label_dims.height + D2_INNER_LABEL_PADDING
if !has_explicit_width {
padding_x = padding_x + label_height
}
if !has_explicit_height {
padding_y = padding_y + label_height
}
}
let fitted = semantic_shape_dimensions_to_fit(
obj.shape_type,
content_width,
content_height,
padding_x,
padding_y,
)
let mut width = fitted.0
let mut height = fitted.1
if obj.shape_type == Circle || obj.shape_type == Square {
let side = if width > height { width } else { height }
width = side
height = side
} else if obj.shape_type == Person {
let fit = limit_ar(width, height, D2_PERSON_AR_LIMIT)
width = fit.0
height = fit.1
} else if obj.shape_type == Oval {
let fit = limit_ar(width, height, D2_OVAL_AR_LIMIT)
width = fit.0
height = fit.1
} else if obj.shape_type == C4Person {
let fit = limit_ar(width, height, D2_C4_PERSON_AR_LIMIT)
width = fit.0
height = fit.1
}
(
if width > 0.0 {
width
} else {
positive_or(config.default_width, 1.0)
},
if height > 0.0 {
height
} else {
positive_or(config.default_height, 1.0)
},
)
}
///|
const D2_FONT_SIZE_M : Int = 16
///|
const D2_FONT_SIZE_L : Int = 20
///|
const D2_FONT_SIZE_XL : Int = 24
///|
const D2_FONT_SIZE_XXL : Int = 28
///|
fn container_level_label_size_like_d2(level : Int) -> Int {
if level <= 1 {
D2_FONT_SIZE_XXL
} else if level == 2 {
D2_FONT_SIZE_XL
} else if level == 3 {
D2_FONT_SIZE_L
} else {
D2_FONT_SIZE_M
}
}
///|
fn object_container_level_like_d2(obj : @graph.ObjectInput) -> Int {
let depth = @graph.syntax_path_depth(obj.abs_id_syntax)
if depth <= 0 {
1
} else {
depth
}
}
///|
fn object_label_font_size_like_d2(
obj : @graph.ObjectInput,
inside_sequence : Bool,
) -> Int {
let mut font_size = D2_FONT_SIZE_M
if obj.shape_type == Class || obj.shape_type == SqlTable {
font_size = D2_FONT_SIZE_L
}
if !inside_sequence &&
(
!obj.child_ids.is_empty() ||
obj.grid_rows is Some(_) ||
obj.grid_columns is Some(_)
) &&
obj.shape_type != Text {
font_size = container_level_label_size_like_d2(
object_container_level_like_d2(obj),
)
}
match obj.style.font_size {
Some(v) => v.as_int().unwrap_or(font_size)
None => font_size
}
}
///|
fn object_label_bold_like_d2(
obj : @graph.ObjectInput,
inside_sequence : Bool,
) -> Bool {
if inside_sequence || obj.shape_type == Class {
false
} else {
style_bool(obj.style.bold, false) ||
(
obj.child_ids.is_empty() &&
!obj.classes.contains("__diago_nested_layout_placeholder") &&
obj.shape_type != Text
)
}
}
///|
const D2_DEFAULT_PADDING : Double = 40.0
///|
const D2_DEFAULT_ARC_DEPTH : Double = 24.0
///|
const D2_PARALLELOGRAM_WEDGE_WIDTH : Double = 26.0
///|
const D2_STORED_DATA_WEDGE_WIDTH : Double = 15.0
///|
const D2_STEP_WEDGE_WIDTH : Double = 35.0
///|
const D2_PACKAGE_TOP_MAX_HEIGHT : Double = 55.0
///|
const D2_PACKAGE_VERTICAL_SCALAR : Double = 0.2
///|
const D2_CALLOUT_DEFAULT_TIP_HEIGHT : Double = 45.0
///|
const D2_PAGE_CORNER_WIDTH : Double = 20.8164
///|
const D2_PAGE_CORNER_HEIGHT : Double = 20.348
///|
const D2_DOC_PATH_HEIGHT : Double = 18.925
///|
const D2_DOC_PATH_INNER_BOTTOM : Double = 14.0
///|
const D2_LABEL_PADDING : Double = 5.0
///|
const D2_INNER_LABEL_PADDING : Double = 5.0
///|
const D2_MIN_SHAPE_SIZE : Double = 5.0
///|
const D2_MAX_ICON_SIZE : Double = 64.0
///|
const D2_PERSON_AR_LIMIT : Double = 1.5
///|
const D2_OVAL_AR_LIMIT : Double = 3.0
///|
const D2_C4_PERSON_AR_LIMIT : Double = 1.5
///|
const D2_C4_HEAD_RADIUS_FACTOR : Double = 0.22
///|
const D2_C4_BODY_TOP_FACTOR : Double = 0.8
///|
const CLOUD_WIDE_INNER_WIDTH : Double = 0.819
///|
const CLOUD_WIDE_INNER_HEIGHT : Double = 0.548
///|
const CLOUD_WIDE_ASPECT_BOUNDARY : Double = (1.0 + D2_DEFAULT_PADDING) /
D2_DEFAULT_PADDING
///|
const CLOUD_TALL_INNER_WIDTH : Double = 0.549
///|
const CLOUD_TALL_INNER_HEIGHT : Double = 0.820
///|
const CLOUD_TALL_ASPECT_BOUNDARY : Double = D2_DEFAULT_PADDING /
(1.0 + D2_DEFAULT_PADDING)
///|
const CLOUD_SQUARE_INNER_WIDTH : Double = 0.663
///|
const CLOUD_SQUARE_INNER_HEIGHT : Double = 0.663
///|
fn semantic_shape_default_padding(
shape_type : @graph.ShapeType,
) -> (Double, Double) {
match shape_type {
Circle => {
let p = D2_DEFAULT_PADDING / 2.0.sqrt()
(p, p)
}
Diamond => (D2_DEFAULT_PADDING / 4.0, D2_DEFAULT_PADDING / 2.0)
Hexagon => (D2_DEFAULT_PADDING / 2.0, D2_DEFAULT_PADDING / 2.0)
Cylinder => (D2_DEFAULT_PADDING, D2_DEFAULT_PADDING / 2.0)
Queue => (D2_DEFAULT_PADDING / 2.0, D2_DEFAULT_PADDING)
StoredData => (D2_DEFAULT_PADDING - 10.0, D2_DEFAULT_PADDING)
Person => (10.0, D2_DEFAULT_PADDING)
C4Person => (10.0, D2_DEFAULT_PADDING)
Page => (D2_DEFAULT_PADDING, D2_PAGE_CORNER_HEIGHT + D2_DEFAULT_PADDING)
Document =>
(
D2_DEFAULT_PADDING,
D2_DEFAULT_PADDING * D2_DOC_PATH_INNER_BOTTOM / D2_DOC_PATH_HEIGHT,
)
Package => (D2_DEFAULT_PADDING, 0.8 * D2_DEFAULT_PADDING)
Step => (D2_DEFAULT_PADDING / 4.0, D2_DEFAULT_PADDING + D2_STEP_WEDGE_WIDTH)
Callout => (D2_DEFAULT_PADDING, D2_DEFAULT_PADDING / 2.0)
Cloud => (D2_DEFAULT_PADDING, D2_DEFAULT_PADDING / 2.0)
_ => (D2_DEFAULT_PADDING, D2_DEFAULT_PADDING)
}
}
///|
fn semantic_shape_dimensions_to_fit(
shape_type : @graph.ShapeType,
width : Double,
height : Double,
padding_x : Double,
padding_y : Double,
) -> (Double, Double) {
match shape_type {
Circle => {
let length = if width + padding_x > height + padding_y {
width + padding_x
} else {
height + padding_y
}
let diameter = (2.0.sqrt() * length).ceil()
(diameter, diameter)
}
Oval => {
let theta = @math.atan2(height, width)
let padded_width = width + padding_x * @math.cos(theta)
let padded_height = height + padding_y * @math.sin(theta)
let mut total_width = (2.0.sqrt() * padded_width).ceil()
let mut total_height = (2.0.sqrt() * padded_height).ceil()
let fit = limit_ar(total_width, total_height, D2_OVAL_AR_LIMIT)
total_width = fit.0
total_height = fit.1
(total_width, total_height)
}
Diamond =>
((2.0 * (width + padding_x)).ceil(), (2.0 * (height + padding_y)).ceil())
Hexagon =>
((1.5 * (width + padding_x)).ceil(), (1.5 * (height + padding_y)).ceil())
Parallelogram =>
(
(width + padding_x + D2_PARALLELOGRAM_WEDGE_WIDTH * 2.0).ceil(),
(height + padding_y).ceil(),
)
Cylinder =>
(
(width + padding_x).ceil(),
(height + padding_y + 3.0 * D2_DEFAULT_ARC_DEPTH).ceil(),
)
Queue =>
(
(width + padding_x + 3.0 * D2_DEFAULT_ARC_DEPTH).ceil(),
(height + padding_y).ceil(),
)
StoredData =>
(
(width + padding_x + 2.0 * D2_STORED_DATA_WEDGE_WIDTH).ceil(),
(height + padding_y).ceil(),
)
Person => {
let mut total_width = width + padding_x
let mut total_height = height + padding_y
let fit = limit_ar(total_width, total_height, D2_PERSON_AR_LIMIT)
total_width = fit.0
total_height = fit.1
(total_width.ceil(), total_height.ceil())
}
C4Person => {
let content_width = width + padding_x
let content_height = height + padding_y
let mut total_width = content_width / 0.9
let head_radius = total_width * D2_C4_HEAD_RADIUS_FACTOR
let body_top = head_radius + head_radius * D2_C4_BODY_TOP_FACTOR
let vertical_padding = total_width * 0.06
let mut total_height = content_height + body_top + vertical_padding
let min_height = total_width * 0.95
if total_height < min_height {
total_height = min_height
}
let fit = limit_ar(total_width, total_height, D2_C4_PERSON_AR_LIMIT)
total_width = fit.0
total_height = fit.1
(total_width.ceil(), total_height.ceil())
}
Cloud => {
let total_width = width + padding_x
let total_height = height + padding_y
let aspect_ratio = total_width / total_height
if aspect_ratio > CLOUD_WIDE_ASPECT_BOUNDARY {
(
(total_width / CLOUD_WIDE_INNER_WIDTH).ceil(),
(total_height / CLOUD_WIDE_INNER_HEIGHT).ceil(),
)
} else if aspect_ratio < CLOUD_TALL_ASPECT_BOUNDARY {
(
(total_width / CLOUD_TALL_INNER_WIDTH).ceil(),
(total_height / CLOUD_TALL_INNER_HEIGHT).ceil(),
)
} else {
(
(total_width / CLOUD_SQUARE_INNER_WIDTH).ceil(),
(total_height / CLOUD_SQUARE_INNER_HEIGHT).ceil(),
)
}
}
Page => {
let mut total_width = width + padding_x
let mut total_height = height + padding_y
if total_height < 3.0 * D2_PAGE_CORNER_HEIGHT {
total_width = total_width + D2_PAGE_CORNER_WIDTH
}
if total_width < 2.0 * D2_PAGE_CORNER_WIDTH {
total_width = 2.0 * D2_PAGE_CORNER_WIDTH
}
if total_height < D2_PAGE_CORNER_HEIGHT {
total_height = D2_PAGE_CORNER_HEIGHT
}
(total_width.ceil(), total_height.ceil())
}
Document =>
(
(width + padding_x).ceil(),
((height + padding_y) * D2_DOC_PATH_HEIGHT / D2_DOC_PATH_INNER_BOTTOM).ceil(),
)
Package => {
let inner_height = height + padding_y
let top_height = inner_height *
D2_PACKAGE_VERTICAL_SCALAR /
(1.0 - D2_PACKAGE_VERTICAL_SCALAR)
let total_height = inner_height +
(if top_height < D2_PACKAGE_TOP_MAX_HEIGHT {
top_height
} else {
D2_PACKAGE_TOP_MAX_HEIGHT
})
((width + padding_x).ceil(), total_height.ceil())
}
Step =>
(
(width + padding_x + 2.0 * D2_STEP_WEDGE_WIDTH).ceil(),
(height + padding_y).ceil(),
)
Callout => {
let mut total_height = height + padding_y
if total_height < D2_CALLOUT_DEFAULT_TIP_HEIGHT {
total_height = total_height * 2.0
} else {
total_height = total_height + D2_CALLOUT_DEFAULT_TIP_HEIGHT
}
((width + padding_x).ceil(), total_height.ceil())
}
_ => ((width + padding_x).ceil(), (height + padding_y).ceil())
}
}
///|
fn limit_ar(
width : Double,
height : Double,
aspect_ratio : Double,
) -> (Double, Double) {
let mut w = width
let mut h = height
if w <= 0.0 && h <= 0.0 {
return (1.0, 1.0)
}
if w <= 0.0 {
w = h / aspect_ratio
}
if h <= 0.0 {
h = w / aspect_ratio
}
if h > 0.0 && w / h > aspect_ratio {
h = w / aspect_ratio
} else if w > 0.0 && h / w > aspect_ratio {
w = h / aspect_ratio
}
(w, h)
}
///|
fn style_bool(value : @graph.StyleValue?, default : Bool) -> Bool {
match value {
Some(v) => v.as_bool().unwrap_or(default)
None => default
}
}
///|
fn style_is_mono(value : @graph.StyleValue?) -> Bool {
match value {
Some(v) =>
match v.get_value().to_lower() {
"mono" | "monospace" | "code" => true
_ => false
}
None => false
}
}
///|
fn positive_or(value : Double, fallback : Double) -> Double {
if value > 0.0 {
value
} else {
fallback
}
}
///|
priv struct GraphOrderContext {
object_order : Map[String, Int]
edge_order : Map[Int, Int]
child_order_by_parent : Map[String, Map[String, Int]]
}
///|
fn save_graph_order_like_d2(graph : @graph.GraphInput) -> GraphOrderContext {
let object_order : Map[String, Int] = Map([])
for i, obj in graph.objects {
object_order[obj.abs_id_syntax] = i
}
let edge_order : Map[Int, Int] = Map([])
for i, edge in graph.edges {
edge_order[edge.index] = i
}
let child_order_by_parent : Map[String, Map[String, Int]] = Map([])
let root_child_order : Map[String, Int] = Map([])
for i, child_id in graph.root.child_ids {
root_child_order[child_id] = i
}
child_order_by_parent[graph.root.abs_id_syntax] = root_child_order
for obj in graph.objects {
let child_order : Map[String, Int] = Map([])
for i, child_id in obj.child_ids {
child_order[child_id] = i
}
child_order_by_parent[obj.abs_id_syntax] = child_order
}
{ object_order, edge_order, child_order_by_parent }
}
///|
fn restore_child_order_like_d2(
parent_id : String,
child_ids : Array[String],
context : GraphOrderContext,
) -> Array[String] {
match context.child_order_by_parent.get(parent_id) {
Some(order) => {
let out = child_ids.copy()
let current_order : Map[String, Int] = Map([])
for i, child_id in out {
current_order[child_id] = i
}
out.sort_by(fn(a, b) {
let a_has = order.contains(a)
let b_has = order.contains(b)
if a_has && b_has {
let cmp = order.get_or_default(a, 0) - order.get_or_default(b, 0)
if cmp != 0 {
return cmp
}
} else if a_has != b_has {
return if a_has { -1 } else { 1 }
}
current_order.get_or_default(a, 0) - current_order.get_or_default(b, 0)
})
out
}
None => child_ids
}
}
///|
fn restore_graph_order_like_d2(
graph : @graph.GraphInput,
laid_out_graph : @graph.GraphInput,
) -> @graph.GraphInput {
let context = save_graph_order_like_d2(graph)
let objects = laid_out_graph.objects.copy()
let current_object_order : Map[String, Int] = Map([])
for i, obj in objects {
current_object_order[obj.abs_id_syntax] = i
}
objects.sort_by(fn(a, b) {
let a_has = context.object_order.contains(a.abs_id_syntax)
let b_has = context.object_order.contains(b.abs_id_syntax)
if a_has && b_has {
let cmp = context.object_order.get_or_default(a.abs_id_syntax, 0) -
context.object_order.get_or_default(b.abs_id_syntax, 0)
if cmp != 0 {
return cmp
}
} else if a_has != b_has {
return if a_has { -1 } else { 1 }
}
current_object_order.get_or_default(a.abs_id_syntax, 0) -
current_object_order.get_or_default(b.abs_id_syntax, 0)
})
let restored_objects : Array[@graph.ObjectInput] = []
for obj in objects {
restored_objects.push(
clone_object_with_parts_and_label(
obj,
obj.box,
obj.label_box,
restore_child_order_like_d2(obj.abs_id_syntax, obj.child_ids, context),
),
)
}
let edges = laid_out_graph.edges.copy()
let current_edge_order : Map[Int, Int] = Map([])
for i, edge in edges {
current_edge_order[edge.index] = i
}
edges.sort_by(fn(a, b) {
let a_has = context.edge_order.contains(a.index)
let b_has = context.edge_order.contains(b.index)
if a_has && b_has {
let cmp = context.edge_order.get_or_default(a.index, 0) -
context.edge_order.get_or_default(b.index, 0)
if cmp != 0 {
return cmp
}
} else if a_has != b_has {
return if a_has { -1 } else { 1 }
}
current_edge_order.get_or_default(a.index, 0) -
current_edge_order.get_or_default(b.index, 0)
})
let root = clone_object_with_parts_and_label(
laid_out_graph.root,
laid_out_graph.root.box,
laid_out_graph.root.label_box,
restore_child_order_like_d2(
graph.root.abs_id_syntax,
laid_out_graph.root.child_ids,
context,
),
)
clone_graph_with_parts(
laid_out_graph,
root,
restored_objects,
edges,
laid_out_graph.layers,
laid_out_graph.scenarios,
laid_out_graph.steps,
)
}
///|
/// Perform layout on a graph (including variants) with a specific engine.
///
/// Returns a new graph with layout applied.
pub fn[Engine : LayoutEngine] layout_with_engine(
engine : Engine,
graph : @graph.GraphInput,
config : LayoutConfig,
direction : Direction,
) -> @graph.GraphInput raise LayoutError {
check_feature_support(engine.engine_name(), engine.features(), graph)
let effective_direction = graph_layout_direction(graph, direction)
let prepared = resolve_default_object_positions_for_engine(
prepare_graph_for_layout(graph),
engine.engine_name(),
)
let (nested_prepared, nested_sequence_ctx) = prepare_nested_sequence_layouts(
engine, prepared, config,
)
let (engine_graph, semantic_ctx) = prepare_layout_semantics(
nested_prepared, config,
)
let laid_out_core = if engine_graph.objects.is_empty() {
engine_graph
} else {
let problem = compile_layout_problem(engine_graph)
let canonical_graph = problem.graph()
let patch = engine.layout(canonical_graph, config, effective_direction)
validate_layout_patch(problem, patch, engine.engine_name())
@graph.apply_layout(engine_graph, patch)
}
let semantic_restored = restore_layout_semantics(
nested_prepared, laid_out_core, semantic_ctx, config,
)
let laid_out = restore_nested_sequence_layouts(
prepared, semantic_restored, nested_sequence_ctx,
)
let layers : Array[@graph.LayerInput] = []
for layer in laid_out.layers {
layers.push(
@graph.LayerInput::new(
layer.name,
layout_with_engine(engine, layer.graph, config, direction),
),
)
}
let scenarios : Array[@graph.ScenarioInput] = []
for scenario in laid_out.scenarios {
scenarios.push(
@graph.ScenarioInput::new(
scenario.name,
layout_with_engine(engine, scenario.graph, config, direction),
),
)
}
let steps : Array[@graph.StepInput] = []
for step in laid_out.steps {
steps.push(
@graph.StepInput::new(
step.name,
layout_with_engine(engine, step.graph, config, direction),
),
)
}
restore_graph_order_like_d2(
graph,
recompute_final_edge_label_boxes_like_d2(
normalize_routes(
clone_graph_with_parts(
laid_out,
laid_out.root,
laid_out.objects,
laid_out.edges,
layers,
scenarios,
steps,
),
),
),
)
}