// 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.
///|
/// Immutable graph input types for layout engines.
///
/// Engines must treat their inputs as read-only. To enforce this at the type
/// level, the engine-facing graph type (`GraphInput`) and all nested types must
/// not contain `mut` fields.
///|
pub struct GraphInput {
name : String
root : ObjectInput
objects : Array[ObjectInput]
edges : Array[EdgeInput]
sequence_fragments : Array[SequenceFragmentInput]
activation_boxes : Array[(String, Box)]
sequence_notes : Array[(String, String, Box)]
sequence_fragments_layout : Array[SequenceFragmentLayout]
layers : Array[LayerInput]
scenarios : Array[ScenarioInput]
steps : Array[StepInput]
legend : LegendInput?
} derive(Show)
///|
pub fn GraphInput::new(name : String) -> GraphInput {
let root = ObjectInput::new("root")
{
name,
root,
objects: [],
edges: [],
sequence_fragments: [],
activation_boxes: [],
sequence_notes: [],
sequence_fragments_layout: [],
layers: [],
scenarios: [],
steps: [],
legend: None,
}
}
///|
pub fn GraphInput::from_parts(
name : String,
root : ObjectInput,
objects : Array[ObjectInput],
edges : Array[EdgeInput],
sequence_fragments : Array[SequenceFragmentInput],
activation_boxes : Array[(String, Box)],
sequence_notes : Array[(String, String, Box)],
sequence_fragments_layout : Array[SequenceFragmentLayout],
layers : Array[LayerInput],
scenarios : Array[ScenarioInput],
steps : Array[StepInput],
legend : LegendInput?,
) -> GraphInput {
{
name,
root,
objects,
edges,
sequence_fragments,
activation_boxes,
sequence_notes,
sequence_fragments_layout,
layers,
scenarios,
steps,
legend,
}
}
///|
pub fn GraphInput::find_object(self : GraphInput, id : String) -> ObjectInput? {
if self.root.id == id {
return Some(self.root)
}
for obj in self.objects {
if obj.id == id {
return Some(obj)
}
}
None
}
///|
pub fn GraphInput::children_of(
self : GraphInput,
obj : ObjectInput,
) -> Array[ObjectInput] {
let children : Array[ObjectInput] = []
for child_id in obj.child_ids {
match self.find_object(child_id) {
Some(child) => children.push(child)
None => ()
}
}
children
}
///|
pub fn GraphInput::get_objects(self : GraphInput) -> Array[ObjectInput] {
self.objects
}
///|
pub fn GraphInput::get_edges(self : GraphInput) -> Array[EdgeInput] {
self.edges
}
///|
pub struct LayerInput {
name : String
graph : GraphInput
} derive(Show)
///|
pub fn LayerInput::new(name : String, graph : GraphInput) -> LayerInput {
{ name, graph }
}
///|
pub struct ScenarioInput {
name : String
graph : GraphInput
} derive(Show)
///|
pub fn ScenarioInput::new(name : String, graph : GraphInput) -> ScenarioInput {
{ name, graph }
}
///|
pub struct StepInput {
name : String
graph : GraphInput
} derive(Show)
///|
pub fn StepInput::new(name : String, graph : GraphInput) -> StepInput {
{ name, graph }
}
///|
pub struct SequenceFragmentInput {
fragment_type : String
condition : String?
start_index : Int
end_index : Int
operands : Array[FragmentOperandInput]
} derive(Show)
///|
pub fn SequenceFragmentInput::new(
fragment_type : String,
condition : String?,
start_index : Int,
end_index : Int,
operands : Array[FragmentOperandInput],
) -> SequenceFragmentInput {
{ fragment_type, condition, start_index, end_index, operands }
}
///|
pub struct FragmentOperandInput {
condition : String?
} derive(Show)
///|
pub fn FragmentOperandInput::new(condition : String?) -> FragmentOperandInput {
{ condition, }
}
///|
/// Immutable representation of a graph object for layout engines.
///
/// Fields:
///
/// * `id` : Unique identifier for the object.
/// * `label` : Display text for the object.
/// * `shape_type` : Visual shape of the object.
/// * `style` : Visual styling properties.
/// * `box` : Optional precomputed bounding box.
/// * `label_box` : Optional precomputed label bounding box.
/// * `child_ids` : Array of child object identifiers.
/// * `z_index` : Layer ordering for visual stacking.
/// * `icon` : Optional icon identifier.
/// * `tooltip` : Optional tooltip text.
/// * `link` : Optional hyperlink URL.
/// * `classes` : Array of CSS class names for styling.
/// * `grid_rows` : Optional number of grid rows for layout.
/// * `grid_columns` : Optional number of grid columns for layout.
/// * `grid_gap` : Optional spacing between grid items.
/// * `horizontal_gap` : Optional horizontal spacing between children.
/// * `vertical_gap` : Optional vertical spacing between children.
/// * `grid_column_span` : Optional number of columns this object spans.
/// * `grid_row_span` : Optional number of rows this object spans.
/// * `near` : Optional identifier of object to position near.
/// * `top` : Optional fixed top coordinate in pixels.
/// * `left` : Optional fixed left coordinate in pixels.
/// * `direction` : Optional layout direction override (`up|down|left|right`).
///
pub struct ObjectInput {
id : String
label : String
shape_type : ShapeType
style : StyleInput
box : Box?
label_box : Box?
child_ids : Array[String]
z_index : Int
icon : String?
tooltip : String?
link : String?
classes : Array[String]
grid_rows : Int?
grid_columns : Int?
grid_gap : Double?
horizontal_gap : Double?
vertical_gap : Double?
grid_column_span : Int?
grid_row_span : Int?
near : String?
top : Int?
left : Int?
direction : String?
} derive(Show)
///|
pub fn ObjectInput::new(id : String) -> ObjectInput {
{
id,
label: id,
shape_type: Rectangle,
style: StyleInput::new(),
box: None,
label_box: None,
child_ids: [],
z_index: 0,
icon: None,
tooltip: None,
link: None,
classes: [],
grid_rows: None,
grid_columns: None,
grid_gap: None,
horizontal_gap: None,
vertical_gap: None,
grid_column_span: None,
grid_row_span: None,
near: None,
top: None,
left: None,
direction: None,
}
}
///|
pub fn ObjectInput::from_parts(
id : String,
label : String,
shape_type : ShapeType,
style : StyleInput,
box : Box?,
label_box : Box?,
child_ids : Array[String],
z_index : Int,
icon : String?,
tooltip : String?,
link : String?,
classes : Array[String],
grid_rows : Int?,
grid_columns : Int?,
grid_gap : Double?,
horizontal_gap : Double?,
vertical_gap : Double?,
grid_column_span : Int?,
grid_row_span : Int?,
near : String?,
top : Int?,
left : Int?,
direction? : String? = None,
) -> ObjectInput {
{
id,
label,
shape_type,
style,
box,
label_box,
child_ids,
z_index,
icon,
tooltip,
link,
classes,
grid_rows,
grid_columns,
grid_gap,
horizontal_gap,
vertical_gap,
grid_column_span,
grid_row_span,
near,
top,
left,
direction,
}
}
///|
pub fn ObjectInput::with_direction(
self : ObjectInput,
direction : String?,
) -> ObjectInput {
{ ..self, direction, }
}
///|
pub fn ObjectInput::get_box(self : ObjectInput) -> Box? {
self.box
}
///|
pub fn ObjectInput::get_label_box(self : ObjectInput) -> Box? {
self.label_box
}
///|
pub struct EdgeInput {
index : Int
src_id : String
dst_id : String
src_arrow : Bool
dst_arrow : Bool
src_arrowhead : ArrowheadType
dst_arrowhead : ArrowheadType
src_arrowhead_label : String?
dst_arrowhead_label : String?
src_arrowhead_label_color : String?
dst_arrowhead_label_color : String?
src_anchor : String?
dst_anchor : String?
label : String
style : StyleInput
route : Array[Point]
bend_points : Array[Point]?
is_curve : Bool
z_index : Int
reference_count : Int
label_box : Box?
src_column_index : Int?
dst_column_index : Int?
} derive(Show)
///|
pub fn EdgeInput::new(
index : Int,
src_id : String,
dst_id : String,
src_arrow : Bool,
dst_arrow : Bool,
) -> EdgeInput {
{
index,
src_id,
dst_id,
src_arrow,
dst_arrow,
src_arrowhead: if src_arrow {
Triangle
} else {
None
},
dst_arrowhead: if dst_arrow {
Triangle
} else {
None
},
src_arrowhead_label: None,
dst_arrowhead_label: None,
src_arrowhead_label_color: None,
dst_arrowhead_label_color: None,
src_anchor: None,
dst_anchor: None,
label: "",
style: StyleInput::new(),
route: [],
bend_points: None,
is_curve: false,
z_index: 0,
reference_count: 1,
label_box: None,
src_column_index: None,
dst_column_index: None,
}
}
///|
pub fn EdgeInput::from_parts(
index : Int,
src_id : String,
dst_id : String,
src_arrow : Bool,
dst_arrow : Bool,
src_arrowhead : ArrowheadType,
dst_arrowhead : ArrowheadType,
src_arrowhead_label : String?,
dst_arrowhead_label : String?,
src_arrowhead_label_color : String?,
dst_arrowhead_label_color : String?,
src_anchor : String?,
dst_anchor : String?,
label : String,
style : StyleInput,
route : Array[Point],
bend_points : Array[Point]?,
is_curve : Bool,
z_index : Int,
reference_count : Int,
label_box : Box?,
src_column_index : Int?,
dst_column_index : Int?,
) -> EdgeInput {
{
index,
src_id,
dst_id,
src_arrow,
dst_arrow,
src_arrowhead,
dst_arrowhead,
src_arrowhead_label,
dst_arrowhead_label,
src_arrowhead_label_color,
dst_arrowhead_label_color,
src_anchor,
dst_anchor,
label,
style,
route,
bend_points,
is_curve,
z_index,
reference_count,
label_box,
src_column_index,
dst_column_index,
}
}
///|
pub fn EdgeInput::get_src_id(self : EdgeInput) -> String {
self.src_id
}
///|
pub fn EdgeInput::get_dst_id(self : EdgeInput) -> String {
self.dst_id
}
///|
pub fn EdgeInput::get_label_box(self : EdgeInput) -> Box? {
self.label_box
}
///|
/// A style value with its source range.
///
/// The value is preserved as a string. Consumers may parse it as needed.
pub struct StyleValue {
value : String
range : @lexer.Range
} derive(Eq, Show)
///|
pub fn StyleValue::new(value : String, range : @lexer.Range) -> StyleValue {
{ value, range }
}
///|
/// Create a style value without a concrete source location.
pub fn StyleValue::synthetic(value : String) -> StyleValue {
let p = @lexer.Position::zero()
{ value, range: @lexer.Range::new(p, p) }
}
///|
pub fn StyleValue::as_bool(self : StyleValue) -> Bool? {
match self.value.to_lower() {
"true" => Some(true)
"false" => Some(false)
_ => None
}
}
///|
pub fn StyleValue::as_int(self : StyleValue) -> Int? {
Some(@strconv.parse_int(self.value)) catch {
_ => None
}
}
///|
pub fn StyleValue::as_double(self : StyleValue) -> Double? {
Some(@strconv.parse_double(self.value)) catch {
_ => None
}
}
///|
pub fn StyleValue::get_value(self : StyleValue) -> String {
self.value
}
///|
pub fn StyleValue::get_range(self : StyleValue) -> @lexer.Range {
self.range
}
///|
pub fn StyleValue::from_string(value : String) -> StyleValue {
StyleValue::synthetic(value)
}
///|
pub fn StyleValue::from_bool(value : Bool) -> StyleValue {
let s = if value { "true" } else { "false" }
StyleValue::synthetic(s)
}
///|
pub fn StyleValue::from_int(value : Int) -> StyleValue {
StyleValue::synthetic("\{value}")
}
///|
pub fn StyleValue::from_double(value : Double) -> StyleValue {
StyleValue::synthetic("\{value}")
}
///|
pub fn StyleValue::from_string_opt(value : String?) -> StyleValue? {
match value {
Some(v) => Some(StyleValue::from_string(v))
None => None
}
}
///|
pub fn StyleValue::from_bool_opt(value : Bool?) -> StyleValue? {
match value {
Some(v) => Some(StyleValue::from_bool(v))
None => None
}
}
///|
pub fn StyleValue::from_int_opt(value : Int?) -> StyleValue? {
match value {
Some(v) => Some(StyleValue::from_int(v))
None => None
}
}
///|
pub fn StyleValue::from_double_opt(value : Double?) -> StyleValue? {
match value {
Some(v) => Some(StyleValue::from_double(v))
None => None
}
}
///|
pub struct StyleInput {
opacity : StyleValue?
stroke : StyleValue?
fill : StyleValue?
stroke_width : StyleValue?
stroke_dash : StyleValue?
border_radius : StyleValue?
shadow : StyleValue?
three_d : StyleValue?
multiple : StyleValue?
font : StyleValue?
font_size : StyleValue?
font_color : StyleValue?
bold : StyleValue?
italic : StyleValue?
underline : StyleValue?
animated : StyleValue?
filled : StyleValue?
double_border : StyleValue?
text_transform : StyleValue?
font_family : StyleValue?
fill_pattern : StyleValue?
sketch : StyleValue?
curved : StyleValue?
} derive(Eq, Show)
///|
pub fn StyleInput::new() -> StyleInput {
{
opacity: None,
stroke: None,
fill: None,
stroke_width: None,
stroke_dash: None,
border_radius: None,
shadow: None,
three_d: None,
multiple: None,
font: None,
font_size: None,
font_color: None,
bold: None,
italic: None,
underline: None,
animated: None,
filled: None,
double_border: None,
text_transform: None,
font_family: None,
fill_pattern: None,
sketch: None,
curved: None,
}
}
///|
pub fn StyleInput::from_parts(
opacity : StyleValue?,
stroke : StyleValue?,
fill : StyleValue?,
stroke_width : StyleValue?,
stroke_dash : StyleValue?,
border_radius : StyleValue?,
shadow : StyleValue?,
three_d : StyleValue?,
multiple : StyleValue?,
font : StyleValue?,
font_size : StyleValue?,
font_color : StyleValue?,
bold : StyleValue?,
italic : StyleValue?,
underline : StyleValue?,
animated : StyleValue?,
filled : StyleValue?,
double_border : StyleValue?,
text_transform : StyleValue?,
font_family : StyleValue?,
fill_pattern : StyleValue?,
sketch : StyleValue?,
curved : StyleValue?,
) -> StyleInput {
{
opacity,
stroke,
fill,
stroke_width,
stroke_dash,
border_radius,
shadow,
three_d,
multiple,
font,
font_size,
font_color,
bold,
italic,
underline,
animated,
filled,
double_border,
text_transform,
font_family,
fill_pattern,
sketch,
curved,
}
}
///|
pub enum LegendEntryInput {
ShapeEntry(shape~ : ShapeType, label~ : String, style~ : StyleInput?)
ConnectionEntry(
src_arrow~ : ArrowheadType,
dst_arrow~ : ArrowheadType,
label~ : String,
style~ : StyleInput?
)
CustomEntry(icon~ : String, label~ : String)
Separator
} derive(Show)
///|
pub fn LegendEntryInput::shape(
shape : ShapeType,
label : String,
style : StyleInput?,
) -> LegendEntryInput {
ShapeEntry(shape~, label~, style~)
}
///|
pub fn LegendEntryInput::connection(
src_arrow : ArrowheadType,
dst_arrow : ArrowheadType,
label : String,
style : StyleInput?,
) -> LegendEntryInput {
ConnectionEntry(src_arrow~, dst_arrow~, label~, style~)
}
///|
pub fn LegendEntryInput::custom(
icon : String,
label : String,
) -> LegendEntryInput {
CustomEntry(icon~, label~)
}
///|
pub fn LegendEntryInput::separator() -> LegendEntryInput {
Separator
}
///|
pub struct LegendInput {
title : String?
position : LegendPosition
entries : Array[LegendEntryInput]
fill : String?
stroke : String?
padding : Double
entry_gap : Double
icon_size : Double
} derive(Show)
///|
pub fn LegendInput::from_parts(
title : String?,
position : LegendPosition,
entries : Array[LegendEntryInput],
fill : String?,
stroke : String?,
padding : Double,
entry_gap : Double,
icon_size : Double,
) -> LegendInput {
{ title, position, entries, fill, stroke, padding, entry_gap, icon_size }
}
///|
pub fn LegendInput::is_empty(self : LegendInput) -> Bool {
self.entries.is_empty()
}
///|
pub fn LegendInput::entry_count(self : LegendInput) -> Int {
let mut count = 0
for entry in self.entries {
match entry {
Separator => ()
_ => count += 1
}
}
count
}
///|
pub fn GraphInput::from_model(graph : GraphModel) -> GraphInput {
let objects : Array[ObjectInput] = []
for obj in graph.objects {
objects.push(ObjectInput::from_model(obj))
}
let edges : Array[EdgeInput] = []
for e in graph.edges {
edges.push(EdgeInput::from_model(e))
}
let fragments : Array[SequenceFragmentInput] = []
for f in graph.sequence_fragments {
fragments.push(SequenceFragmentInput::from_model(f))
}
let layers : Array[LayerInput] = []
for layer in graph.layers {
layers.push(
LayerInput::new(layer.name, GraphInput::from_model(layer.graph)),
)
}
let scenarios : Array[ScenarioInput] = []
for scenario in graph.scenarios {
scenarios.push(
ScenarioInput::new(scenario.name, GraphInput::from_model(scenario.graph)),
)
}
let steps : Array[StepInput] = []
for step in graph.steps {
steps.push(StepInput::new(step.name, GraphInput::from_model(step.graph)))
}
{
name: graph.name,
root: ObjectInput::from_model(graph.root),
objects,
edges,
sequence_fragments: fragments,
activation_boxes: [],
sequence_notes: [],
sequence_fragments_layout: [],
layers,
scenarios,
steps,
legend: match graph.legend {
Some(l) => Some(LegendInput::from_model(l))
None => None
},
}
}
///|
pub fn ObjectInput::from_model(obj : ObjectModel) -> ObjectInput {
let child_ids : Array[String] = []
for child in obj.children {
child_ids.push(child.id)
}
let classes : Array[String] = []
for c in obj.classes {
classes.push(c)
}
ObjectInput::from_parts(
obj.id,
obj.label,
obj.shape_type,
StyleInput::from_model(obj.style),
obj.box,
obj.label_box,
child_ids,
obj.z_index,
obj.icon,
obj.tooltip,
obj.link,
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,
)
}
///|
pub fn EdgeInput::from_model(edge : EdgeModel) -> EdgeInput {
let bend_points = match edge.bend_points {
Some(ps) => {
let copied : Array[Point] = []
for p in ps {
copied.push(p)
}
Some(copied)
}
None => None
}
EdgeInput::from_parts(
edge.index,
edge.src_id,
edge.dst_id,
edge.src_arrow,
edge.dst_arrow,
edge.src_arrowhead,
edge.dst_arrowhead,
edge.src_arrowhead_label,
edge.dst_arrowhead_label,
edge.src_arrowhead_label_color,
edge.dst_arrowhead_label_color,
edge.src_anchor,
edge.dst_anchor,
edge.label,
StyleInput::from_model(edge.style),
[],
bend_points,
edge.is_curve,
edge.z_index,
edge.reference_count,
edge.label_box,
edge.src_column_index,
edge.dst_column_index,
)
}
///|
pub fn StyleInput::from_model(style : StyleModel) -> StyleInput {
StyleInput::from_parts(
style.opacity,
style.stroke,
style.fill,
style.stroke_width,
style.stroke_dash,
style.border_radius,
style.shadow,
style.three_d,
style.multiple,
style.font,
style.font_size,
style.font_color,
style.bold,
style.italic,
style.underline,
style.animated,
style.filled,
style.double_border,
style.text_transform,
style.font_family,
style.fill_pattern,
style.sketch,
style.curved,
)
}
///|
pub fn SequenceFragmentInput::from_model(
fragment : SequenceFragmentModel,
) -> SequenceFragmentInput {
let operands : Array[FragmentOperandInput] = []
for op in fragment.operands {
operands.push({ condition: op.condition })
}
{
fragment_type: fragment.fragment_type,
condition: fragment.condition,
start_index: fragment.start_index,
end_index: fragment.end_index,
operands,
}
}
///|
pub fn LegendInput::from_model(legend : LegendModel) -> LegendInput {
let entries : Array[LegendEntryInput] = []
for entry in legend.entries {
entries.push(entry)
}
LegendInput::from_parts(
legend.title,
legend.position,
entries,
legend.fill,
legend.stroke,
legend.padding,
legend.entry_gap,
legend.icon_size,
)
}