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