graph

package
v0.3.22 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: AGPL-3.0 Imports: 6 Imported by: 0

Documentation

Overview

Package graph defines flowgo's in-memory graph model and the .flowgo text-format parser/serializer.

The package is intentionally small: types with their JSON tags (which must match the wire format the editor consumes over /state and /save), Parse, and Serialize. Anything richer (validation, HTTP handlers, MCP tools) lives in the upstream binary or in downstream consumers.

Pure hex-lattice math for hexagon-mode boxes — the Go port of src/graph/hex.ts. Keep the two in lockstep: same constants, same axial conventions, same never-overlap invariant. The editor uses the TS side for live drag-snap; this side lets the MCP tools honour the identical contract when an agent (not a pointer) places hexes.

Hexagons are uniform, flat-top, and never resizable: HexW × HexH CSS pixels. The lattice is implicit and LOCAL: there is no world origin — snapping anchors the lattice at the centre of the nearest existing hexagon. Axial coordinates (q, r) follow the redblobgames flat-top convention.

Key invariant: HexSnapRadius > HexW. Inside the radius a hex snaps onto a free cell (so it cannot overlap); outside it, its centre is more than a full hex width from every other centre, which makes overlap physically impossible. Either way two hexagons never overlap.

Index

Constants

View Source
const (
	// HexW / HexH: fixed footprint of every hexagon (flat-top;
	// H = W·√3/2 rounded to a whole pixel — see hex.ts).
	HexW = 240.0
	HexH = 208.0

	// Centre-to-centre lattice steps: +q is 0.75·W right and half a
	// row down; +r is one full row straight down.
	HexCol = HexW * 0.75 // 180
	HexRow = HexH        // 208

	// Magnetic range: snapping engages when a proposed centre comes
	// within this distance of another hexagon's centre.
	HexSnapRadius = HexW * 1.01 // 242.4

)
View Source
const MaxLabelLen = 500

MaxLabelLen mirrors MAX_LABEL_LEN in the JS editor. Box and text labels are clamped to this length on every entry path that produces new graph data — JS finish(), MCP add_box / update_box / add_text, any future ingestion. The validator flags anything longer in already-stored data.

Variables

This section is empty.

Functions

func HexesOverlap added in v0.3.19

func HexesOverlap(a, b HexPoint) bool

HexesOverlap reports whether two fixed-size flat-top hexagons centred at a and b overlap. Closed-form separating-axis result: the Minkowski sum of the hex with itself is the same hexagon scaled 2×, so the centres overlap iff their difference lies strictly inside that 2× hexagon. Exact edge-to-edge contact (adjacent lattice cells) counts as NOT overlapping.

func NormalizeLabel added in v0.0.17

func NormalizeLabel(raw string) string

NormalizeLabel collapses every run of non-newline whitespace to a single space, trims each line, drops fully-blank leading / trailing lines, and hard-caps to MaxLabelLen. Newlines are preserved — they render as hard line breaks in the editor and round-trip through the .flowgo file as a `\n` escape inside a quoted label. Mirrors normalizeLabel() in src/graph/label.ts.

The .flowgo text format is line-based, so a literal newline inside a label would corrupt the file; the serializer in graph.go handles the escape and the tokenizer decodes it on the way back in.

func Serialize

func Serialize(g Graph) string

Serialize emits the .flowgo text format. Empty maps are dropped — they get re-created on demand if a consumer navigates back to them.

When g.Version is non-empty, a `version <semver>` directive is emitted as the first line so consumers (older flowgo binaries, tools) can detect what wrote the file. Callers stamp the field at save time.

func Validate added in v0.1.3

func Validate(g Graph) []error

Validate runs semantic checks the .flowgo parser doesn't perform. Returns every violation it finds rather than stopping at the first one, so a single CI run surfaces all problems at once.

It is a superset of ValidateWritable: everything that would corrupt the file, plus complaints about graphs that persist perfectly well but don't mean anything sensible (orphaned submaps, edges pointing at deleted nodes, out-of-range styles).

func ValidateWritable added in v0.3.7

func ValidateWritable(g Graph) []error

ValidateWritable returns only the violations that would damage the .flowgo file itself — fields whose bytes do not survive a Serialize → Parse round-trip. Every path that writes a caller- supplied graph to disk must run this first.

The distinction from Validate matters: a mid-edit document can legitimately fail Validate (a submap outliving the node it hung off, an edge whose target was just deleted) while serializing and re-parsing perfectly. Gating saves on the full validator would lock the editor out of persisting documents it is allowed to produce, which is a worse failure than the corruption being prevented. This subset only rejects input that cannot be written down at all.

The invariant it buys: for any g with no ValidateWritable errors, Parse(Serialize(g)) succeeds and returns the same elements.

func WorldToAxial added in v0.3.19

func WorldToAxial(origin HexPoint, p HexPoint) (q, r float64)

WorldToAxial converts a world point to fractional axial coordinates on the lattice anchored at origin. Fractional on purpose: feed through AxialRound to get the containing cell.

Types

type Axial added in v0.3.19

type Axial struct {
	Q int
	R int
}

Axial is a lattice cell in axial coordinates.

func AxialRound added in v0.3.19

func AxialRound(q, r float64) Axial

AxialRound rounds fractional axial coordinates to the nearest cell via cube rounding: round all three cube coords, then fix the one with the largest rounding error so q + r + s stays 0. Plain independent rounding picks the wrong cell near boundaries.

func NearestCell added in v0.3.19

func NearestCell(origin HexPoint, p HexPoint) Axial

NearestCell returns the lattice cell nearest to world point p on the lattice anchored at origin.

func NearestFreeCell added in v0.3.19

func NearestFreeCell(origin HexPoint, p HexPoint, occupied []HexPoint, maxRings int) (Axial, bool)

NearestFreeCell finds the free cell nearest to world point p on the lattice anchored at origin. A cell is free when a hexagon centred there would overlap none of the occupied centres. Searches outward ring by ring up to maxRings; within the first ring with any free cell, picks the one closest to p. Returns (Axial{}, false) when every cell inside maxRings is blocked.

type Box

type Box struct {
	ID      string  `json:"id"`
	Label   string  `json:"label"`
	X       float64 `json:"x"`
	Y       float64 `json:"y"`
	Palette int     `json:"palette,omitempty"`
	Font    int     `json:"font,omitempty"`
	// Anchor marks this node as the map-level recenter target. At most
	// one node per map carries Anchor=true; the parser/serializer enforce
	// the invariant. Persisted in the .flowgo text format as a separate
	// per-map `anchor <id>` directive rather than a positional token.
	Anchor bool `json:"anchor,omitempty"`
	// W/H, when both > 0, pin the node to an explicit on-canvas size in
	// data pixels (the user resized it in the editor). Zero means
	// auto-size: the node hugs its label like it always has. Persisted
	// as a separate `nodesize <id> <w> <h>` directive (legacy spelling
	// `boxsize` still parses) — the positional slots on the `node`
	// line are all claimed by back-compat baggage.
	// Ignored for special shapes (Shape!=0), which have fixed sizes.
	W float64 `json:"w,omitempty"`
	H float64 `json:"h,omitempty"`
	// Shape selects the render silhouette: 0 (default) is the classic
	// auto-sized rectangle, 1 hexagon, 2 circle, 3 triangle (all fixed
	// uniform size, never resizable; hexagons additionally lattice-
	// snap). 4-9 are reserved. Persisted as a separate
	// `nodeshape <id> <shape>` directive after the node block (legacy
	// spelling `boxshape` still parses) — the positional slots on the
	// `node` line are all claimed by back-compat baggage (mirrors the
	// linestyle precedent for lines).
	Shape int `json:"shape,omitempty"`
}

Box is a node on a map. JSON tags are part of the public contract: they're consumed by the editor and any other process that exchanges graphs as JSON. (The Go type keeps its historical Box name — the text format's canonical directive is `node`, with `box` as the deprecated legacy spelling; renaming the public type would break downstream consumers for no wire-level gain.)

The `.flowgo` text format keeps a vestigial "sides" slot between the y coordinate and the palette token (always emitted as 4) so old node directives like `node b1 hi 0 0 3 5` still parse positionally — the polygon feature is gone, but the wire layout is preserved.

type Edge

type Edge struct {
	From       string `json:"from"`
	FromHandle string `json:"fromHandle,omitempty"`
	To         string `json:"to"`
	ToHandle   string `json:"toHandle,omitempty"`
	Palette    int    `json:"palette,omitempty"`
	// Label is the text drawn at the edge midpoint — "depends on",
	// "triggers", "owns". Empty means unlabelled.
	//
	// Persisted as the FIFTH positional token on the `edge` line,
	// after the palette: `edge <from> <to> <palette> <label>`. It has
	// to go after, not before: slot 4 has been the optional palette
	// since the format existed, and Parse reads it with strconv.Atoi,
	// so a label there would either be misread as a palette (label
	// "3") or hard-error every existing reader. A label therefore
	// forces a palette token to be emitted; when the edge has no
	// palette of its own the default sentinel `1` fills the slot,
	// exactly like the `line` directive does to park its mid
	// coordinates in a stable position. Parse ignores palette 1.
	//
	// Unlabelled edges emit no extra token at all, so every document
	// written before edge labels existed keeps its exact bytes.
	//
	// Compatibility (measured against 0.3.12, not assumed): older
	// binaries do NOT reject the five-token line — their edge case
	// stops reading after the palette — so an old flowgo opens a
	// labelled map fine. It drops the labels when it next WRITES the
	// file. Gentler than the `defaultshape` break in #208, but still
	// one-way: don't round-trip a labelled map through an old binary.
	//
	// Unlike a node, an edge has no id, and the .flowgo format does
	// not stop a hand-written file from carrying two `edge a b` lines
	// — so a side-table directive (the `nodesize` / `nodeshape`
	// pattern) could not say WHICH edge it labelled. The positional
	// token has no such ambiguity.
	Label string `json:"label,omitempty"`
}

Edge connects two nodes within the same map.

type Graph

type Graph struct {
	Version string     `json:"version,omitempty"`
	Maps    []NamedMap `json:"maps"`
	// DefaultShape records the shape a canvas double-click creates in
	// this document: 0 (absent) rectangle, 1 hexagon, 2 circle,
	// 3 triangle. Persisted as a document-level `defaultshape <n>`
	// directive right after `version`; zero is never emitted. The
	// legacy `hexagons on` directive still parses (as DefaultShape=1)
	// but is no longer written — the per-browser hexagon setting it
	// backed was retired in favour of this per-file default.
	DefaultShape int `json:"defaultShape,omitempty"`
}

Graph is the full document — every map keyed by its path.

Version records the flowgo binary version that last wrote this graph. It's stamped at save time and surfaced for tools that need to gate behaviour on the writer's version. Empty Version means the file pre-dates the directive (older flowgo) and should be treated as "unknown".

func Parse

func Parse(s string) (Graph, error)

Parse reads the .flowgo text format and returns the resulting Graph. Unknown directives produce an error rather than being silently dropped, so a downstream package init that depends on Parse fails loudly when the format gains a new directive.

type HexPoint added in v0.3.19

type HexPoint struct {
	X float64
	Y float64
}

HexPoint is a world-space point (a hexagon centre, mostly).

func AxialToWorld added in v0.3.19

func AxialToWorld(origin HexPoint, cell Axial) HexPoint

AxialToWorld converts a cell to its world centre, for a lattice anchored at origin (itself cell (0,0)).

func SettleHexCenters added in v0.3.19

func SettleHexCenters(centers []HexPoint) []HexPoint

SettleHexCenters is the invariant repair for centres that arrived without live snapping (raw imports, set_state): walk the centres in order and relocate any hex that overlaps an earlier (already settled) one to the nearest free cell on the lattice anchored at the hex it collided with. Returns a new slice; non-colliding centres pass through untouched.

Unlike the magnetic snap, repair must not give up just because the neighbourhood is crowded — a stress import can stack hexes inside a blob far wider than the snap's 6-ring window — so the search depth scales with the population (settleRings).

func SnapHexCenter added in v0.3.19

func SnapHexCenter(proposed HexPoint, others []HexPoint) (HexPoint, bool)

SnapHexCenter is the magnetic snap: given a proposed hex centre and the centres of every OTHER hexagon on the map, returns the snapped centre and true — or false when the hex is out of magnetic range (or no nearby cell is free) and should stay exactly where it was proposed. The lattice is anchored at the nearest other hex, so the snapped hex always lands flush against it.

type Image added in v0.1.8

type Image struct {
	ID     string  `json:"id"`
	Src    string  `json:"src"`
	X      float64 `json:"x"`
	Y      float64 `json:"y"`
	Width  float64 `json:"width"`
	Height float64 `json:"height"`
}

Image is a raster asset placed on a map. Src is a path relative to the .flowgo file (e.g. "flowgo-media/<hash>.png"); the binary lives in the flowgo-media/ sibling folder, never inline in the text file. Width/Height are the on-canvas display size in data pixels.

type Line

type Line struct {
	ID      string      `json:"id"`
	X1      float64     `json:"x1"`
	Y1      float64     `json:"y1"`
	X2      float64     `json:"x2"`
	Y2      float64     `json:"y2"`
	Palette int         `json:"palette,omitempty"`
	Style   int         `json:"style,omitempty"`
	Mids    [][]float64 `json:"mids,omitempty"`
}

Line is a static segment that runs through its endpoints and any intermediate Mid control points. Style governs how each pair of consecutive points is drawn: 1 (or 0/unset) renders straight segments (sharp polyline), 2 renders a smooth quadratic-bezier chain, 3 renders right-angle elbows (orthogonal).

type NamedMap

type NamedMap struct {
	Path    string   `json:"path"`
	Boxes   []Box    `json:"boxes"`
	Edges   []Edge   `json:"edges"`
	Texts   []Text   `json:"texts,omitempty"`
	Lines   []Line   `json:"lines,omitempty"`
	Strokes []Stroke `json:"strokes,omitempty"`
	Images  []Image  `json:"images,omitempty"`
}

NamedMap is one canvas at a given path. Submap paths are slash- separated node ids: "/A/B" hangs off node A on "/" and node B on "/A".

type Stroke

type Stroke struct {
	ID      string      `json:"id"`
	Points  [][]float64 `json:"points"`
	Palette int         `json:"palette,omitempty"`
}

Stroke is a freehand polyline (brush mode).

type Text

type Text struct {
	ID      string  `json:"id"`
	Label   string  `json:"label"`
	X       float64 `json:"x"`
	Y       float64 `json:"y"`
	Palette int     `json:"palette,omitempty"`
	Font    int     `json:"font,omitempty"`
}

Text is a free-floating annotation.

Jump to

Keyboard shortcuts

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