layout

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

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

View Source
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

View Source
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

func CollapsedGapH(aType, bType NodeType) int

CollapsedGapH returns the horizontal gap between two nodes using margin collapsing. Gap = max(A.MarginX, B.MarginX)

func CollapsedGapV

func CollapsedGapV(aType, bType NodeType) int

CollapsedGapV returns the vertical gap between two nodes using margin collapsing. Gap = max(A.MarginY, B.MarginY)

func ComputeEdgeStubs

func ComputeEdgeStubs(g *Graph, routes []EdgeRoute) map[int]EdgeStubs

ComputeEdgeStubs returns the stub polylines of every edge classified "stubbed" under the canvas policy, keyed by edge index.

func DetourBlockedEdges

func DetourBlockedEdges(g *Graph) int

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

func EdgeEndpointSide(node, other Node, port EdgePort) string

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

func EdgePortPoint(node, _ Node, port EdgePort) (int, int)

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

func OrderSharedPorts(g *Graph) int

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

type EdgePort struct {
	Side     string
	Position float64
}

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.

func RoutesOf

func RoutesOf(g *Graph) []EdgeRoute

RoutesOf returns the edge routes of a graph: the emitted ones (every engine output carries explicit routes). A route-less edge — a hand-written layout.json — falls back to centre ports resolved by geometry.

func (EdgeRoute) Segments

func (r EdgeRoute) Segments(sx, sy, tx, ty int) [][4]int

Segments returns the route's polyline as consecutive point pairs, given the resolved port endpoints. A bend-free route is one straight segment.

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 EdgeStubs

type EdgeStubs struct {
	Source []Position
	Target []Position
}

EdgeStubs is one stubbed edge's pair of rendered polylines.

type EdgeType

type EdgeType string

EdgeType for layout semantics Spec: gl:docs/dev/layout-engine.md#L16

const (
	EdgeLeadsTo   EdgeType = "leadsto"
	EdgePartOf    EdgeType = "partof"
	EdgeExpresses EdgeType = "expresses"
	EdgeNearTo    EdgeType = "nearto"
)

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

const (
	NodeEvent      NodeType = "event"
	NodeThing      NodeType = "thing"
	NodeConcept    NodeType = "concept"
	NodeUnresolved NodeType = "unresolved"
	NodeBoundary   NodeType = "boundary"
)

type PortJSON

type PortJSON struct {
	Side     string  `json:"side"`
	Position float64 `json:"position"`
}

PortJSON is a serialized edge port (side + fractional position).

type Position

type Position struct {
	X int
	Y int
}

Position holds x,y coordinates

type VPSCConstraint

type VPSCConstraint struct {
	Left, Right int
	Gap         float64
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL