nx_part_solver_lib.nx source
↩ module page · 1095 lines · 49528 B
1// nx_part_solver_lib.nx -- ★CONSTRAINT-BASED PART PLACEMENT (operator method pointer 2026-08-12:
2// arXiv 2406.11824 Infinigen Indoors organizes ROOMS with a constraint DSL + maximal-satisfaction solver;
3// operator transfer: "use similar logic ... to organizing the generated elements of a human").
4// This is rung 1 of that transfer: parts are POINTS with radii; constraints are DECLARATIVE ROWS in a
5// data file (rule 11 -- the anatomy lives in data, never typed coordinates in an emitter); the solver is
6// deterministic integer constraint PROJECTION (fixed pass count, fixed order). The SAME engine is meant
7// to assemble face/organ parts AND, later, furnish rooms and city blocks (one engine, bodies AND worlds).
8//
9// ★RUNG 2 (2026-08-14): EXTENT + ORIENTATION + ENVELOPE AS FIRST-CLASS PARTS -- the rung named from two
10// directions on /world/procgen ("orientation + extent (parts are points today)" and W4 R1 "orientation +
11// extents + envelope as first-class parts"). A part may now carry a BOX and a FACING, and three new
12// constraints consume them, so orientation is SOLVED geometry, not decoration: turning a part swaps its
13// footprint, which changes what it collides with and whether it still fits its envelope.
14//
15// Constraint rows (whitespace-separated, # comments):
16// part <name> <x> <y> <z> <r> declare a part with an INITIAL (possibly garbage) placement
17// extent <a> <ex> <ey> <ez> give a declared part HALF-EXTENTS (a box). Default 0 = a point.
18// facing <a> <fx> <fy> orientation as an integer direction vector in the ground plane,
19// snapped to the nearest cardinal. Default (1,0) = +x.
20// attach <a> <b> <dx> <dy> <dz> hard: pos[a] = pos[b] + (dx,dy,dz)
21// mirror <a> <b> bilateral symmetry about the x=0 plane: ax=-bx, ay=by, az=bz
22// station <a> <lo> <hi> a.z clamped into [lo,hi] (the anatomical station band)
23// clear <a> <b> <mind> centre distance >= mind (no-intersection of POINTS)
24// inside <a> <b> <maxd> centre distance <= maxd (containment; a moves toward b)
25// band <a> <b> <lo> <hi> centre distance inside [lo,hi]
26// ratio <a> <b> <c> <d> <num> <den> [tol_permil] dist(a,b) : dist(c,d) = num:den
27// noclip <a> <b> [gap] BOX no-intersection: the two oriented footprints must be
28// separated by >= gap on some ground axis. Parts whose z-intervals
29// do not overlap are exempt (different storeys never collide).
30// envelope <a> <b> box a lies ENTIRELY INSIDE box b -- the room shell / city block /
31// skull cavity. Only a moves: an envelope is GIVEN, not solved.
32// face <a> <b> a's facing is the cardinal direction toward b (chairs face the
33// table, buildings face the street). Orientation solved from
34// geometry, never typed.
35//
36// ORIENTATION IS EXACT BY CONSTRUCTION, AND THE BOUND IS DECLARED: facing snaps to the four cardinal
37// directions, so a rotated footprint is an exact swap of ex/ey -- no trig, no fixed-point error, and the
38// measured violation is the true violation. Arbitrary-angle boxes need an exact-enough integer rotation
39// contract (the fleet's rc_sin_q14 is a Bhaskara approximation, ~2 permil -- fine for a camera, NOT for a
40// predicate whose whole contract is an exact refusal). That is the NEXT rung, and it is named, not claimed.
41//
42// VERDICT CONTRACT (fail-closed, never fabricate): after PS_PASSES projection passes every constraint's
43// violation is MEASURED; SOLVED only if all violations <= PS_TOL, else INCONSISTENT with the WORST
44// constraint NAMED -- an unsatisfiable set degrades loudly, it never reports a fabricated success.
45// license_tier: ORIGINAL No hw writes (Rule 26).
46import "nx_syscalls.nx"
47
48import "nx_vecmath.nx"
49const PS_MAXP: i64 = 256
50const PS_MAXC: i64 = 512
51const PS_NAMEB: i64 = 32
52const PS_PASSES: i64 = 96
53const PS_TOL: i64 = 2
54const PS_ERRFD: i64 = 2
55const PS_K_ATTACH: i64 = 1
56const PS_K_MIRROR: i64 = 2
57const PS_K_STATION: i64 = 3
58const PS_K_CLEAR: i64 = 4
59const PS_K_INSIDE: i64 = 5
60// ★BEAUTY-AS-PROPORTIONS kinds (operator 2026-08-12): aesthetic canons are RELATIONS BETWEEN DISTANCES
61// (φ-band ratios, feature-spacing bands), so beauty lives in the constraint DATA, never in code.
62const PS_K_RATIO: i64 = 6
63const PS_K_BAND: i64 = 7
64// ★EXTENT/ORIENTATION/ENVELOPE kinds (rung 2): a part with a box and a facing.
65const PS_K_NOCLIP: i64 = 8
66const PS_K_ENVELOPE: i64 = 9
67const PS_K_FACE: i64 = 10
68const PS_RATIO_DEFTOL: i64 = 60
69const PS_E_PARSE: i64 = 3
70const PS_E_INCONSISTENT: i64 = 4
71
72static PS_NP: i64
73static PS_NC: i64
74static PS_PN: i64
75static PS_PX: i64
76static PS_PY: i64
77static PS_PZ: i64
78static PS_PR: i64
79static PS_EX: i64
80static PS_EY: i64
81static PS_EZ: i64
82static PS_FX: i64
83static PS_FY: i64
84static PS_CK: i64
85static PS_CA: i64
86static PS_CB: i64
87static PS_C1: i64
88static PS_C2: i64
89static PS_C3: i64
90static PS_CC: i64
91static PS_CD: i64
92static PS_WORST: i64
93static PS_WORSTV: i64
94static PS_RESID: i64
95
96// ---- PG10 (2026-09-06): SCENE COMPOSITION OVER THE LIVE SOLVER -- one constraint set, no second solver -------
97// ps_scene_compose(b, n) lowers a SCENE grammar onto the rows above and then calls ps_load + ps_solve: the scene
98// is composed BY the part solver, so rooms, trees, water and beings are placed against each other in ONE constraint
99// set and an unsatisfiable scene is refused by the solver's own worst-constraint name. Scene rows (whitespace, #):
100// seed <n> the placement seed (initial positions only; the solver decides the rest)
101// site <ex> <ey> <ez> the world box, centred at the origin -- REQUIRED, exactly one, named `site`
102// water <name> <ex> <ey> <dx> <dy> a water body: a ground slab at a GIVEN offset from the site centre (attach)
103// room <name> <ex> <ey> <ez> a building footprint and height; inside the site; clips nothing; faces water
104// tree <name> <r> a crown; inside the site; clips nothing
105// being <name> <r> inside the site; clips nothing; lives within SC_NEAR of the first room
106// near <a> <b> <lo> <hi> an explicit distance band between two scene parts
107// Lowering: every solid gets `envelope <p> site`; every solid pair gets `noclip a b SC_GAP` (0 against water, a
108// shoreline is walked); rooms get `face <room> <water>`; beings get `band <being> <room> lo hi` with lo derived
109// from the room's footprint and the being's radius. Returns 1 SOLVED, 0 INCONSISTENT (ps_worst names it), negative
110// = a NAMED refusal (-SC_E_PARSE grammar, -SC_E_CAP the pair count would exceed PS_MAXC -- a bound, announced).
111// The composed rows stay readable through ps_scene_rows/ps_scene_len so a gate can prove the composer WROTE rows
112// and the solver SOLVED them -- nothing else moved a part.
113const SC_MAXP: i64 = 64
114const SC_NAMEB: i64 = 24
115const SC_BUFB: i64 = 65536
116const SC_K_SITE: i64 = 1
117const SC_K_WATER: i64 = 2
118const SC_K_ROOM: i64 = 3
119const SC_K_TREE: i64 = 4
120const SC_K_BEING: i64 = 5
121const SC_GAP: i64 = 2 // the pavement between solids
122const SC_NEAR: i64 = 40 // how far from its house a being may wander (the band width)
123const SC_WATER_EZ: i64 = 1 // a water body is a ground slab one unit tall, so ground solids collide with it
124const SC_E_PARSE: i64 = 3
125const SC_E_CAP: i64 = 5
126const SC_LCG_A: i64 = 48271
127const SC_LCG_M: i64 = 2147483647
128const SC_MAXNEAR: i64 = 64
129const SC_ROWB: i64 = 96 // the longest row the composer can write; the buffer guard keeps this headroom
130static SC_NP: i64
131static SC_NAMES: i64
132static SC_KIND: i64
133static SC_A: i64
134static SC_B: i64
135static SC_C: i64
136static SC_D: i64
137static SC_E: i64
138static SC_BUF: i64
139static SC_LEN: i64
140static SC_SEED: i64
141static SC_NEARN: i64
142static SC_NEAR_A: i64
143static SC_NEAR_B: i64
144static SC_NEAR_LO: i64
145static SC_NEAR_HI: i64
146static SC_OVER: i64
147func sc_reset() -> i64 {
148 if SC_NAMES == 0 {
149 SC_NAMES = sys_mmap(SC_MAXP*SC_NAMEB) as i64
150 SC_KIND = sys_mmap(SC_MAXP*8) as i64
151 SC_A = sys_mmap(SC_MAXP*8) as i64
152 SC_B = sys_mmap(SC_MAXP*8) as i64
153 SC_C = sys_mmap(SC_MAXP*8) as i64
154 SC_D = sys_mmap(SC_MAXP*8) as i64
155 SC_E = sys_mmap(SC_MAXP*8) as i64
156 SC_BUF = sys_mmap(SC_BUFB) as i64
157 SC_NEAR_A = sys_mmap(SC_MAXNEAR*8) as i64
158 SC_NEAR_B = sys_mmap(SC_MAXNEAR*8) as i64
159 SC_NEAR_LO = sys_mmap(SC_MAXNEAR*8) as i64
160 SC_NEAR_HI = sys_mmap(SC_MAXNEAR*8) as i64
161 }
162 let nm: *u8 = SC_NAMES as *u8
163 var i: i64 = 0
164 while i < SC_MAXP*SC_NAMEB { nm[i] = 0 as u8; i = i + 1 }
165 SC_NP = 0
166 SC_LEN = 0
167 SC_SEED = 1
168 SC_NEARN = 0
169 SC_OVER = 0
170 return 0
171}
172func sc_name(i: i64) -> *u8 { return ((SC_NAMES as i64) + i*SC_NAMEB) as *u8 }
173func sc_find(b: *u8, ts: i64, tl: i64) -> i64 {
174 var i: i64 = 0
175 while i < SC_NP {
176 let nm: *u8 = sc_name(i)
177 if ps_slen(nm) == tl {
178 var j: i64 = 0
179 var ok: i64 = 1
180 while j < tl { if (nm[j] as i64) != (b[ts+j] as i64) { ok = 0; j = tl } else { j = j + 1 } }
181 if ok == 1 { return i }
182 }
183 i = i + 1
184 }
185 return 0 - 1
186}
187// add a scene part; refuses a duplicate name (two parts wearing one name would silently merge in the solver)
188func sc_add(b: *u8, ts: i64, tl: i64, kind: i64, a: i64, bb: i64, c: i64, d: i64, e: i64) -> i64 {
189 if SC_NP >= SC_MAXP { ps_err("SCENE-REFUSE part capacity\n" as *u8); return 0 - SC_E_CAP }
190 if sc_find(b, ts, tl) >= 0 { ps_err("SCENE-REFUSE duplicate part name\n" as *u8); return 0 - SC_E_PARSE }
191 let dst: *u8 = sc_name(SC_NP)
192 var cl: i64 = tl
193 if cl > SC_NAMEB - 1 { cl = SC_NAMEB - 1 }
194 var q: i64 = 0
195 while q < cl { dst[q] = b[ts+q]; q = q + 1 }
196 dst[cl] = 0 as u8
197 let k: *i64 = SC_KIND as *i64
198 let pa: *i64 = SC_A as *i64
199 let pb: *i64 = SC_B as *i64
200 let pc: *i64 = SC_C as *i64
201 let pd: *i64 = SC_D as *i64
202 let pe: *i64 = SC_E as *i64
203 k[SC_NP] = kind; pa[SC_NP] = a; pb[SC_NP] = bb; pc[SC_NP] = c; pd[SC_NP] = d; pe[SC_NP] = e
204 SC_NP = SC_NP + 1
205 return SC_NP - 1
206}
207func sc_rand() -> i64 {
208 if SC_SEED <= 0 { SC_SEED = 1 }
209 SC_SEED = (SC_SEED * SC_LCG_A) % SC_LCG_M
210 return SC_SEED
211}
212// the row writer: strings and integers into the composed buffer; an overflow is REMEMBERED, never silently cut
213func sc_w(s: *u8) -> i64 {
214 let n: i64 = ps_slen(s)
215 if SC_LEN + n + SC_ROWB > SC_BUFB { SC_OVER = 1; return 0 }
216 let dst: *u8 = SC_BUF as *u8
217 var i: i64 = 0
218 while i < n { dst[SC_LEN + i] = s[i]; i = i + 1 }
219 SC_LEN = SC_LEN + n
220 return 0
221}
222func sc_wn(v: i64) -> i64 {
223 if SC_LEN + 24 + SC_ROWB > SC_BUFB { SC_OVER = 1; return 0 }
224 let dst: *u8 = SC_BUF as *u8
225 var m: i64 = v
226 if m < 0 { dst[SC_LEN] = 45 as u8; SC_LEN = SC_LEN + 1; m = 0 - m }
227 if m == 0 { dst[SC_LEN] = 48 as u8; SC_LEN = SC_LEN + 1; return 0 }
228 let t: *u8 = sys_mmap(32)
229 var k: i64 = 0
230 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
231 var i: i64 = 0
232 while i < k { dst[SC_LEN + i] = t[k - 1 - i]; i = i + 1 }
233 SC_LEN = SC_LEN + k
234 return 0
235}
236func sc_sp() -> i64 { return sc_w(" " as *u8) }
237func sc_nl() -> i64 { return sc_w("\n" as *u8) }
238func sc_kind(i: i64) -> i64 { let k: *i64 = SC_KIND as *i64; return k[i] }
239func sc_a(i: i64) -> i64 { let a: *i64 = SC_A as *i64; return a[i] }
240func sc_b(i: i64) -> i64 { let a: *i64 = SC_B as *i64; return a[i] }
241func sc_c(i: i64) -> i64 { let a: *i64 = SC_C as *i64; return a[i] }
242func sc_d(i: i64) -> i64 { let a: *i64 = SC_D as *i64; return a[i] }
243func sc_e(i: i64) -> i64 { let a: *i64 = SC_E as *i64; return a[i] }
244func sc_first_of(kind: i64) -> i64 {
245 var i: i64 = 0
246 while i < SC_NP { if sc_kind(i) == kind { return i } i = i + 1 }
247 return 0 - 1
248}
249func ps_scene_rows() -> *u8 { return SC_BUF as *u8 }
250func ps_scene_len() -> i64 { return SC_LEN }
251func ps_scene_np() -> i64 { return SC_NP }
252func ps_scene_nc() -> i64 { return PS_NC }
253func ps_scene_compose(b: *u8, n: i64) -> i64 {
254 sc_reset()
255 let tk: *i64 = sys_mmap(16*8) as *i64
256 var p: i64 = 0
257 var site: i64 = 0 - 1
258 while p < n {
259 var e: i64 = p
260 var go: i64 = 1
261 while go == 1 {
262 if e >= n { go = 0 } else { if (b[e] as i64) == 10 { go = 0 } else { e = e + 1 } }
263 }
264 var nt: i64 = ps_ntok(b, p, e, tk)
265 if nt > 0 { if (b[tk[0]] as i64) == 35 { nt = 0 } }
266 if nt > 0 {
267 var hd: i64 = 0
268 if ps_tokeq(b, tk[0], tk[1], "seed" as *u8) == 1 {
269 if nt < 2 { ps_err("SCENE-REFUSE seed row needs a number\n" as *u8); return 0 - SC_E_PARSE }
270 SC_SEED = ps_toknum(b, tk[2], tk[3])
271 hd = 1
272 }
273 if hd == 0 { if ps_tokeq(b, tk[0], tk[1], "site" as *u8) == 1 {
274 if nt < 4 { ps_err("SCENE-REFUSE site row needs ex ey ez\n" as *u8); return 0 - SC_E_PARSE }
275 if site >= 0 { ps_err("SCENE-REFUSE a scene has exactly one site\n" as *u8); return 0 - SC_E_PARSE }
276 let sn: *u8 = "site" as *u8
277 site = sc_add(sn, 0, 4, SC_K_SITE, ps_toknum(b, tk[2], tk[3]), ps_toknum(b, tk[4], tk[5]), ps_toknum(b, tk[6], tk[7]), 0, 0)
278 if site < 0 { return site }
279 hd = 1
280 } }
281 if hd == 0 { if ps_tokeq(b, tk[0], tk[1], "water" as *u8) == 1 {
282 if nt < 6 { ps_err("SCENE-REFUSE water row needs name ex ey dx dy\n" as *u8); return 0 - SC_E_PARSE }
283 let r: i64 = sc_add(b, tk[2], tk[3], SC_K_WATER, ps_toknum(b, tk[4], tk[5]), ps_toknum(b, tk[6], tk[7]), SC_WATER_EZ, ps_toknum(b, tk[8], tk[9]), ps_toknum(b, tk[10], tk[11]))
284 if r < 0 { return r }
285 hd = 1
286 } }
287 if hd == 0 { if ps_tokeq(b, tk[0], tk[1], "room" as *u8) == 1 {
288 if nt < 5 { ps_err("SCENE-REFUSE room row needs name ex ey ez\n" as *u8); return 0 - SC_E_PARSE }
289 let r: i64 = sc_add(b, tk[2], tk[3], SC_K_ROOM, ps_toknum(b, tk[4], tk[5]), ps_toknum(b, tk[6], tk[7]), ps_toknum(b, tk[8], tk[9]), 0, 0)
290 if r < 0 { return r }
291 hd = 1
292 } }
293 if hd == 0 { if ps_tokeq(b, tk[0], tk[1], "tree" as *u8) == 1 {
294 if nt < 3 { ps_err("SCENE-REFUSE tree row needs name r\n" as *u8); return 0 - SC_E_PARSE }
295 let rr: i64 = ps_toknum(b, tk[4], tk[5])
296 let r: i64 = sc_add(b, tk[2], tk[3], SC_K_TREE, rr, rr, rr, 0, 0)
297 if r < 0 { return r }
298 hd = 1
299 } }
300 if hd == 0 { if ps_tokeq(b, tk[0], tk[1], "being" as *u8) == 1 {
301 if nt < 3 { ps_err("SCENE-REFUSE being row needs name r\n" as *u8); return 0 - SC_E_PARSE }
302 let rr: i64 = ps_toknum(b, tk[4], tk[5])
303 let r: i64 = sc_add(b, tk[2], tk[3], SC_K_BEING, rr, rr, rr, 0, 0)
304 if r < 0 { return r }
305 hd = 1
306 } }
307 if hd == 0 { if ps_tokeq(b, tk[0], tk[1], "near" as *u8) == 1 {
308 if nt < 5 { ps_err("SCENE-REFUSE near row needs a b lo hi\n" as *u8); return 0 - SC_E_PARSE }
309 let ia: i64 = sc_find(b, tk[2], tk[3])
310 let ib: i64 = sc_find(b, tk[4], tk[5])
311 if ia < 0 { ps_err("SCENE-REFUSE near names an undeclared part\n" as *u8); return 0 - SC_E_PARSE }
312 if ib < 0 { ps_err("SCENE-REFUSE near names an undeclared part\n" as *u8); return 0 - SC_E_PARSE }
313 if SC_NEARN >= SC_MAXNEAR { ps_err("SCENE-REFUSE near capacity\n" as *u8); return 0 - SC_E_CAP }
314 let na: *i64 = SC_NEAR_A as *i64
315 let nb: *i64 = SC_NEAR_B as *i64
316 let nlo: *i64 = SC_NEAR_LO as *i64
317 let nhi: *i64 = SC_NEAR_HI as *i64
318 na[SC_NEARN] = ia; nb[SC_NEARN] = ib; nlo[SC_NEARN] = ps_toknum(b, tk[6], tk[7]); nhi[SC_NEARN] = ps_toknum(b, tk[8], tk[9])
319 SC_NEARN = SC_NEARN + 1
320 hd = 1
321 } }
322 if hd == 0 { ps_err("SCENE-REFUSE unknown scene row keyword\n" as *u8); return 0 - SC_E_PARSE }
323 }
324 p = e + 1
325 }
326 if site < 0 { ps_err("SCENE-REFUSE a scene needs a site row\n" as *u8); return 0 - SC_E_PARSE }
327 // the bound, announced: rows = parts*3 + pairs + rooms + beings + near, against the solver's PS_MAXC
328 let ns: i64 = SC_NP - 1
329 let pairs: i64 = ns*(ns-1)/2
330 var rooms: i64 = 0
331 var beings: i64 = 0
332 var i: i64 = 0
333 while i < SC_NP { if sc_kind(i) == SC_K_ROOM { rooms = rooms + 1 } if sc_kind(i) == SC_K_BEING { beings = beings + 1 } i = i + 1 }
334 let rows: i64 = SC_NP*3 + pairs + rooms + beings + SC_NEARN
335 if rows > PS_MAXC { ps_err("SCENE-REFUSE constraint capacity: the pairwise no-clip set exceeds PS_MAXC -- fewer solids, or a bigger solver\n" as *u8); return 0 - SC_E_CAP }
336 // emit: the site, then every part with its envelope, then the pairwise no-clip set, faces, bands
337 let sx: i64 = sc_a(site)
338 let sy: i64 = sc_b(site)
339 sc_w("part site 0 0 0 0\n" as *u8)
340 sc_w("extent site " as *u8); sc_wn(sx); sc_sp(); sc_wn(sy); sc_sp(); sc_wn(sc_c(site)); sc_nl()
341 i = 0
342 while i < SC_NP {
343 if i != site {
344 let k: i64 = sc_kind(i)
345 if k == SC_K_WATER {
346 sc_w("part " as *u8); sc_w(sc_name(i)); sc_sp(); sc_wn(sc_d(i)); sc_sp(); sc_wn(sc_e(i)); sc_w(" 0 0\n" as *u8)
347 sc_w("extent " as *u8); sc_w(sc_name(i)); sc_sp(); sc_wn(sc_a(i)); sc_sp(); sc_wn(sc_b(i)); sc_sp(); sc_wn(SC_WATER_EZ); sc_nl()
348 sc_w("attach " as *u8); sc_w(sc_name(i)); sc_w(" site " as *u8); sc_wn(sc_d(i)); sc_sp(); sc_wn(sc_e(i)); sc_w(" 0\n" as *u8)
349 } else {
350 let x0: i64 = sc_rand() % (2*sx + 1) - sx
351 let y0: i64 = sc_rand() % (2*sy + 1) - sy
352 var pr: i64 = 0
353 if k != SC_K_ROOM { pr = sc_a(i) }
354 sc_w("part " as *u8); sc_w(sc_name(i)); sc_sp(); sc_wn(x0); sc_sp(); sc_wn(y0); sc_w(" 0 " as *u8); sc_wn(pr); sc_nl()
355 sc_w("extent " as *u8); sc_w(sc_name(i)); sc_sp(); sc_wn(sc_a(i)); sc_sp(); sc_wn(sc_b(i)); sc_sp(); sc_wn(sc_c(i)); sc_nl()
356 sc_w("envelope " as *u8); sc_w(sc_name(i)); sc_w(" site\n" as *u8)
357 }
358 }
359 i = i + 1
360 }
361 i = 0
362 while i < SC_NP {
363 var j: i64 = i + 1
364 while j < SC_NP {
365 if i != site { if j != site {
366 var gap: i64 = SC_GAP
367 if sc_kind(i) == SC_K_WATER { gap = 0 }
368 if sc_kind(j) == SC_K_WATER { gap = 0 }
369 sc_w("noclip " as *u8); sc_w(sc_name(i)); sc_sp(); sc_w(sc_name(j)); sc_sp(); sc_wn(gap); sc_nl()
370 } }
371 j = j + 1
372 }
373 i = i + 1
374 }
375 let water: i64 = sc_first_of(SC_K_WATER)
376 let home: i64 = sc_first_of(SC_K_ROOM)
377 i = 0
378 while i < SC_NP {
379 if sc_kind(i) == SC_K_ROOM { if water >= 0 {
380 sc_w("face " as *u8); sc_w(sc_name(i)); sc_sp(); sc_w(sc_name(water)); sc_nl()
381 } }
382 if sc_kind(i) == SC_K_BEING { if home >= 0 {
383 var lo: i64 = sc_a(home)
384 if sc_b(home) > lo { lo = sc_b(home) }
385 lo = lo + sc_a(i) + SC_GAP
386 sc_w("band " as *u8); sc_w(sc_name(i)); sc_sp(); sc_w(sc_name(home)); sc_sp(); sc_wn(lo); sc_sp(); sc_wn(lo + SC_NEAR); sc_nl()
387 } }
388 i = i + 1
389 }
390 let na: *i64 = SC_NEAR_A as *i64
391 let nb: *i64 = SC_NEAR_B as *i64
392 let nlo: *i64 = SC_NEAR_LO as *i64
393 let nhi: *i64 = SC_NEAR_HI as *i64
394 i = 0
395 while i < SC_NEARN {
396 sc_w("band " as *u8); sc_w(sc_name(na[i])); sc_sp(); sc_w(sc_name(nb[i])); sc_sp(); sc_wn(nlo[i]); sc_sp(); sc_wn(nhi[i]); sc_nl()
397 i = i + 1
398 }
399 if SC_OVER == 1 { ps_err("SCENE-REFUSE composed rows exceed the buffer\n" as *u8); return 0 - SC_E_CAP }
400 let rc: i64 = ps_load(SC_BUF as *u8, SC_LEN)
401 if rc != 0 { return rc }
402 return ps_solve()
403}
404
405func ps_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
406func ps_err(s: *u8) -> i64 { sys_write(PS_ERRFD, s, ps_slen(s)); return 0 }
407func ps_out(s: *u8) -> i64 { sys_write(1, s, ps_slen(s)); return 0 }
408func ps_outn(v: i64) -> i64 {
409 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
410 var m: i64 = v
411 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
412 let t: *u8 = sys_mmap(32)
413 var k: i64 = 0
414 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
415 let o: *u8 = sys_mmap(32)
416 var i: i64 = 0
417 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
418 sys_write(1, o, k)
419 return 0
420}
421// RETIRED ONTO THE SHARED OWNER 2026-08-24: the eighth private copy of the same Newton floor-sqrt found in one
422// session; vm_isqrt is gate-proven exact over 20,000 inputs and the alias keeps every call site untouched.
423func ps_isqrt(v: i64) -> i64 { return vm_isqrt(v) }
424func ps_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
425
426// ---- cardinal snap: an integer direction vector -> one of (1,0) (0,1) (-1,0) (0,-1). EXACT, no trig. ----
427func ps_cardx(rx: i64, ry: i64) -> i64 {
428 if ps_abs(rx) >= ps_abs(ry) { if rx < 0 { return 0 - 1 } return 1 }
429 return 0
430}
431func ps_cardy(rx: i64, ry: i64) -> i64 {
432 if ps_abs(rx) >= ps_abs(ry) { return 0 }
433 if ry < 0 { return 0 - 1 }
434 return 1
435}
436
437func ps_reset() -> i64 {
438 if PS_PN == 0 {
439 PS_PN = sys_mmap(PS_MAXP*PS_NAMEB) as i64
440 PS_PX = sys_mmap(PS_MAXP*8) as i64
441 PS_PY = sys_mmap(PS_MAXP*8) as i64
442 PS_PZ = sys_mmap(PS_MAXP*8) as i64
443 PS_PR = sys_mmap(PS_MAXP*8) as i64
444 PS_EX = sys_mmap(PS_MAXP*8) as i64
445 PS_EY = sys_mmap(PS_MAXP*8) as i64
446 PS_EZ = sys_mmap(PS_MAXP*8) as i64
447 PS_FX = sys_mmap(PS_MAXP*8) as i64
448 PS_FY = sys_mmap(PS_MAXP*8) as i64
449 PS_CK = sys_mmap(PS_MAXC*8) as i64
450 PS_CA = sys_mmap(PS_MAXC*8) as i64
451 PS_CB = sys_mmap(PS_MAXC*8) as i64
452 PS_C1 = sys_mmap(PS_MAXC*8) as i64
453 PS_C2 = sys_mmap(PS_MAXC*8) as i64
454 PS_C3 = sys_mmap(PS_MAXC*8) as i64
455 PS_CC = sys_mmap(PS_MAXC*8) as i64
456 PS_CD = sys_mmap(PS_MAXC*8) as i64
457 }
458 let pn: *u8 = PS_PN as *u8
459 var i: i64 = 0
460 while i < PS_MAXP*PS_NAMEB { pn[i] = 0 as u8; i = i + 1 }
461 PS_NP = 0
462 PS_NC = 0
463 PS_WORST = 0 - 1
464 PS_WORSTV = 0
465 PS_RESID = 0
466 return 0
467}
468func ps_np() -> i64 { return PS_NP }
469func ps_x(i: i64) -> i64 { let a: *i64 = PS_PX as *i64; return a[i] }
470func ps_y(i: i64) -> i64 { let a: *i64 = PS_PY as *i64; return a[i] }
471func ps_z(i: i64) -> i64 { let a: *i64 = PS_PZ as *i64; return a[i] }
472func ps_r(i: i64) -> i64 { let a: *i64 = PS_PR as *i64; return a[i] }
473func ps_ex(i: i64) -> i64 { let a: *i64 = PS_EX as *i64; return a[i] }
474func ps_ey(i: i64) -> i64 { let a: *i64 = PS_EY as *i64; return a[i] }
475func ps_ez(i: i64) -> i64 { let a: *i64 = PS_EZ as *i64; return a[i] }
476func ps_fx(i: i64) -> i64 { let a: *i64 = PS_FX as *i64; return a[i] }
477func ps_fy(i: i64) -> i64 { let a: *i64 = PS_FY as *i64; return a[i] }
478func ps_name(i: i64) -> *u8 { return ((PS_PN as i64) + i*PS_NAMEB) as *u8 }
479// EFFECTIVE half-extents: turning a part 90 degrees swaps its ground footprint. This is the whole point
480// of orientation being first-class -- a facing that did not change the footprint would be decoration.
481func ps_exx(i: i64) -> i64 { let f: *i64 = PS_FY as *i64; if f[i] != 0 { return ps_ey(i) } return ps_ex(i) }
482func ps_eyy(i: i64) -> i64 { let f: *i64 = PS_FY as *i64; if f[i] != 0 { return ps_ex(i) } return ps_ey(i) }
483func ps_worst() -> i64 { return PS_WORST }
484func ps_worstv() -> i64 { return PS_WORSTV }
485func ps_resid() -> i64 { return PS_RESID }
486func ps_kindname(k: i64) -> *u8 {
487 if k == PS_K_ATTACH { return "attach" as *u8 }
488 if k == PS_K_MIRROR { return "mirror" as *u8 }
489 if k == PS_K_STATION { return "station" as *u8 }
490 if k == PS_K_CLEAR { return "clear" as *u8 }
491 if k == PS_K_INSIDE { return "inside" as *u8 }
492 if k == PS_K_RATIO { return "ratio" as *u8 }
493 if k == PS_K_BAND { return "band" as *u8 }
494 if k == PS_K_NOCLIP { return "noclip" as *u8 }
495 if k == PS_K_ENVELOPE { return "envelope" as *u8 }
496 if k == PS_K_FACE { return "face" as *u8 }
497 return "unknown" as *u8
498}
499func ps_find(name: *u8) -> i64 {
500 var i: i64 = 0
501 while i < PS_NP {
502 let pn: *u8 = ps_name(i)
503 var j: i64 = 0
504 var eq: i64 = 0 - 1
505 while eq < 0 {
506 let ca: i64 = pn[j] as i64
507 let cb: i64 = name[j] as i64
508 if ca != cb { eq = 0 } else { if ca == 0 { eq = 1 } else { j = j + 1 } }
509 }
510 if eq == 1 { return i }
511 i = i + 1
512 }
513 return 0 - 1
514}
515
516// ---- tokenizer: tokens in [s,e), out pairs (start,len) up to 8; returns count ----
517func ps_ntok(b: *u8, s: i64, e: i64, out: *i64) -> i64 {
518 var n: i64 = 0
519 var p: i64 = s
520 while p < e {
521 var c: i64 = b[p] as i64
522 var issp: i64 = 0
523 if c == 32 { issp = 1 }
524 if c == 9 { issp = 1 }
525 if c == 13 { issp = 1 }
526 if issp == 1 { p = p + 1 } else {
527 let ts: i64 = p
528 var go: i64 = 1
529 while go == 1 {
530 if p >= e { go = 0 } else {
531 let c2: i64 = b[p] as i64
532 var sp2: i64 = 0
533 if c2 == 32 { sp2 = 1 }
534 if c2 == 9 { sp2 = 1 }
535 if c2 == 13 { sp2 = 1 }
536 if sp2 == 1 { go = 0 } else { p = p + 1 }
537 }
538 }
539 if n < 8 { out[n*2] = ts; out[n*2+1] = p - ts; n = n + 1 }
540 }
541 }
542 return n
543}
544func ps_tokeq(b: *u8, ts: i64, tl: i64, kw: *u8) -> i64 {
545 if ps_slen(kw) != tl { return 0 }
546 var i: i64 = 0
547 while i < tl { if (b[ts+i] as i64) != (kw[i] as i64) { return 0 } i = i + 1 }
548 return 1
549}
550func ps_toknum(b: *u8, ts: i64, tl: i64) -> i64 {
551 var v: i64 = 0
552 var i: i64 = 0
553 var ng: i64 = 0
554 if tl > 0 { if (b[ts] as i64) == 45 { ng = 1; i = 1 } }
555 while i < tl {
556 let c: i64 = b[ts+i] as i64
557 if c >= 48 { if c <= 57 { v = v*10 + (c - 48) } }
558 i = i + 1
559 }
560 if ng == 1 { return 0 - v }
561 return v
562}
563func ps_tokpart(b: *u8, ts: i64, tl: i64) -> i64 {
564 var i: i64 = 0
565 while i < PS_NP {
566 let pn: *u8 = ps_name(i)
567 if ps_slen(pn) == tl {
568 var j: i64 = 0
569 var ok: i64 = 1
570 while j < tl { if (pn[j] as i64) != (b[ts+j] as i64) { ok = 0; j = tl } else { j = j + 1 } }
571 if ok == 1 { return i }
572 }
573 i = i + 1
574 }
575 return 0 - 1
576}
577
578// parse the constraint file. 0 ok, negative = named parse refusal (printed with the line number).
579func ps_load(b: *u8, n: i64) -> i64 {
580 ps_reset()
581 let tk: *i64 = sys_mmap(16*8) as *i64
582 let px: *i64 = PS_PX as *i64
583 let py: *i64 = PS_PY as *i64
584 let pz: *i64 = PS_PZ as *i64
585 let prr: *i64 = PS_PR as *i64
586 let pex: *i64 = PS_EX as *i64
587 let pey: *i64 = PS_EY as *i64
588 let pez: *i64 = PS_EZ as *i64
589 let pfx: *i64 = PS_FX as *i64
590 let pfy: *i64 = PS_FY as *i64
591 let ck: *i64 = PS_CK as *i64
592 let ca: *i64 = PS_CA as *i64
593 let cb: *i64 = PS_CB as *i64
594 let c1: *i64 = PS_C1 as *i64
595 let c2: *i64 = PS_C2 as *i64
596 let c3: *i64 = PS_C3 as *i64
597 let ccv: *i64 = PS_CC as *i64
598 let cdv: *i64 = PS_CD as *i64
599 var p: i64 = 0
600 var ln: i64 = 0
601 while p < n {
602 ln = ln + 1
603 var e: i64 = p
604 var go: i64 = 1
605 while go == 1 {
606 if e >= n { go = 0 } else { if (b[e] as i64) == 10 { go = 0 } else { e = e + 1 } }
607 }
608 var nt: i64 = ps_ntok(b, p, e, tk)
609 if nt > 0 { if (b[tk[0]] as i64) == 35 { nt = 0 } }
610 if nt > 0 {
611 var hd: i64 = 0
612 if ps_tokeq(b, tk[0], tk[1], "part" as *u8) == 1 {
613 if nt < 6 { ps_err("PART-SOLVER-REFUSE part row needs name x y z r\n" as *u8); return 0 - PS_E_PARSE }
614 if PS_NP >= PS_MAXP { ps_err("PART-SOLVER-REFUSE part capacity\n" as *u8); return 0 - PS_E_PARSE }
615 let dst: *u8 = ps_name(PS_NP)
616 var cl: i64 = tk[3]
617 if cl > PS_NAMEB - 1 { cl = PS_NAMEB - 1 }
618 var q: i64 = 0
619 while q < cl { dst[q] = b[tk[2]+q]; q = q + 1 }
620 dst[cl] = 0 as u8
621 px[PS_NP] = ps_toknum(b, tk[4], tk[5])
622 py[PS_NP] = ps_toknum(b, tk[6], tk[7])
623 pz[PS_NP] = ps_toknum(b, tk[8], tk[9])
624 prr[PS_NP] = ps_toknum(b, tk[10], tk[11])
625 // a part is a POINT facing +x until an extent/facing row says otherwise -- so every
626 // constraint file written before rung 2 loads and solves byte-identically.
627 pex[PS_NP] = 0
628 pey[PS_NP] = 0
629 pez[PS_NP] = 0
630 pfx[PS_NP] = 1
631 pfy[PS_NP] = 0
632 PS_NP = PS_NP + 1
633 hd = 1
634 }
635 if hd == 0 { if ps_tokeq(b, tk[0], tk[1], "extent" as *u8) == 1 {
636 if nt < 5 { ps_err("PART-SOLVER-REFUSE extent row needs part ex ey ez\n" as *u8); return 0 - PS_E_PARSE }
637 let ie: i64 = ps_tokpart(b, tk[2], tk[3])
638 if ie < 0 { ps_err("PART-SOLVER-REFUSE extent names an undeclared part\n" as *u8); return 0 - PS_E_PARSE }
639 let vex: i64 = ps_toknum(b, tk[4], tk[5])
640 let vey: i64 = ps_toknum(b, tk[6], tk[7])
641 let vez: i64 = ps_toknum(b, tk[8], tk[9])
642 if vex < 0 { ps_err("PART-SOLVER-REFUSE extent half-sizes must not be negative\n" as *u8); return 0 - PS_E_PARSE }
643 if vey < 0 { ps_err("PART-SOLVER-REFUSE extent half-sizes must not be negative\n" as *u8); return 0 - PS_E_PARSE }
644 if vez < 0 { ps_err("PART-SOLVER-REFUSE extent half-sizes must not be negative\n" as *u8); return 0 - PS_E_PARSE }
645 pex[ie] = vex
646 pey[ie] = vey
647 pez[ie] = vez
648 hd = 1
649 } }
650 if hd == 0 { if ps_tokeq(b, tk[0], tk[1], "facing" as *u8) == 1 {
651 if nt < 4 { ps_err("PART-SOLVER-REFUSE facing row needs part fx fy\n" as *u8); return 0 - PS_E_PARSE }
652 let ifc: i64 = ps_tokpart(b, tk[2], tk[3])
653 if ifc < 0 { ps_err("PART-SOLVER-REFUSE facing names an undeclared part\n" as *u8); return 0 - PS_E_PARSE }
654 let rfx: i64 = ps_toknum(b, tk[4], tk[5])
655 let rfy: i64 = ps_toknum(b, tk[6], tk[7])
656 if rfx == 0 { if rfy == 0 { ps_err("PART-SOLVER-REFUSE facing vector is zero -- a direction of nothing is not a direction\n" as *u8); return 0 - PS_E_PARSE } }
657 pfx[ifc] = ps_cardx(rfx, rfy)
658 pfy[ifc] = ps_cardy(rfx, rfy)
659 hd = 1
660 } }
661 if hd == 0 {
662 var kind: i64 = 0
663 if ps_tokeq(b, tk[0], tk[1], "attach" as *u8) == 1 { kind = PS_K_ATTACH }
664 if ps_tokeq(b, tk[0], tk[1], "mirror" as *u8) == 1 { kind = PS_K_MIRROR }
665 if ps_tokeq(b, tk[0], tk[1], "station" as *u8) == 1 { kind = PS_K_STATION }
666 if ps_tokeq(b, tk[0], tk[1], "clear" as *u8) == 1 { kind = PS_K_CLEAR }
667 if ps_tokeq(b, tk[0], tk[1], "inside" as *u8) == 1 { kind = PS_K_INSIDE }
668 if ps_tokeq(b, tk[0], tk[1], "ratio" as *u8) == 1 { kind = PS_K_RATIO }
669 if ps_tokeq(b, tk[0], tk[1], "band" as *u8) == 1 { kind = PS_K_BAND }
670 if ps_tokeq(b, tk[0], tk[1], "noclip" as *u8) == 1 { kind = PS_K_NOCLIP }
671 if ps_tokeq(b, tk[0], tk[1], "envelope" as *u8) == 1 { kind = PS_K_ENVELOPE }
672 if ps_tokeq(b, tk[0], tk[1], "face" as *u8) == 1 { kind = PS_K_FACE }
673 if kind == 0 { ps_err("PART-SOLVER-REFUSE unknown row keyword\n" as *u8); return 0 - PS_E_PARSE }
674 if PS_NC >= PS_MAXC { ps_err("PART-SOLVER-REFUSE constraint capacity\n" as *u8); return 0 - PS_E_PARSE }
675 let ia: i64 = ps_tokpart(b, tk[2], tk[3])
676 if ia < 0 { ps_err("PART-SOLVER-REFUSE constraint names an undeclared part\n" as *u8); return 0 - PS_E_PARSE }
677 var ib: i64 = 0 - 1
678 var ic: i64 = 0 - 1
679 var id2: i64 = 0 - 1
680 var v1: i64 = 0
681 var v2: i64 = 0
682 var v3: i64 = 0
683 if kind == PS_K_STATION {
684 if nt < 4 { ps_err("PART-SOLVER-REFUSE station row needs part lo hi\n" as *u8); return 0 - PS_E_PARSE }
685 v1 = ps_toknum(b, tk[4], tk[5])
686 v2 = ps_toknum(b, tk[6], tk[7])
687 } else {
688 if nt < 3 { ps_err("PART-SOLVER-REFUSE row needs two parts\n" as *u8); return 0 - PS_E_PARSE }
689 ib = ps_tokpart(b, tk[4], tk[5])
690 if ib < 0 { ps_err("PART-SOLVER-REFUSE constraint names an undeclared part\n" as *u8); return 0 - PS_E_PARSE }
691 if kind == PS_K_ATTACH {
692 if nt < 6 { ps_err("PART-SOLVER-REFUSE attach row needs a b dx dy dz\n" as *u8); return 0 - PS_E_PARSE }
693 v1 = ps_toknum(b, tk[6], tk[7])
694 v2 = ps_toknum(b, tk[8], tk[9])
695 v3 = ps_toknum(b, tk[10], tk[11])
696 }
697 if kind == PS_K_CLEAR { if nt < 4 { ps_err("PART-SOLVER-REFUSE clear row needs a b mind\n" as *u8); return 0 - PS_E_PARSE } }
698 if kind == PS_K_CLEAR { v1 = ps_toknum(b, tk[6], tk[7]) }
699 if kind == PS_K_INSIDE { if nt < 4 { ps_err("PART-SOLVER-REFUSE inside row needs a b maxd\n" as *u8); return 0 - PS_E_PARSE } }
700 if kind == PS_K_INSIDE { v1 = ps_toknum(b, tk[6], tk[7]) }
701 // noclip's gap is OPTIONAL and defaults to 0 (touching boxes are separated). A gap is
702 // the door-swing / fire-lane / pavement -- clearance is DATA, never a hidden constant.
703 if kind == PS_K_NOCLIP { if nt >= 4 { v1 = ps_toknum(b, tk[6], tk[7]) } }
704 if kind == PS_K_NOCLIP { if v1 < 0 { ps_err("PART-SOLVER-REFUSE noclip gap must not be negative\n" as *u8); return 0 - PS_E_PARSE } }
705 if kind == PS_K_BAND {
706 if nt < 5 { ps_err("PART-SOLVER-REFUSE band row needs a b lo hi\n" as *u8); return 0 - PS_E_PARSE }
707 v1 = ps_toknum(b, tk[6], tk[7])
708 v2 = ps_toknum(b, tk[8], tk[9])
709 }
710 if kind == PS_K_RATIO {
711 // ratio a b c d num den [tol_permil]: dist(a,b) : dist(c,d) = num:den within tol
712 // (c,d) is the REFERENCE pair -- only (a,b) is adjusted. Deterministic by construction.
713 if nt < 7 { ps_err("PART-SOLVER-REFUSE ratio row needs a b c d num den [tol]\n" as *u8); return 0 - PS_E_PARSE }
714 ic = ps_tokpart(b, tk[6], tk[7])
715 id2 = ps_tokpart(b, tk[8], tk[9])
716 if ic < 0 { ps_err("PART-SOLVER-REFUSE constraint names an undeclared part\n" as *u8); return 0 - PS_E_PARSE }
717 if id2 < 0 { ps_err("PART-SOLVER-REFUSE constraint names an undeclared part\n" as *u8); return 0 - PS_E_PARSE }
718 v1 = ps_toknum(b, tk[10], tk[11])
719 v2 = ps_toknum(b, tk[12], tk[13])
720 v3 = PS_RATIO_DEFTOL
721 if nt >= 8 { v3 = ps_toknum(b, tk[14], tk[15]) }
722 if v1 < 1 { ps_err("PART-SOLVER-REFUSE ratio num must be positive\n" as *u8); return 0 - PS_E_PARSE }
723 if v2 < 1 { ps_err("PART-SOLVER-REFUSE ratio den must be positive\n" as *u8); return 0 - PS_E_PARSE }
724 }
725 }
726 ck[PS_NC] = kind
727 ca[PS_NC] = ia
728 cb[PS_NC] = ib
729 ccv[PS_NC] = ic
730 cdv[PS_NC] = id2
731 c1[PS_NC] = v1
732 c2[PS_NC] = v2
733 c3[PS_NC] = v3
734 PS_NC = PS_NC + 1
735 }
736 }
737 p = e + 1
738 }
739 if PS_NP < 1 { ps_err("PART-SOLVER-REFUSE no parts declared\n" as *u8); return 0 - PS_E_PARSE }
740 return 0
741}
742
743// do the z intervals of two parts overlap? Parts on different storeys never collide in the ground plane.
744// Non-strict on purpose: two zero-height parts on the same floor DO share their plane.
745func ps_zoverlap(a: i64, b2: i64) -> i64 {
746 let pz: *i64 = PS_PZ as *i64
747 if ps_abs(pz[a] - pz[b2]) > ps_ez(a) + ps_ez(b2) { return 0 }
748 return 1
749}
750
751func ps_apply(ci: i64) -> i64 {
752 let px: *i64 = PS_PX as *i64
753 let py: *i64 = PS_PY as *i64
754 let pz: *i64 = PS_PZ as *i64
755 let pfx: *i64 = PS_FX as *i64
756 let pfy: *i64 = PS_FY as *i64
757 let ck: *i64 = PS_CK as *i64
758 let cav: *i64 = PS_CA as *i64
759 let cbv: *i64 = PS_CB as *i64
760 let c1: *i64 = PS_C1 as *i64
761 let c2: *i64 = PS_C2 as *i64
762 let c3: *i64 = PS_C3 as *i64
763 let k: i64 = ck[ci]
764 let a: i64 = cav[ci]
765 let b2: i64 = cbv[ci]
766 if k == PS_K_ATTACH {
767 px[a] = px[b2] + c1[ci]
768 py[a] = py[b2] + c2[ci]
769 pz[a] = pz[b2] + c3[ci]
770 }
771 if k == PS_K_MIRROR {
772 let sx: i64 = px[a] + px[b2]
773 px[a] = px[a] - sx/2
774 px[b2] = px[b2] - (sx - sx/2)
775 let my: i64 = (py[a] + py[b2])/2
776 py[a] = my
777 py[b2] = my
778 let mz: i64 = (pz[a] + pz[b2])/2
779 pz[a] = mz
780 pz[b2] = mz
781 }
782 if k == PS_K_STATION {
783 if pz[a] < c1[ci] { pz[a] = c1[ci] }
784 if pz[a] > c2[ci] { pz[a] = c2[ci] }
785 }
786 if k == PS_K_CLEAR {
787 let dx: i64 = px[a] - px[b2]
788 let dy: i64 = py[a] - py[b2]
789 let dz: i64 = pz[a] - pz[b2]
790 let d: i64 = ps_isqrt(dx*dx + dy*dy + dz*dz)
791 if d == 0 {
792 px[a] = px[a] + c1[ci]/2 + 1
793 px[b2] = px[b2] - c1[ci]/2 - 1
794 } else { if d < c1[ci] {
795 let push: i64 = c1[ci] - d
796 var mx: i64 = dx*push/(2*d)
797 var my2: i64 = dy*push/(2*d)
798 var mz2: i64 = dz*push/(2*d)
799 if mx == 0 { if my2 == 0 { if mz2 == 0 { mx = 1 } } }
800 px[a] = px[a] + mx
801 py[a] = py[a] + my2
802 pz[a] = pz[a] + mz2
803 px[b2] = px[b2] - mx
804 py[b2] = py[b2] - my2
805 pz[b2] = pz[b2] - mz2
806 } }
807 }
808 if k == PS_K_INSIDE {
809 let dx2: i64 = px[a] - px[b2]
810 let dy3: i64 = py[a] - py[b2]
811 let dz3: i64 = pz[a] - pz[b2]
812 let d2: i64 = ps_isqrt(dx2*dx2 + dy3*dy3 + dz3*dz3)
813 if d2 > c1[ci] { if d2 > 0 {
814 px[a] = px[b2] + dx2*c1[ci]/d2
815 py[a] = py[b2] + dy3*c1[ci]/d2
816 pz[a] = pz[b2] + dz3*c1[ci]/d2
817 } }
818 }
819 if k == PS_K_BAND {
820 let bdx: i64 = px[a] - px[b2]
821 let bdy: i64 = py[a] - py[b2]
822 let bdz: i64 = pz[a] - pz[b2]
823 let bd: i64 = ps_isqrt(bdx*bdx + bdy*bdy + bdz*bdz)
824 var tgt: i64 = 0 - 1
825 if bd < c1[ci] { tgt = c1[ci] }
826 if bd > c2[ci] { tgt = c2[ci] }
827 if tgt >= 0 {
828 if bd == 0 {
829 px[a] = px[a] + tgt/2 + 1
830 px[b2] = px[b2] - tgt/2 - 1
831 } else {
832 let dif: i64 = tgt - bd
833 var mx: i64 = bdx*dif/(2*bd)
834 var my3: i64 = bdy*dif/(2*bd)
835 var mz3: i64 = bdz*dif/(2*bd)
836 if mx == 0 { if my3 == 0 { if mz3 == 0 { if dif > 0 { mx = 1 } else { mx = 0 - 1 } } } }
837 px[a] = px[a] + mx
838 py[a] = py[a] + my3
839 pz[a] = pz[a] + mz3
840 px[b2] = px[b2] - mx
841 py[b2] = py[b2] - my3
842 pz[b2] = pz[b2] - mz3
843 }
844 }
845 }
846 if k == PS_K_RATIO {
847 let ccv2: *i64 = PS_CC as *i64
848 let cdv2: *i64 = PS_CD as *i64
849 let c: i64 = ccv2[ci]
850 let d4: i64 = cdv2[ci]
851 let rdx: i64 = px[c] - px[d4]
852 let rdy: i64 = py[c] - py[d4]
853 let rdz: i64 = pz[c] - pz[d4]
854 let dref: i64 = ps_isqrt(rdx*rdx + rdy*rdy + rdz*rdz)
855 let tgt2: i64 = dref*c1[ci]/c2[ci]
856 let adx: i64 = px[a] - px[b2]
857 let ady: i64 = py[a] - py[b2]
858 let adz: i64 = pz[a] - pz[b2]
859 let dab: i64 = ps_isqrt(adx*adx + ady*ady + adz*adz)
860 let slack: i64 = tgt2*c3[ci]/1000 + PS_TOL
861 var dif2: i64 = tgt2 - dab
862 var adif: i64 = dif2
863 if adif < 0 { adif = 0 - adif }
864 if adif > slack {
865 if dab == 0 {
866 px[a] = px[a] + tgt2/2 + 1
867 px[b2] = px[b2] - tgt2/2 - 1
868 } else {
869 var mx2: i64 = adx*dif2/(2*dab)
870 var my4: i64 = ady*dif2/(2*dab)
871 var mz4: i64 = adz*dif2/(2*dab)
872 if mx2 == 0 { if my4 == 0 { if mz4 == 0 { if dif2 > 0 { mx2 = 1 } else { mx2 = 0 - 1 } } } }
873 px[a] = px[a] + mx2
874 py[a] = py[a] + my4
875 pz[a] = pz[a] + mz4
876 px[b2] = px[b2] - mx2
877 py[b2] = py[b2] - my4
878 pz[b2] = pz[b2] - mz4
879 }
880 }
881 }
882 // ★NOCLIP: separate two oriented FOOTPRINTS along the cheapest escape axis. Exempt when the z
883 // intervals miss, so a first-floor flat never pushes a ground-floor shop sideways.
884 if k == PS_K_NOCLIP {
885 if ps_zoverlap(a, b2) == 1 {
886 let ndx: i64 = px[a] - px[b2]
887 let ndy: i64 = py[a] - py[b2]
888 let ox: i64 = (ps_exx(a) + ps_exx(b2) + c1[ci]) - ps_abs(ndx)
889 let oy: i64 = (ps_eyy(a) + ps_eyy(b2) + c1[ci]) - ps_abs(ndy)
890 if ox > 0 { if oy > 0 {
891 if ox <= oy {
892 let mvx: i64 = (ox + 1)/2
893 var sgx: i64 = 1
894 if ndx < 0 { sgx = 0 - 1 }
895 px[a] = px[a] + sgx*mvx
896 px[b2] = px[b2] - sgx*mvx
897 } else {
898 let mvy: i64 = (oy + 1)/2
899 var sgy: i64 = 1
900 if ndy < 0 { sgy = 0 - 1 }
901 py[a] = py[a] + sgy*mvy
902 py[b2] = py[b2] - sgy*mvy
903 }
904 } }
905 }
906 }
907 // ★ENVELOPE: slide a until its box is wholly inside b's box. ONLY a moves -- the room shell / block
908 // boundary is an INPUT (the same lesson the cottage taught: the envelope is given, the contents solved).
909 if k == PS_K_ENVELOPE {
910 let rax: i64 = ps_exx(a)
911 let ray: i64 = ps_eyy(a)
912 let raz: i64 = ps_ez(a)
913 let rbx: i64 = ps_exx(b2)
914 let rby: i64 = ps_eyy(b2)
915 let rbz: i64 = ps_ez(b2)
916 let slx: i64 = rbx - rax
917 if slx < 0 { px[a] = px[b2] } else {
918 if px[a] < px[b2] - slx { px[a] = px[b2] - slx }
919 if px[a] > px[b2] + slx { px[a] = px[b2] + slx }
920 }
921 let sly: i64 = rby - ray
922 if sly < 0 { py[a] = py[b2] } else {
923 if py[a] < py[b2] - sly { py[a] = py[b2] - sly }
924 if py[a] > py[b2] + sly { py[a] = py[b2] + sly }
925 }
926 let slz: i64 = rbz - raz
927 if slz < 0 { pz[a] = pz[b2] } else {
928 if pz[a] < pz[b2] - slz { pz[a] = pz[b2] - slz }
929 if pz[a] > pz[b2] + slz { pz[a] = pz[b2] + slz }
930 }
931 }
932 // ★FACE: orientation SOLVED from geometry. Turning a part swaps its footprint, so this row is
933 // load-bearing on noclip and envelope -- not a label.
934 if k == PS_K_FACE {
935 let fdx: i64 = px[b2] - px[a]
936 let fdy: i64 = py[b2] - py[a]
937 var any: i64 = 0
938 if fdx != 0 { any = 1 }
939 if fdy != 0 { any = 1 }
940 if any == 1 {
941 pfx[a] = ps_cardx(fdx, fdy)
942 pfy[a] = ps_cardy(fdx, fdy)
943 }
944 }
945 return 0
946}
947
948// one constraint's measured violation (0 = satisfied)
949func ps_violation(ci: i64) -> i64 {
950 let px: *i64 = PS_PX as *i64
951 let py: *i64 = PS_PY as *i64
952 let pz: *i64 = PS_PZ as *i64
953 let ck: *i64 = PS_CK as *i64
954 let cav: *i64 = PS_CA as *i64
955 let cbv: *i64 = PS_CB as *i64
956 let c1: *i64 = PS_C1 as *i64
957 let c2: *i64 = PS_C2 as *i64
958 let c3: *i64 = PS_C3 as *i64
959 let k: i64 = ck[ci]
960 let a: i64 = cav[ci]
961 let b2: i64 = cbv[ci]
962 if k == PS_K_ATTACH {
963 return ps_abs(px[a] - (px[b2] + c1[ci])) + ps_abs(py[a] - (py[b2] + c2[ci])) + ps_abs(pz[a] - (pz[b2] + c3[ci]))
964 }
965 if k == PS_K_MIRROR {
966 return ps_abs(px[a] + px[b2]) + ps_abs(py[a] - py[b2]) + ps_abs(pz[a] - pz[b2])
967 }
968 if k == PS_K_STATION {
969 if pz[a] < c1[ci] { return c1[ci] - pz[a] }
970 if pz[a] > c2[ci] { return pz[a] - c2[ci] }
971 return 0
972 }
973 // penetration depth of two footprints = the SHALLOWEST escape. 0 when separated on any ground axis
974 // or when the z intervals miss.
975 if k == PS_K_NOCLIP {
976 if ps_zoverlap(a, b2) == 0 { return 0 }
977 let vox: i64 = (ps_exx(a) + ps_exx(b2) + c1[ci]) - ps_abs(px[a] - px[b2])
978 let voy: i64 = (ps_eyy(a) + ps_eyy(b2) + c1[ci]) - ps_abs(py[a] - py[b2])
979 if vox <= 0 { return 0 }
980 if voy <= 0 { return 0 }
981 if vox <= voy { return vox }
982 return voy
983 }
984 // total overflow of a's box beyond b's faces. 0 when wholly contained.
985 if k == PS_K_ENVELOPE {
986 var ov: i64 = 0
987 let qx: i64 = ps_abs(px[a] - px[b2]) + ps_exx(a) - ps_exx(b2)
988 if qx > 0 { ov = ov + qx }
989 let qy: i64 = ps_abs(py[a] - py[b2]) + ps_eyy(a) - ps_eyy(b2)
990 if qy > 0 { ov = ov + qy }
991 let qz: i64 = ps_abs(pz[a] - pz[b2]) + ps_ez(a) - ps_ez(b2)
992 if qz > 0 { ov = ov + qz }
993 return ov
994 }
995 // orientation error, in milli-cosine, against the BEST cardinal facing toward b: exactly 0 when the
996 // part already points the right way, strictly positive otherwise. Never a fabricated near-miss.
997 if k == PS_K_FACE {
998 let gdx: i64 = px[b2] - px[a]
999 let gdy: i64 = py[b2] - py[a]
1000 let gd: i64 = ps_isqrt(gdx*gdx + gdy*gdy)
1001 if gd == 0 { return 0 }
1002 let bfx: i64 = ps_cardx(gdx, gdy)
1003 let bfy: i64 = ps_cardy(gdx, gdy)
1004 let best: i64 = (bfx*gdx + bfy*gdy)*1000/gd
1005 let have: i64 = (ps_fx(a)*gdx + ps_fy(a)*gdy)*1000/gd
1006 if have >= best { return 0 }
1007 return best - have
1008 }
1009 let dx: i64 = px[a] - px[b2]
1010 let dy: i64 = py[a] - py[b2]
1011 let dz: i64 = pz[a] - pz[b2]
1012 let d: i64 = ps_isqrt(dx*dx + dy*dy + dz*dz)
1013 if k == PS_K_CLEAR { if d < c1[ci] { return c1[ci] - d } return 0 }
1014 if k == PS_K_INSIDE { if d > c1[ci] { return d - c1[ci] } return 0 }
1015 if k == PS_K_BAND {
1016 if d < c1[ci] { return c1[ci] - d }
1017 if d > c2[ci] { return d - c2[ci] }
1018 return 0
1019 }
1020 if k == PS_K_RATIO {
1021 let ccv3: *i64 = PS_CC as *i64
1022 let cdv3: *i64 = PS_CD as *i64
1023 let rdx2: i64 = px[ccv3[ci]] - px[cdv3[ci]]
1024 let rdy2: i64 = py[ccv3[ci]] - py[cdv3[ci]]
1025 let rdz2: i64 = pz[ccv3[ci]] - pz[cdv3[ci]]
1026 let dref2: i64 = ps_isqrt(rdx2*rdx2 + rdy2*rdy2 + rdz2*rdz2)
1027 let tgt3: i64 = dref2*c1[ci]/c2[ci]
1028 var dev: i64 = d - tgt3
1029 if dev < 0 { dev = 0 - dev }
1030 let slack2: i64 = tgt3*c3[ci]/1000
1031 if dev > slack2 { return dev - slack2 }
1032 return 0
1033 }
1034 return 0
1035}
1036
1037// fixed-order fixed-count projection passes, then MEASURE. 1 = SOLVED, 0 = INCONSISTENT (worst named).
1038func ps_solve() -> i64 {
1039 var pass: i64 = 0
1040 while pass < PS_PASSES {
1041 var ci: i64 = 0
1042 while ci < PS_NC { ps_apply(ci); ci = ci + 1 }
1043 pass = pass + 1
1044 }
1045 PS_RESID = 0
1046 PS_WORST = 0 - 1
1047 PS_WORSTV = 0
1048 var ok: i64 = 1
1049 var c: i64 = 0
1050 while c < PS_NC {
1051 let v: i64 = ps_violation(c)
1052 PS_RESID = PS_RESID + v
1053 if v > PS_WORSTV { PS_WORSTV = v; PS_WORST = c }
1054 if v > PS_TOL { ok = 0 }
1055 c = c + 1
1056 }
1057 return ok
1058}
1059func ps_ckind(ci: i64) -> i64 { let ck: *i64 = PS_CK as *i64; return ck[ci] }
1060
1061func ps_dump() -> i64 {
1062 var i: i64 = 0
1063 while i < PS_NP {
1064 ps_out(" part " as *u8)
1065 ps_out(ps_name(i))
1066 ps_out(" " as *u8)
1067 ps_outn(ps_x(i))
1068 ps_out(" " as *u8)
1069 ps_outn(ps_y(i))
1070 ps_out(" " as *u8)
1071 ps_outn(ps_z(i))
1072 ps_out("\n" as *u8)
1073 // the box line appears ONLY for a part that declared one, so every pre-rung-2 constraint file
1074 // still prints byte-identically. A point has nothing to say about its footprint.
1075 if ps_ex(i) + ps_ey(i) + ps_ez(i) > 0 {
1076 ps_out(" box " as *u8)
1077 ps_outn(ps_ex(i))
1078 ps_out(" " as *u8)
1079 ps_outn(ps_ey(i))
1080 ps_out(" " as *u8)
1081 ps_outn(ps_ez(i))
1082 ps_out(" facing " as *u8)
1083 ps_outn(ps_fx(i))
1084 ps_out(" " as *u8)
1085 ps_outn(ps_fy(i))
1086 ps_out(" footprint " as *u8)
1087 ps_outn(ps_exx(i))
1088 ps_out(" " as *u8)
1089 ps_outn(ps_eyy(i))
1090 ps_out("\n" as *u8)
1091 }
1092 i = i + 1
1093 }
1094 return 0
1095}