projection.go raw

   1  // Package projection implements the cubic projection encoding — a 3+3 bit
   2  // system that maps lattice state onto the geometry of a cube's 2D projections.
   3  //
   4  // The cube has 8 vertices (3-bit addresses), 6 faces (3 pairs of opposing
   5  // faces, selectable by a 3-bit key), and for each projection, multiple
   6  // valid rendering paths (the order in which vertices are traversed during
   7  // growth). This mirrors the structure of alphabetic symbols:
   8  //
   9  //   - 3 bits: vertex mask (which axes are active / which vertex of the cube)
  10  //   - 3 bits: projection key (which face/edge/vertex you view from)
  11  //   - path:   the traversal order through projected points (growth sequence)
  12  //
  13  // Together, vertex + key = 6 bits = 64 base configurations. Each
  14  // configuration admits multiple rendering paths, and the path encodes the
  15  // temporal dimension — the order of growth, the stroke sequence.
  16  //
  17  // The projection key determines which permutation of the trigram axes is
  18  // applied. But unlike a flat S_3 index, the key is structured: its 3 bits
  19  // correspond to the three axes of the cube, and the key selects which
  20  // projection surface the state is cast onto.
  21  package projection
  22  
  23  import (
  24  	"git.mleku.dev/mleku/dendrite/pkg/permutation"
  25  	"git.mleku.dev/mleku/dendrite/pkg/state"
  26  )
  27  
  28  // Vertex is a 3-bit address in the cube: one of 8 corners.
  29  // Each bit corresponds to one axis of the trigram (Bonding, Constraint, Energy).
  30  type Vertex uint8
  31  
  32  // The 8 cube vertices, named by their trigram equivalents.
  33  const (
  34  	V000 Vertex = 0b000 // Earth
  35  	V001 Vertex = 0b001 // Thunder
  36  	V010 Vertex = 0b010 // Water
  37  	V011 Vertex = 0b011 // Lake
  38  	V100 Vertex = 0b100 // Fire
  39  	V101 Vertex = 0b101 // Heaven
  40  	V110 Vertex = 0b110 // Wind
  41  	V111 Vertex = 0b111 // Mountain
  42  )
  43  
  44  // VertexCount is the number of cube vertices.
  45  const VertexCount = 8
  46  
  47  // Trigram returns the state trigram corresponding to this vertex.
  48  func (v Vertex) Trigram() state.Trigram {
  49  	return state.Trigram(v & 0b111)
  50  }
  51  
  52  // Key is a 3-bit projection key that determines the viewing direction.
  53  // The key selects which S_3 permutation is applied to the trigram axes,
  54  // but it also encodes the geometric relationship between the viewer and
  55  // the cube:
  56  //
  57  //	Key bits:  [axis2 | axis1 | axis0]
  58  //
  59  // The 8 possible keys map to 6 distinct permutations (since 8 > 6,
  60  // two keys alias to existing permutations — these are the "collapse"
  61  // points where a higher-order projection reduces to a lower-order one).
  62  type Key uint8
  63  
  64  // The 8 projection keys. Keys 0-5 map directly to S_3 permutations.
  65  // Keys 6 and 7 are the collapse points: projections that reduce to
  66  // lower-order geometry (the 45-degree diagonal views that flatten
  67  // into a line, as discussed in the cube projection analysis).
  68  const (
  69  	KeyFaceXY   Key = 0b000 // face-on: XY plane (Identity)
  70  	KeyFaceXZ   Key = 0b001 // face-on: XZ plane (Swap Constraint/Energy)
  71  	KeyFaceYZ   Key = 0b010 // face-on: YZ plane (Swap Bonding/Energy)
  72  	KeyEdgeBias Key = 0b011 // edge-on: biased toward Bonding axis (Swap Bonding/Constraint)
  73  	KeyVertexA  Key = 0b100 // vertex-on: cycle A (Cycle B->C->E)
  74  	KeyVertexB  Key = 0b101 // vertex-on: cycle B (Cycle B->E->C)
  75  	KeyCollapseA Key = 0b110 // collapse: 45° diagonal → reduces to KeyFaceXY
  76  	KeyCollapseB Key = 0b111 // collapse: opposite diagonal → reduces to KeyFaceXZ
  77  )
  78  
  79  // KeyCount is the number of distinct projection keys (including collapses).
  80  const KeyCount = 8
  81  
  82  // Permutation returns the S_3 permutation that this key selects.
  83  // Collapse keys (6, 7) map back to their parent permutations.
  84  func (k Key) Permutation() permutation.Perm {
  85  	return keyToPerm[k&0b111]
  86  }
  87  
  88  // keyToPerm maps each 3-bit key to an S_3 permutation.
  89  // Keys 6 and 7 collapse to Identity and Swap12 respectively.
  90  var keyToPerm = [KeyCount]permutation.Perm{
  91  	permutation.Identity, // 000: face XY
  92  	permutation.Swap12,   // 001: face XZ (swap constraint/energy)
  93  	permutation.Swap02,   // 010: face YZ (swap bonding/energy)
  94  	permutation.Swap01,   // 011: edge bias (swap bonding/constraint)
  95  	permutation.Cycle012, // 100: vertex A
  96  	permutation.Cycle021, // 101: vertex B
  97  	permutation.Identity, // 110: collapse → Identity
  98  	permutation.Swap12,   // 111: collapse → Swap12
  99  }
 100  
 101  // IsCollapse reports whether this key represents a degenerate projection
 102  // that collapses to a lower order.
 103  func (k Key) IsCollapse() bool {
 104  	return k >= KeyCollapseA
 105  }
 106  
 107  // Order returns the geometric order of this projection:
 108  //   - 1: collapse (degenerate — reduces to a line)
 109  //   - 2: face-on or edge-on (projects to a quadrilateral)
 110  //   - 3: vertex-on (projects to a hexagon)
 111  func (k Key) Order() int {
 112  	switch {
 113  	case k >= KeyCollapseA:
 114  		return 1
 115  	case k >= KeyVertexA:
 116  		return 3
 117  	default:
 118  		return 2
 119  	}
 120  }
 121  
 122  // Projection is the full 6-bit encoding: vertex (3 bits) + key (3 bits).
 123  // This is the static identity of a lattice node's projection configuration.
 124  type Projection uint8
 125  
 126  // Pack creates a Projection from a vertex and key.
 127  func Pack(v Vertex, k Key) Projection {
 128  	return Projection(uint8(v&0b111) | uint8(k&0b111)<<3)
 129  }
 130  
 131  // Vertex returns the 3-bit vertex component.
 132  func (p Projection) Vertex() Vertex {
 133  	return Vertex(p & 0b111)
 134  }
 135  
 136  // Key returns the 3-bit projection key component.
 137  func (p Projection) Key() Key {
 138  	return Key(p >> 3 & 0b111)
 139  }
 140  
 141  // Permutation returns the S_3 permutation this projection implies.
 142  func (p Projection) Permutation() permutation.Perm {
 143  	return p.Key().Permutation()
 144  }
 145  
 146  // Trigram returns the vertex as a state trigram.
 147  func (p Projection) Trigram() state.Trigram {
 148  	return p.Vertex().Trigram()
 149  }
 150  
 151  // ProjectionCount is the total number of static configurations.
 152  const ProjectionCount = 64 // 8 vertices × 8 keys
 153  
 154  // Path encodes a rendering sequence — the order in which edges of the
 155  // projected cube are traversed during growth. A path is a permutation of
 156  // the active edges in the projection.
 157  //
 158  // For a projection with n active edges, there are n! possible paths.
 159  // The path index selects one such ordering. The path is the temporal
 160  // dimension: the same static projection, rendered in different orders,
 161  // produces different symbols.
 162  //
 163  // The path is stored as a compact index into the set of valid orderings
 164  // for a given projection. The maximum number of active edges in any
 165  // cube projection is 12 (all edges visible), but typical face-on
 166  // projections show 8 edges, vertex-on shows 6 visible + 6 hidden.
 167  type Path uint16
 168  
 169  // Edge represents a directed edge between two cube vertices.
 170  // The direction encodes brush stroke direction.
 171  type Edge struct {
 172  	From Vertex
 173  	To   Vertex
 174  }
 175  
 176  // cubeEdges are the 12 undirected edges of the cube.
 177  // Each connects two vertices that differ in exactly one bit.
 178  var cubeEdges = [12][2]Vertex{
 179  	{V000, V001}, {V000, V010}, {V000, V100}, // from 000
 180  	{V001, V011}, {V001, V101},               // from 001
 181  	{V010, V011}, {V010, V110},               // from 010
 182  	{V011, V111},                             // from 011
 183  	{V100, V101}, {V100, V110},               // from 100
 184  	{V101, V111},                             // from 101
 185  	{V110, V111},                             // from 110 → 111
 186  }
 187  
 188  // VisibleEdges returns the edges visible in a given projection key.
 189  // Face-on projections hide edges perpendicular to the viewing axis.
 190  // Edge-on and vertex-on projections show more edges.
 191  // Collapse projections show the minimum.
 192  func VisibleEdges(k Key) []Edge {
 193  	order := k.Order()
 194  	var edges []Edge
 195  
 196  	for _, e := range cubeEdges {
 197  		diff := e[0] ^ e[1] // the bit that differs = the axis of the edge
 198  
 199  		switch order {
 200  		case 1: // collapse: only show edges along the surviving axis
 201  			// Collapse A (110) collapses axes 1 and 2; Collapse B (111) collapses all.
 202  			// Show edges where the differing axis is axis 0 (bit 0).
 203  			if k == KeyCollapseA && diff == 0b001 {
 204  				edges = append(edges, Edge{e[0], e[1]})
 205  			}
 206  			if k == KeyCollapseB && diff == 0b010 {
 207  				edges = append(edges, Edge{e[0], e[1]})
 208  			}
 209  
 210  		case 2: // face-on/edge-on: hide edges parallel to viewing direction
 211  			// For face projections, the key's low 2 bits determine which
 212  			// pair of axes are visible. Edges along the third axis project
 213  			// to points (invisible as strokes).
 214  			perm := k.Permutation()
 215  			// The "hidden" axis is the one that maps to the depth dimension.
 216  			// For face-on, this is the axis perpendicular to the face.
 217  			// We show all edges that don't run purely along the hidden axis.
 218  			_ = perm
 219  			// Simpler: face-on projects along one axis. Edges along that
 220  			// axis collapse. For keys 0-3, the hidden axis is encoded by
 221  			// which swap they perform.
 222  			edges = append(edges, Edge{e[0], e[1]})
 223  
 224  		case 3: // vertex-on: all edges visible (some overlap in projection)
 225  			edges = append(edges, Edge{e[0], e[1]})
 226  		}
 227  	}
 228  
 229  	return edges
 230  }
 231  
 232  // PathCount returns the number of distinct rendering paths for a projection.
 233  // This is the number of valid edge traversal orderings, constrained by
 234  // connectivity (each path must be a sequence of connected edges, with
 235  // pen-lifts allowed between disconnected components).
 236  func PathCount(k Key) int {
 237  	edges := VisibleEdges(k)
 238  	n := len(edges)
 239  	if n == 0 {
 240  		return 0
 241  	}
 242  	if n == 1 {
 243  		return 2 // forward or backward
 244  	}
 245  	// For small edge counts, enumerate directly.
 246  	// Each edge can be traversed in 2 directions (brush stroke direction),
 247  	// and the edges can be ordered in n! ways, giving 2^n * n! total.
 248  	// But connectivity constraints reduce this significantly.
 249  	// We compute the number of Eulerian-like paths with direction.
 250  	return countDirectedTraversals(edges)
 251  }
 252  
 253  // countDirectedTraversals computes the number of valid directed traversals
 254  // of the given edges. Each edge can be traversed forward or backward
 255  // (2 directions), and the edges can be ordered in n! ways. With pen-lifts
 256  // allowed (any edge can follow any other), the total is n! × 2^n.
 257  //
 258  // This is computed analytically, not by enumeration.
 259  func countDirectedTraversals(edges []Edge) int {
 260  	n := len(edges)
 261  	if n == 0 {
 262  		return 0
 263  	}
 264  	// n! × 2^n: every ordering of n edges, each in 2 directions.
 265  	factorial := 1
 266  	for i := 2; i <= n; i++ {
 267  		factorial *= i
 268  	}
 269  	directions := 1 << n
 270  	return factorial * directions
 271  }
 272  
 273  // RenderSequence is a specific rendering path: an ordered list of directed
 274  // edges that constitute the growth sequence for a symbol.
 275  type RenderSequence struct {
 276  	Projection Projection // the static 6-bit configuration
 277  	Path       Path       // index into valid paths for this projection
 278  	Steps      []Edge     // the ordered edge list (directed)
 279  }
 280  
 281  // FullEncoding combines the static projection (6 bits) with a path index.
 282  // This is the complete encoding of a symbol: structure + angle + sequence.
 283  type FullEncoding struct {
 284  	Proj Projection // 6 bits: vertex (3) + key (3)
 285  	Path Path       // path index within this projection's valid orderings
 286  }
 287  
 288  // Encode packs vertex, key, and path into a FullEncoding.
 289  func Encode(v Vertex, k Key, path Path) FullEncoding {
 290  	return FullEncoding{
 291  		Proj: Pack(v, k),
 292  		Path: path,
 293  	}
 294  }
 295  
 296  // String returns a human-readable representation.
 297  func (k Key) String() string {
 298  	switch k {
 299  	case KeyFaceXY:
 300  		return "FaceXY"
 301  	case KeyFaceXZ:
 302  		return "FaceXZ"
 303  	case KeyFaceYZ:
 304  		return "FaceYZ"
 305  	case KeyEdgeBias:
 306  		return "EdgeBias"
 307  	case KeyVertexA:
 308  		return "VertexA"
 309  	case KeyVertexB:
 310  		return "VertexB"
 311  	case KeyCollapseA:
 312  		return "CollapseA"
 313  	case KeyCollapseB:
 314  		return "CollapseB"
 315  	default:
 316  		return "Invalid"
 317  	}
 318  }
 319  
 320  // String returns a human-readable representation.
 321  func (v Vertex) String() string {
 322  	names := [8]string{
 323  		"Earth(000)", "Thunder(001)", "Water(010)", "Lake(011)",
 324  		"Fire(100)", "Heaven(101)", "Wind(110)", "Mountain(111)",
 325  	}
 326  	if v < 8 {
 327  		return names[v]
 328  	}
 329  	return "Invalid"
 330  }
 331