Documentation
¶
Overview ¶
Package layout generates positioned graph layouts from IPM documents. Produces renderer-agnostic coordinate data for downstream renderers.
Spec: gl:docs/dev/layout-engine.md Architecture: gl:docs/dev/layout-engine.md#L23 SVG renderer: gl:pkg/ipmsvg/svg.go
Index ¶
- Constants
- Variables
- func CollapsedGapH(aType, bType NodeType) int
- func CollapsedGapV(aType, bType NodeType) int
- func ComputeEdgeStubs(g *Graph, routes []EdgeRoute) map[int]EdgeStubs
- func DetourBlockedEdges(g *Graph) int
- func EdgeEndpointSide(node, other Node, port EdgePort) string
- func EdgePortPoint(node, _ Node, port EdgePort) (int, int)
- func GrowToFit(positions []float64, order []int, before, after int, minSpan float64, ...) []float64
- func OrderSharedPorts(g *Graph) int
- func SolveSeparations(vars []VPSCVar, constraints []VPSCConstraint) []float64
- type Bounds
- type Constants
- type Container
- type Edge
- type EdgePort
- type EdgeRoute
- type EdgeRouteJSON
- type EdgeStubs
- type EdgeType
- type Graph
- type LayoutEdge
- type LayoutNode
- type Meta
- type Node
- type NodeStyle
- type NodeType
- type PortJSON
- type Position
- type VPSCConstraint
- type VPSCVar
Constants ¶
const ( GridStep = 10 // Grid alignment step DefaultCellW = 120 // Default node width DefaultCellH = 60 // Default node height AspectRatioLimit = 3 // Max node height:width before the aspect rule widens it MaxNodeWidth = 600 // Width-growth cap for the aspect rule (5 * DefaultCellW) BoundaryCellSize = 40 // S/E boundary node size CharsPerLine = 12 // For text width estimation LinesPerBaseH = 3 // Lines fitting in base height LineHeight = 20 // Height per extra line EventVGap = 60 // Vertical gap between events AuxHGap = 60 // Horizontal gap between aux nodes (things/concepts) and their anchor event TuckEdgeClear = 12 // Tuck offset past the anchor's center, clearing the vertical flow edge ThingHGap = 120 // Horizontal gap between standalone things (2x AuxHGap) ThingVGap = 40 // Vertical gap between standalone things AuxVGap = 20 // Vertical gap between auxiliary nodes (things/concepts) in VDistribute ConceptGap = 120 // Horizontal gap between concepts (2x AuxHGap) BoundaryVGap = 40 // Gap between boundary and first/last event // MaxFanAngleDeg caps the angle SPREAD between the two outermost edges // fanning from (or into) one event/boundary: each edge is held within // MaxFanAngleDeg/2 of the fan axis, so a wide or distant set of targets // is pushed further from the source until the spread closes to at most // this — never an obtuse near-flat fan. The required perpendicular gap for // a parallel offset dx is dx/tan(MaxFanAngleDeg/2). 150° → each edge within // 75° of vertical, min-slope 1/tan75 ≈ 0.27. // // UNIFIED with MaxBoundaryFanAngleDeg at 150° ("the same // angle for both"): interior fork/join fans and S/E boundary fans use ONE // angle, so a fork→row→join (or S→row→join) diamond reads as symmetric // halves. History: 150° → 130° (tightened) → 150° again (re-unified // with the boundary). The two constants are kept as a // seam in case they ever need to diverge again. MaxFanAngleDeg = 150 // MaxBoundaryFanAngleDeg caps S/E boundary fans — currently EQUAL to the // interior MaxFanAngleDeg (150°, see above): one fan angle everywhere keeps // the S→row and row→join halves of a diamond symmetric. // 150° → each boundary edge within 75° of vertical (min-slope ≈ 0.27); a // wide S/E fan keeps its square BoundaryCellSize marker close to the flow. // History: 130° (= interior) → 115° (steepened for the k8s PVC fan) → 130° // → 150°. The square marker's fan ports are a bit tighter than a 120px // event's (the brief box-widening to mirror them read badly, reverted). MaxBoundaryFanAngleDeg = 150 // BoundaryFanSlopeDen sets the minimum steepness of a LONE start boundary's // edge: S lifts until the single S->start edge has |dy| >= |dx|/den. A wide // start FAN (>=2 starts) is now capped at MaxFanAngleDeg via v5FanGap — the // same rule as the fork/join/E fans — so its S→row gap matches the row→join // gap (the old /2 made the start fan 18px taller). This // constant only governs the rare offset-lone-start case. The END lone // terminal keeps the gentler /5 (terminals converging to E never crowd). BoundaryFanSlopeDen = 2 // MaxCorridorLeverage caps how far the leads-to corridor-clearance pass // will relocate a join/member event to slip its incoming edge past a // previous-row aux box. The required member push amplifies the box's // clearance need by span/den (span = member-to-port horizontal run; den = // port-to-blocker-near-edge run). A blocker hugging the port (den << span) // otherwise levers a modest box depth into a multi-row near-vertical skew. // Past this leverage the blocker is in the port's own corridor — the edge // router bends around it instead of the layout relocating the member. MaxCorridorLeverage = 2 GraphGap = 120 // Gap between disconnected components MinNodeGap = 20 // Minimum gap between non-event nodes during overlap resolution StandaloneChainGap = 40 // Vertical gap for simple standalone node chains like A --> B ConceptChainVGap = 40 // Vertical gap between concepts in a chain (2x MinNodeGap) MarginX = 40 // Canvas margin X MarginY = 40 // Canvas margin Y ForkHGap = 60 // Horizontal gap in fork patterns TargetAspectRatio = 16.0 / 9.0 // Maximum canvas width-to-height ratio before compaction )
Layout constants Spec: gl:docs/dev/layout-engine.md
Variables ¶
var NodeStyles = map[NodeType]NodeStyle{ NodeEvent: {MarginX: 60, MarginY: 60}, NodeThing: {MarginX: 60, MarginY: 20}, NodeConcept: {MarginX: 60, MarginY: 20}, NodeUnresolved: {MarginX: 60, MarginY: 20}, NodeBoundary: {MarginX: 0, MarginY: 40}, }
Default styles per node type. Gap between adjacent nodes = max(A.Margin, B.Margin) (collapsing) Margin values are set to produce current gap behavior: - EventVGap (60) → Event.MarginY = 60 - EventVGap (60) → Event.MarginY = 60 - AuxHGap (60) → Thing/Concept.MarginX = 60 - AuxVGap/MinNodeGap (20) → Thing/Concept.MarginY = 20 - BoundaryVGap (40) → Boundary.MarginY = 40
Functions ¶
func CollapsedGapH ¶
CollapsedGapH returns the horizontal gap between two nodes using margin collapsing. Gap = max(A.MarginX, B.MarginX)
func CollapsedGapV ¶
CollapsedGapV returns the vertical gap between two nodes using margin collapsing. Gap = max(A.MarginY, B.MarginY)
func ComputeEdgeStubs ¶
ComputeEdgeStubs returns the stub polylines of every edge classified "stubbed" under the canvas policy, keyed by edge index.
func DetourBlockedEdges ¶
DetourBlockedEdges gives a bend path to every edge whose straight port-to-port line cuts a node box, and leaves every other edge alone. It returns the number of edges it rerouted.
Obstacles are the graph's real boxes. CONTAINER nodes are excluded: a shell encloses its own members by construction, so counting it would make every edge inside a container look blocked and there would be no clean route to find. An edge's own endpoints are excluded too.
Existing Route ports are honoured as the endpoints to route between; only Bends are written. Call it after the positions are final.
func EdgeEndpointSide ¶
EdgeEndpointSide returns the side of node that an edge endpoint attaches to, resolving a "center" port to the side its center→center line toward other would exit. Exposed so tooling (e.g. the fitness runner) can record the concrete attachment side of each routed edge endpoint.
func EdgePortPoint ¶
EdgePortPoint returns the absolute (x, y) pixel coordinate where the given port attaches to node. The second Node argument is intentionally unused: it is kept for call-site symmetry with the (from, to) convention shared by the other port helpers, so callers can pass the opposite endpoint uniformly.
func GrowToFit ¶
func GrowToFit(positions []float64, order []int, before, after int, minSpan float64, weights []float64) []float64
GrowToFit is the substrate's "make room here" helper: positions holds the current coordinates of ordered interval starts (e.g. row tops), order is their index order along the axis, and need demands that positions[after]−positions[before] ≥ minSpan for one adjacent pair. All other adjacent gaps keep at least their current size, so growth inserts space exactly between the colliding rows and shifts everything beyond — the elastic-between behavior as one solve instead of cascading shifts.
func OrderSharedPorts ¶
OrderSharedPorts fixes the crossing that happens when several edges leave one side of a node in the wrong order.
An edge port is box-relative — "left side, 40% down" — so it survives a node moving. Its ORDER along that side does not: the engine spread the fan to match where the partners were when it placed them, and once a consumer moves nodes (a zoom/canvas consumer's frames), two edges sharing a side can end up assigned the opposite way round from their partners. They then cross immediately, right next to the node they share.
This permutes the fractional positions WITHIN each (node, side) group so their order matches the partners' order along that side. The set of fractions is unchanged — the engine's chosen spacing is kept exactly, only which edge gets which slot changes — and no edge moves to a different side, so nothing else about the drawing shifts.
It cannot introduce a crossing it did not remove: after the permutation the ports are monotonic in the partner coordinate, which is the definition of "these two do not cross at this end".
Returns the number of endpoints whose slot changed.
func SolveSeparations ¶
func SolveSeparations(vars []VPSCVar, constraints []VPSCConstraint) []float64
SolveSeparations returns positions for vars satisfying every constraint. Constraint graphs must be acyclic in the Left→Right direction (a cycle has no feasible solution); the caller guarantees this by construction — the skeleton's order relation is a DAG by definition.
Types ¶
type Bounds ¶
type Bounds struct {
Width int `json:"width"`
Height int `json:"height"`
MarginX int `json:"marginX"`
MarginY int `json:"marginY"`
}
Bounds captures canvas dimensions.
type Constants ¶
type Constants struct {
Grid int `json:"gridStep"`
}
Constants reflects the layout constants embedded in the engine.
type Container ¶
type Container struct {
ChildNodeIDs []string `json:"childNodeIDs,omitempty"`
ShellStyle string `json:"shellStyle,omitempty"`
}
Container holds expanded composite-event shell metadata for a node. Its bounds come from the node itself (X, Y, Width, Height).
type Edge ¶
type Edge struct {
From string `json:"from"`
To string `json:"to"`
Dir string `json:"dir"`
Base string `json:"base"`
Style string `json:"style"`
Label string `json:"label,omitempty"`
FromPort *EdgePort `json:"fromPort,omitempty"`
ToPort *EdgePort `json:"toPort,omitempty"`
// Route is the OUTPUT geometry the layout owns (docs/dev/layout-engine.md
// "Edge routes in the layout output"): ports, bend waypoints and — for
// stubbed edges — the two short stub polylines. FromPort/ToPort above
// remain INPUT pins honoured by the route computation.
Route *EdgeRouteJSON `json:"route,omitempty"`
// Visibility is the canonical rendering class under the canvas stub
// policy ("" = visible in full, "stubbed" = hidden behind "?" badges by
// interactive consumers). Emitted so every renderer and metric shares ONE
// predicate (V6-EDGE-VISIBILITY.md); flat renderers may ignore it.
Visibility string `json:"visibility,omitempty"`
// Deferred marks a tie excluded from placement by the first-usage rule
// (v5DeferLateTies): the node anchors to its earliest user; this later
// tie is always rendered hidden (numbered stub pair + ghost line).
Deferred bool `json:"deferred,omitempty"`
}
Edge describes a styled logical connection.
FromPort / ToPort are optional attachment hints. When non-nil they override the default auto-computed port for that endpoint, pinning the arrow to a specific side ("left"/"right"/"top"/"bottom"/"center") and position along that side (0.0-1.0). Leave nil for default routing.
type EdgePort ¶
EdgePort pins an edge endpoint to a node side at a fractional position along it (0..1; 0.5 = the side's centre).
type EdgeRoute ¶
type EdgeRoute struct {
Source EdgePort
Target EdgePort
// Bends are intermediate waypoints between the source and target ports,
// in draw order — the polyline plumbing of the routing design. The obstacle router
// (routeAroundObstacles → detourPolyline) and the visibility refinement
// pass produce these bends when a straight chord would cut an obstacle box.
Bends []Position
}
EdgeRoute is the computed geometry of one edge: the resolved source and target ports plus any intermediate bend waypoints.
type EdgeRouteJSON ¶
type EdgeRouteJSON struct {
Source PortJSON `json:"source"`
Target PortJSON `json:"target"`
Bends []Position `json:"bends,omitempty"`
SourceStub []Position `json:"sourceStub,omitempty"`
TargetStub []Position `json:"targetStub,omitempty"`
}
EdgeRouteJSON is the serialized form of an edge's computed route.
type EdgeType ¶
type EdgeType string
EdgeType for layout semantics Spec: gl:docs/dev/layout-engine.md#L16
type Graph ¶
type Graph struct {
Version string `json:"version"`
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
Meta Meta `json:"meta"`
}
Graph represents the output ipm-simple-graph structure. Spec: gl:docs/dev/layout-engine.md#L346
type LayoutEdge ¶
type LayoutEdge struct {
ID int
SourceID int
TargetID int
EdgeType EdgeType
Dir string
Label string
}
LayoutEdge holds layout-relevant edge data
type LayoutNode ¶
type LayoutNode struct {
ID int
ModelID int // Original model ID (for boundary nodes, this is 0)
Type NodeType
Label string
Alias string
Tooltip string
Width int
Height int
X int
Y int
}
LayoutNode holds layout-relevant node data (internal)
type Meta ¶
type Meta struct {
Bounds Bounds `json:"bounds"`
Constants Constants `json:"constants"`
Warnings []string `json:"warnings,omitempty"`
}
Meta includes auxiliary information for renderers and diagnostics.
type Node ¶
type Node struct {
ID string `json:"id"`
Type string `json:"type"`
Label string `json:"label,omitempty"`
LabelOriginal string `json:"label-original,omitempty"`
Alias string `json:"alias,omitempty"`
Tooltip string `json:"tooltip,omitempty"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
RenderKind string `json:"renderKind,omitempty"`
ParentNodeIDs []string `json:"parentNodeIDs,omitempty"`
Container *Container `json:"container,omitempty"`
// Candidates lists an Unresolved node's possible kinds (lowercased "event"/
// "thing"/"concept"), primary first. Set only when Type == "unresolved";
// ipmsvg renders one corner swatch per candidate.
Candidates []string `json:"candidates,omitempty"`
}
Node describes a positioned node in the ipm-simple-graph output.
type NodeStyle ¶
type NodeStyle struct {
MarginX int // Horizontal margin (left = right)
MarginY int // Vertical margin (top = bottom)
}
NodeStyle defines spacing properties for a node type. Uses CSS-like margin model with collapsing. Spec: gl:docs/dev/layout-engine.md
type NodeType ¶
type NodeType string
NodeType for layout purposes (internal) Spec: gl:docs/dev/layout-engine.md#L16
type VPSCConstraint ¶
VPSCConstraint demands Position[Left] + Gap ≤ Position[Right].
type VPSCVar ¶
type VPSCVar struct {
// Desired is the position the variable wants (current/ideal coordinate).
Desired float64
// Weight scales the displacement cost; use large weights for boxes that
// must not move (skeleton rows/columns) and small ones for movable aux.
// Must be > 0.
Weight float64
}
VPSCVar is one variable on one axis.