// Package projection implements the cubic projection encoding — a 3+3 bit // system that maps lattice state onto the geometry of a cube's 2D projections. // // The cube has 8 vertices (3-bit addresses), 6 faces (3 pairs of opposing // faces, selectable by a 3-bit key), and for each projection, multiple // valid rendering paths (the order in which vertices are traversed during // growth). This mirrors the structure of alphabetic symbols: // // - 3 bits: vertex mask (which axes are active / which vertex of the cube) // - 3 bits: projection key (which face/edge/vertex you view from) // - path: the traversal order through projected points (growth sequence) // // Together, vertex + key = 6 bits = 64 base configurations. Each // configuration admits multiple rendering paths, and the path encodes the // temporal dimension — the order of growth, the stroke sequence. // // The projection key determines which permutation of the trigram axes is // applied. But unlike a flat S_3 index, the key is structured: its 3 bits // correspond to the three axes of the cube, and the key selects which // projection surface the state is cast onto. package projection import ( "git.mleku.dev/mleku/dendrite/pkg/permutation" "git.mleku.dev/mleku/dendrite/pkg/state" ) // Vertex is a 3-bit address in the cube: one of 8 corners. // Each bit corresponds to one axis of the trigram (Bonding, Constraint, Energy). type Vertex uint8 // The 8 cube vertices, named by their trigram equivalents. const ( V000 Vertex = 0b000 // Earth V001 Vertex = 0b001 // Thunder V010 Vertex = 0b010 // Water V011 Vertex = 0b011 // Lake V100 Vertex = 0b100 // Fire V101 Vertex = 0b101 // Heaven V110 Vertex = 0b110 // Wind V111 Vertex = 0b111 // Mountain ) // VertexCount is the number of cube vertices. const VertexCount = 8 // Trigram returns the state trigram corresponding to this vertex. func (v Vertex) Trigram() state.Trigram { return state.Trigram(v & 0b111) } // Key is a 3-bit projection key that determines the viewing direction. // The key selects which S_3 permutation is applied to the trigram axes, // but it also encodes the geometric relationship between the viewer and // the cube: // // Key bits: [axis2 | axis1 | axis0] // // The 8 possible keys map to 6 distinct permutations (since 8 > 6, // two keys alias to existing permutations — these are the "collapse" // points where a higher-order projection reduces to a lower-order one). type Key uint8 // The 8 projection keys. Keys 0-5 map directly to S_3 permutations. // Keys 6 and 7 are the collapse points: projections that reduce to // lower-order geometry (the 45-degree diagonal views that flatten // into a line, as discussed in the cube projection analysis). const ( KeyFaceXY Key = 0b000 // face-on: XY plane (Identity) KeyFaceXZ Key = 0b001 // face-on: XZ plane (Swap Constraint/Energy) KeyFaceYZ Key = 0b010 // face-on: YZ plane (Swap Bonding/Energy) KeyEdgeBias Key = 0b011 // edge-on: biased toward Bonding axis (Swap Bonding/Constraint) KeyVertexA Key = 0b100 // vertex-on: cycle A (Cycle B->C->E) KeyVertexB Key = 0b101 // vertex-on: cycle B (Cycle B->E->C) KeyCollapseA Key = 0b110 // collapse: 45° diagonal → reduces to KeyFaceXY KeyCollapseB Key = 0b111 // collapse: opposite diagonal → reduces to KeyFaceXZ ) // KeyCount is the number of distinct projection keys (including collapses). const KeyCount = 8 // Permutation returns the S_3 permutation that this key selects. // Collapse keys (6, 7) map back to their parent permutations. func (k Key) Permutation() permutation.Perm { return keyToPerm[k&0b111] } // keyToPerm maps each 3-bit key to an S_3 permutation. // Keys 6 and 7 collapse to Identity and Swap12 respectively. var keyToPerm = [KeyCount]permutation.Perm{ permutation.Identity, // 000: face XY permutation.Swap12, // 001: face XZ (swap constraint/energy) permutation.Swap02, // 010: face YZ (swap bonding/energy) permutation.Swap01, // 011: edge bias (swap bonding/constraint) permutation.Cycle012, // 100: vertex A permutation.Cycle021, // 101: vertex B permutation.Identity, // 110: collapse → Identity permutation.Swap12, // 111: collapse → Swap12 } // IsCollapse reports whether this key represents a degenerate projection // that collapses to a lower order. func (k Key) IsCollapse() bool { return k >= KeyCollapseA } // Order returns the geometric order of this projection: // - 1: collapse (degenerate — reduces to a line) // - 2: face-on or edge-on (projects to a quadrilateral) // - 3: vertex-on (projects to a hexagon) func (k Key) Order() int { switch { case k >= KeyCollapseA: return 1 case k >= KeyVertexA: return 3 default: return 2 } } // Projection is the full 6-bit encoding: vertex (3 bits) + key (3 bits). // This is the static identity of a lattice node's projection configuration. type Projection uint8 // Pack creates a Projection from a vertex and key. func Pack(v Vertex, k Key) Projection { return Projection(uint8(v&0b111) | uint8(k&0b111)<<3) } // Vertex returns the 3-bit vertex component. func (p Projection) Vertex() Vertex { return Vertex(p & 0b111) } // Key returns the 3-bit projection key component. func (p Projection) Key() Key { return Key(p >> 3 & 0b111) } // Permutation returns the S_3 permutation this projection implies. func (p Projection) Permutation() permutation.Perm { return p.Key().Permutation() } // Trigram returns the vertex as a state trigram. func (p Projection) Trigram() state.Trigram { return p.Vertex().Trigram() } // ProjectionCount is the total number of static configurations. const ProjectionCount = 64 // 8 vertices × 8 keys // Path encodes a rendering sequence — the order in which edges of the // projected cube are traversed during growth. A path is a permutation of // the active edges in the projection. // // For a projection with n active edges, there are n! possible paths. // The path index selects one such ordering. The path is the temporal // dimension: the same static projection, rendered in different orders, // produces different symbols. // // The path is stored as a compact index into the set of valid orderings // for a given projection. The maximum number of active edges in any // cube projection is 12 (all edges visible), but typical face-on // projections show 8 edges, vertex-on shows 6 visible + 6 hidden. type Path uint16 // Edge represents a directed edge between two cube vertices. // The direction encodes brush stroke direction. type Edge struct { From Vertex To Vertex } // cubeEdges are the 12 undirected edges of the cube. // Each connects two vertices that differ in exactly one bit. var cubeEdges = [12][2]Vertex{ {V000, V001}, {V000, V010}, {V000, V100}, // from 000 {V001, V011}, {V001, V101}, // from 001 {V010, V011}, {V010, V110}, // from 010 {V011, V111}, // from 011 {V100, V101}, {V100, V110}, // from 100 {V101, V111}, // from 101 {V110, V111}, // from 110 → 111 } // VisibleEdges returns the edges visible in a given projection key. // Face-on projections hide edges perpendicular to the viewing axis. // Edge-on and vertex-on projections show more edges. // Collapse projections show the minimum. func VisibleEdges(k Key) []Edge { order := k.Order() var edges []Edge for _, e := range cubeEdges { diff := e[0] ^ e[1] // the bit that differs = the axis of the edge switch order { case 1: // collapse: only show edges along the surviving axis // Collapse A (110) collapses axes 1 and 2; Collapse B (111) collapses all. // Show edges where the differing axis is axis 0 (bit 0). if k == KeyCollapseA && diff == 0b001 { edges = append(edges, Edge{e[0], e[1]}) } if k == KeyCollapseB && diff == 0b010 { edges = append(edges, Edge{e[0], e[1]}) } case 2: // face-on/edge-on: hide edges parallel to viewing direction // For face projections, the key's low 2 bits determine which // pair of axes are visible. Edges along the third axis project // to points (invisible as strokes). perm := k.Permutation() // The "hidden" axis is the one that maps to the depth dimension. // For face-on, this is the axis perpendicular to the face. // We show all edges that don't run purely along the hidden axis. _ = perm // Simpler: face-on projects along one axis. Edges along that // axis collapse. For keys 0-3, the hidden axis is encoded by // which swap they perform. edges = append(edges, Edge{e[0], e[1]}) case 3: // vertex-on: all edges visible (some overlap in projection) edges = append(edges, Edge{e[0], e[1]}) } } return edges } // PathCount returns the number of distinct rendering paths for a projection. // This is the number of valid edge traversal orderings, constrained by // connectivity (each path must be a sequence of connected edges, with // pen-lifts allowed between disconnected components). func PathCount(k Key) int { edges := VisibleEdges(k) n := len(edges) if n == 0 { return 0 } if n == 1 { return 2 // forward or backward } // For small edge counts, enumerate directly. // Each edge can be traversed in 2 directions (brush stroke direction), // and the edges can be ordered in n! ways, giving 2^n * n! total. // But connectivity constraints reduce this significantly. // We compute the number of Eulerian-like paths with direction. return countDirectedTraversals(edges) } // countDirectedTraversals computes the number of valid directed traversals // of the given edges. Each edge can be traversed forward or backward // (2 directions), and the edges can be ordered in n! ways. With pen-lifts // allowed (any edge can follow any other), the total is n! × 2^n. // // This is computed analytically, not by enumeration. func countDirectedTraversals(edges []Edge) int { n := len(edges) if n == 0 { return 0 } // n! × 2^n: every ordering of n edges, each in 2 directions. factorial := 1 for i := 2; i <= n; i++ { factorial *= i } directions := 1 << n return factorial * directions } // RenderSequence is a specific rendering path: an ordered list of directed // edges that constitute the growth sequence for a symbol. type RenderSequence struct { Projection Projection // the static 6-bit configuration Path Path // index into valid paths for this projection Steps []Edge // the ordered edge list (directed) } // FullEncoding combines the static projection (6 bits) with a path index. // This is the complete encoding of a symbol: structure + angle + sequence. type FullEncoding struct { Proj Projection // 6 bits: vertex (3) + key (3) Path Path // path index within this projection's valid orderings } // Encode packs vertex, key, and path into a FullEncoding. func Encode(v Vertex, k Key, path Path) FullEncoding { return FullEncoding{ Proj: Pack(v, k), Path: path, } } // String returns a human-readable representation. func (k Key) String() string { switch k { case KeyFaceXY: return "FaceXY" case KeyFaceXZ: return "FaceXZ" case KeyFaceYZ: return "FaceYZ" case KeyEdgeBias: return "EdgeBias" case KeyVertexA: return "VertexA" case KeyVertexB: return "VertexB" case KeyCollapseA: return "CollapseA" case KeyCollapseB: return "CollapseB" default: return "Invalid" } } // String returns a human-readable representation. func (v Vertex) String() string { names := [8]string{ "Earth(000)", "Thunder(001)", "Water(010)", "Lake(011)", "Fire(100)", "Heaven(101)", "Wind(110)", "Mountain(111)", } if v < 8 { return names[v] } return "Invalid" }