nx_nxa_anim_lib.nx source
↩ module page · 427 lines · 19581 B
1// nx_nxa_anim_lib.nx -- THE ONE NXA CLIP EVALUATOR: channel-2 packed world-delta keys, sampled at a
2// clip time, blended toward an idle POSE, and written as the DUAL-QUATERNION ROWS the cast shader skins
3// with. Integer-only and syscall-free so the wasm engine imports it; every consumer (engine, gates,
4// native players) reads ONE implementation of the format's own runtime form.
5//
6// WHY IT EXISTS (GE23, measured 2026-09-06 on the served beach page): the WebGPU cast pipeline already
7// skins on the GPU from a per-frame joint table, and the ONLY per-frame page logic left was animEval --
8// hand-written JavaScript sampling ANIM tracks with shortest-path nlerp, subtracting a fitted linear
9// root drift, blending toward the frozen stand pose, then forming T = b + dt - D b and the dual half
10// T x real / 2. That is the format's normative runtime form (knowledge/nxa_format_spec.md: M x = D (x - b)
11// + b + dt, shortest-path lerp between keys) expressed in the page instead of the engine. This lib is
12// that arithmetic in NishiLang, driven by the ENGINE's own stride phase rather than a page clock.
13//
14// THE UNIT TRAP, NAMED (spec + nx_nxa_play's audit): ANIM channel-2 dt LANES are MILLIMETRES packed
15// /100 to fit i16, while SKEL binds and the rows are in 0.01 mm model units, so dt lanes are scaled by
16// NA_DT_MM_TO_MODEL here. POSE entry dt words are ALREADY model units (nx_nxa_pose applies the x100
17// at freeze time; debt 1785380029) and are NOT scaled again. Two sections, two conventions, both cited.
18//
19// KEY LAYOUT (spec, channel 2, two words per key, little-endian lanes):
20// w0 = [t_ms u16][dqx i16][dqy i16][dqz i16] w1 = [dqw i16][dtx i16][dty i16][dtz i16]
21// ROW LAYOUT (what nxGpuCast uploads, per joint): real = q (q12), dual = (T x q)/2 with T in model units,
22// laid out as out[0..nj*4) real rows then out[nj*4..nj*8) dual rows; the page divides both by 4096.
23// license_tier: ORIGINAL No hw writes (Rule 26).
24import "nx_nxa_fk.nx"
25
26const NA_Q12: i64 = 4096
27const NA_LANE_MASK: i64 = 65535
28const NA_LANE_HALF: i64 = 32767
29const NA_LANE_FULL: i64 = 65536
30const NA_LANE_B1: i64 = 16
31const NA_LANE_B2: i64 = 32
32const NA_LANE_B3: i64 = 48
33const NA_DT_MM_TO_MODEL: i64 = 100 // spec: ANIM dt lanes are mm packed /100; binds are 0.01 mm
34const NA_TRK_HDR_W: i64 = 3 // per track: [joint][channel][nkeys]
35const NA_CH2: i64 = 2
36const NA_CH2_KEY_W: i64 = 2
37const NA_CH0_KEY_W: i64 = 4 // legacy verbose T keys, 32 bytes -- skipped, never sampled
38const NA_CH1_KEY_W: i64 = 5 // legacy verbose R keys, 40 bytes -- skipped, never sampled
39const NA_ROW_W: i64 = 8 // real x4 + dual x4 per joint
40const NA_IDX_W: i64 = 2 // per joint: key word offset (or -1), nkeys
41const NA_SKEL_W: i64 = 8 // spec: [parent][tx ty tz][qx qy qz qw]
42const NA_SKEL_TX: i64 = 1
43const NA_SKEL_TY: i64 = 2
44const NA_SKEL_TZ: i64 = 3
45const NA_POSE_ENTRY_W: i64 = 8 // spec: [joint][q x4][dt x3]
46const NA_RV_W: i64 = 6 // x0 y0 vx16 vy16 t0 span
47const NA_RV_FX: i64 = 65536 // drift velocity carried as units per ms x 65536
48const NA_ROOT_Z: i64 = 89000 // the page's root rule: pelvis-region bind nearest (0,0,.52H)
49const NA_ROOT_XW: i64 = 2 // ... weighting |x| twice, verbatim from loadNPC
50const NA_MPB_DEFAULT: i64 = 700 // the page's shipped default ms-per-block before a travel fit
51const NA_TRAVEL_MIN_PERMIL: i64 = 1500 // the page fits cadence only when the root travels > 1.5 blocks
52const NA_FIG_UNITS: i64 = 171530 // the reference figure's VERT extent, 0.01 mm units (1.7153 m)
53const NA_FIG_BLOCKS_CENTI: i64 = 178 // ... which the page maps to 1.78 world blocks
54const NA_SCR_W: i64 = 64 // scratch words na_rows needs from its caller
55const NA_RC_BAD_TRACKS: i64 = 0 - 1
56const NA_RC_OVERRUN: i64 = 0 - 2
57
58// sign-extend a 16-bit lane out of an i64 word
59func na_i16(w: i64, sh: i64) -> i64 {
60 var v: i64 = (w >> sh) & NA_LANE_MASK
61 if v > NA_LANE_HALF { v = v - NA_LANE_FULL }
62 return v
63}
64func na_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
65
66// index the channel-2 tracks: tidx[j*2] = key word offset into anim (or -1), tidx[j*2+1] = nkeys.
67// Bounds-checked against nw so a truncated section is REFUSED BY NAME, never read past. Returns the
68// clip duration in ms (the latest last-key time over the indexed tracks), or a negative rc.
69func na_index(anim: *i64, nw: i64, nj: i64, tidx: *i64) -> i64 {
70 var j: i64 = 0
71 while j < nj { tidx[j*NA_IDX_W] = 0 - 1; tidx[j*NA_IDX_W + 1] = 0; j = j + 1 }
72 if nw < 1 { return NA_RC_BAD_TRACKS }
73 let ntr: i64 = anim[0]
74 if ntr < 0 { return NA_RC_BAD_TRACKS }
75 var ao: i64 = 1
76 var dur: i64 = 0
77 var t: i64 = 0
78 while t < ntr {
79 if ao + NA_TRK_HDR_W > nw { return NA_RC_OVERRUN }
80 let jj: i64 = anim[ao]
81 let chn: i64 = anim[ao + 1]
82 let nk: i64 = anim[ao + 2]
83 ao = ao + NA_TRK_HDR_W
84 if nk < 0 { return NA_RC_BAD_TRACKS }
85 var kw: i64 = NA_CH0_KEY_W
86 if chn == 1 { kw = NA_CH1_KEY_W }
87 if chn == NA_CH2 { kw = NA_CH2_KEY_W }
88 // Divide remaining validated space before multiplying an external key count.
89 if nk > (nw - ao)/kw { return NA_RC_OVERRUN }
90 if chn == NA_CH2 { if nk > 0 { if jj >= 0 { if jj < nj {
91 tidx[jj*NA_IDX_W] = ao
92 tidx[jj*NA_IDX_W + 1] = nk
93 let tl: i64 = anim[ao + (nk - 1)*NA_CH2_KEY_W] & NA_LANE_MASK
94 if tl > dur { dur = tl }
95 } } } }
96 ao = ao + nk*kw
97 t = t + 1
98 }
99 return dur
100}
101
102func na_key_t(anim: *i64, ko: i64, i: i64) -> i64 { return anim[ko + i*NA_CH2_KEY_W] & NA_LANE_MASK }
103func na_key_q(anim: *i64, ko: i64, i: i64, q: *i64) -> i64 {
104 let w0: i64 = anim[ko + i*NA_CH2_KEY_W]
105 let w1: i64 = anim[ko + i*NA_CH2_KEY_W + 1]
106 q[0] = na_i16(w0, NA_LANE_B1)
107 q[1] = na_i16(w0, NA_LANE_B2)
108 q[2] = na_i16(w0, NA_LANE_B3)
109 q[3] = na_i16(w1, 0)
110 return 0
111}
112func na_key_d(anim: *i64, ko: i64, i: i64, d: *i64) -> i64 {
113 let w1: i64 = anim[ko + i*NA_CH2_KEY_W + 1]
114 d[0] = na_i16(w1, NA_LANE_B1)*NA_DT_MM_TO_MODEL
115 d[1] = na_i16(w1, NA_LANE_B2)*NA_DT_MM_TO_MODEL
116 d[2] = na_i16(w1, NA_LANE_B3)*NA_DT_MM_TO_MODEL
117 return 0
118}
119
120// sample one track at t_ms: the bracketing key pair, u in q12, shortest-path nlerp on the delta quat
121// (negate the second key when the dot is negative -- the spec's own player note), linear lerp on dt.
122// scr = 8 words.
123// Ordering is established once after na_index validates the packed section.
124// Keep the legacy two-word index ABI; callers own this one-word-per-joint sidecar.
125func na_index_order(anim: *i64, tidx: *i64, nj: i64, order: *i64) -> i64 {
126 var j: i64 = 0
127 while j < nj {
128 let ko: i64 = tidx[j*NA_IDX_W]
129 let nk: i64 = tidx[j*NA_IDX_W + 1]
130 order[j] = 0
131 if ko >= 0 { if nk > 0 {
132 order[j] = 1
133 var i: i64 = 1
134 while i < nk {
135 if na_key_t(anim, ko, i) < na_key_t(anim, ko, i - 1) { order[j] = 0 }
136 i = i + 1
137 }
138 } }
139 j = j + 1
140 }
141 return 0
142}
143
144func na_key_linear(anim: *i64, ko: i64, nk: i64, t: i64) -> i64 {
145 var c: i64 = 0
146 var i: i64 = 1
147 while i < nk { if na_key_t(anim, ko, i) <= t { c = i } i = i + 1 }
148 return c
149}
150
151// Upper bound selects the final equal-time key, matching legacy duplicate semantics.
152func na_key_ordered(anim: *i64, ko: i64, nk: i64, t: i64) -> i64 {
153 var lo: i64 = 0
154 var hi: i64 = nk
155 while lo < hi {
156 let mid: i64 = lo + (hi - lo)/2
157 if na_key_t(anim, ko, mid) <= t { lo = mid + 1 } else { hi = mid }
158 }
159 if lo == 0 { return 0 }
160 return lo - 1
161}
162
163func na_eval_track(anim: *i64, ko: i64, nk: i64, t: i64, q: *i64, d: *i64, scr: *i64) -> i64 {
164 return na_eval_key(anim, ko, nk, na_key_linear(anim, ko, nk, t), t, q, d, scr)
165}
166
167func na_eval_track_ordered(anim: *i64, ko: i64, nk: i64, t: i64, q: *i64, d: *i64, scr: *i64) -> i64 {
168 return na_eval_key(anim, ko, nk, na_key_ordered(anim, ko, nk, t), t, q, d, scr)
169}
170
171func na_eval_key(anim: *i64, ko: i64, nk: i64, c: i64, t: i64, q: *i64, d: *i64, scr: *i64) -> i64 {
172 var k1: i64 = c + 1
173 if k1 >= nk { k1 = c }
174 let t0: i64 = na_key_t(anim, ko, c)
175 let t1: i64 = na_key_t(anim, ko, k1)
176 var u: i64 = 0
177 if t1 > t0 { u = (t - t0)*NA_Q12/(t1 - t0) }
178 if u < 0 { u = 0 }
179 if u > NA_Q12 { u = NA_Q12 }
180 na_key_q(anim, ko, c, q)
181 let b: *i64 = scr
182 na_key_q(anim, ko, k1, b)
183 let dot: i64 = q[0]*b[0] + q[1]*b[1] + q[2]*b[2] + q[3]*b[3]
184 if dot < 0 { b[0] = 0 - b[0]; b[1] = 0 - b[1]; b[2] = 0 - b[2]; b[3] = 0 - b[3] }
185 q[0] = q[0] + (b[0] - q[0])*u/NA_Q12
186 q[1] = q[1] + (b[1] - q[1])*u/NA_Q12
187 q[2] = q[2] + (b[2] - q[2])*u/NA_Q12
188 q[3] = q[3] + (b[3] - q[3])*u/NA_Q12
189 nf_qnorm(q)
190 na_key_d(anim, ko, c, d)
191 let db: *i64 = ((scr as i64) + 32) as *i64
192 na_key_d(anim, ko, k1, db)
193 d[0] = d[0] + (db[0] - d[0])*u/NA_Q12
194 d[1] = d[1] + (db[1] - d[1])*u/NA_Q12
195 d[2] = d[2] + (db[2] - d[2])*u/NA_Q12
196 return 0
197}
198
199// the root joint: the page's own rule, verbatim -- the bind nearest (0, 0, .52H) with |x| weighted twice
200func na_root_joint(skel: *i64, nj: i64) -> i64 {
201 var rj: i64 = 0 - 1
202 var best: i64 = 0
203 var j: i64 = 0
204 while j < nj {
205 let s: i64 = na_abs(skel[j*NA_SKEL_W + NA_SKEL_TZ] - NA_ROOT_Z) + NA_ROOT_XW*na_abs(skel[j*NA_SKEL_W + NA_SKEL_TX])
206 if rj < 0 { rj = j; best = s } else { if s < best { best = s; rj = j } }
207 j = j + 1
208 }
209 return rj
210}
211
212// fit the LINEAR horizontal drift of the root track once (first key to last key), so the runtime
213// subtracts only that line and the oscillatory residual -- sway, weight shift -- survives on every
214// joint. rv = [x0 y0 vx16 vy16 t0 span]; span 0 means no fit (nothing is subtracted).
215func na_fit_root(anim: *i64, tidx: *i64, rj: i64, rv: *i64, scr: *i64) -> i64 {
216 var k: i64 = 0
217 while k < NA_RV_W { rv[k] = 0; k = k + 1 }
218 if rj < 0 { return 0 }
219 let ko: i64 = tidx[rj*NA_IDX_W]
220 let nk: i64 = tidx[rj*NA_IDX_W + 1]
221 if ko < 0 { return 0 }
222 if nk < 2 { return 0 }
223 let d0: *i64 = scr
224 let dl: *i64 = ((scr as i64) + 32) as *i64
225 na_key_d(anim, ko, 0, d0)
226 na_key_d(anim, ko, nk - 1, dl)
227 let t0: i64 = na_key_t(anim, ko, 0)
228 var spn: i64 = na_key_t(anim, ko, nk - 1) - t0
229 if spn < 1 { spn = 1 }
230 rv[0] = d0[0]
231 rv[1] = d0[1]
232 rv[2] = (dl[0] - d0[0])*NA_RV_FX/spn
233 rv[3] = (dl[1] - d0[1])*NA_RV_FX/spn
234 rv[4] = t0
235 rv[5] = spn
236 return 1
237}
238
239// clip milliseconds per world block walked, DERIVED from the root's own horizontal travel over the
240// clip (the page's rule): fitted only when the root travels more than 1.5 blocks, else the shipped
241// default. Returns mpb.
242func na_mpb(anim: *i64, tidx: *i64, rj: i64, dur: i64, scr: *i64) -> i64 {
243 if rj < 0 { return NA_MPB_DEFAULT }
244 let ko: i64 = tidx[rj*NA_IDX_W]
245 let nk: i64 = tidx[rj*NA_IDX_W + 1]
246 if ko < 0 { return NA_MPB_DEFAULT }
247 if nk < 1 { return NA_MPB_DEFAULT }
248 let d: *i64 = scr
249 var x0: i64 = 0
250 var x1: i64 = 0
251 var y0: i64 = 0
252 var y1: i64 = 0
253 var i: i64 = 0
254 while i < nk {
255 na_key_d(anim, ko, i, d)
256 if i == 0 { x0 = d[0]; x1 = d[0]; y0 = d[1]; y1 = d[1] } else {
257 if d[0] < x0 { x0 = d[0] }
258 if d[0] > x1 { x1 = d[0] }
259 if d[1] < y0 { y0 = d[1] }
260 if d[1] > y1 { y1 = d[1] }
261 }
262 i = i + 1
263 }
264 let dx: i64 = x1 - x0
265 let dy: i64 = y1 - y0
266 let hyp: i64 = nf_isqrt(dx*dx + dy*dy)
267 let blocks_permil: i64 = hyp*NA_FIG_BLOCKS_CENTI*10/NA_FIG_UNITS
268 if blocks_permil <= NA_TRAVEL_MIN_PERMIL { return NA_MPB_DEFAULT }
269 var du: i64 = dur
270 if du < 1 { du = 1 }
271 return du*1000/blocks_permil
272}
273
274// THE ROWS. For every joint: sample its track at t_ms (identity when untracked, exactly as the page
275// wrote identity rows), subtract the fitted root drift from the horizontal dt, blend toward the idle
276// POSE by iw (q12; shortest path, renormalised), then T = b + dt - D b with D b from nx_nxa_fk's
277// quaternion rotate, and the dual-quaternion rows. out = nj*8 words; scr = NA_SCR_W words.
278func na_rows(anim: *i64, tidx: *i64, nj: i64, skel: *i64, pq: *i64, pd: *i64, haspose: i64, rv: *i64, t: i64, iw: i64, out: *i64, scr: *i64) -> i64 {
279 return na_rows_indexed(anim, tidx, 0 as *i64, nj, skel, pq, pd, haspose, rv, t, iw, out, scr)
280}
281
282func na_rows_indexed(anim: *i64, tidx: *i64, order: *i64, nj: i64, skel: *i64, pq: *i64, pd: *i64, haspose: i64, rv: *i64, t: i64, iw: i64, out: *i64, scr: *i64) -> i64 {
283 let q: *i64 = scr
284 let d: *i64 = ((scr as i64) + 32) as *i64
285 let db: *i64 = ((scr as i64) + 64) as *i64
286 let qs: *i64 = ((scr as i64) + 96) as *i64
287 let es: *i64 = ((scr as i64) + 256) as *i64
288 var j: i64 = 0
289 while j < nj {
290 let ko: i64 = tidx[j*NA_IDX_W]
291 let nk: i64 = tidx[j*NA_IDX_W + 1]
292 let ro: i64 = j*4
293 let du: i64 = nj*4 + j*4
294 if ko < 0 {
295 out[ro] = 0; out[ro + 1] = 0; out[ro + 2] = 0; out[ro + 3] = NA_Q12
296 out[du] = 0; out[du + 1] = 0; out[du + 2] = 0; out[du + 3] = 0
297 } else {
298 var ordered: i64 = 0
299 if (order as i64) != 0 { ordered = order[j] }
300 if ordered == 1 { na_eval_track_ordered(anim, ko, nk, t, q, d, es) } else { na_eval_track(anim, ko, nk, t, q, d, es) }
301 if rv[5] > 0 {
302 d[0] = d[0] - (rv[0] + rv[2]*(t - rv[4])/NA_RV_FX)
303 d[1] = d[1] - (rv[1] + rv[3]*(t - rv[4])/NA_RV_FX)
304 }
305 if haspose == 1 { if iw > 0 {
306 let px: i64 = pq[j*4]
307 let py: i64 = pq[j*4 + 1]
308 let pz: i64 = pq[j*4 + 2]
309 let pw: i64 = pq[j*4 + 3]
310 var sg: i64 = 1
311 if q[0]*px + q[1]*py + q[2]*pz + q[3]*pw < 0 { sg = 0 - 1 }
312 q[0] = q[0] + (sg*px - q[0])*iw/NA_Q12
313 q[1] = q[1] + (sg*py - q[1])*iw/NA_Q12
314 q[2] = q[2] + (sg*pz - q[2])*iw/NA_Q12
315 q[3] = q[3] + (sg*pw - q[3])*iw/NA_Q12
316 nf_qnorm(q)
317 d[0] = d[0] + (pd[j*3] - d[0])*iw/NA_Q12
318 d[1] = d[1] + (pd[j*3 + 1] - d[1])*iw/NA_Q12
319 d[2] = d[2] + (pd[j*3 + 2] - d[2])*iw/NA_Q12
320 } }
321 let bx: i64 = skel[j*NA_SKEL_W + NA_SKEL_TX]
322 let by: i64 = skel[j*NA_SKEL_W + NA_SKEL_TY]
323 let bz: i64 = skel[j*NA_SKEL_W + NA_SKEL_TZ]
324 nf_qrotv(q, bx, by, bz, db, qs)
325 let tx: i64 = bx + d[0] - db[0]
326 let ty: i64 = by + d[1] - db[1]
327 let tz: i64 = bz + d[2] - db[2]
328 out[ro] = q[0]; out[ro + 1] = q[1]; out[ro + 2] = q[2]; out[ro + 3] = q[3]
329 out[du] = (tx*q[3] + ty*q[2] - tz*q[1])/2
330 out[du + 1] = (0 - tx*q[2] + ty*q[3] + tz*q[0])/2
331 out[du + 2] = (tx*q[1] - ty*q[0] + tz*q[3])/2
332 out[du + 3] = (0 - tx*q[0] - ty*q[1] - tz*q[2])/2
333 }
334 j = j + 1
335 }
336 return 0
337}
338
339// In-place gait calibration uses the asset's admitted foot landmarks. The rearward
340// travel of the lower sole maps path distance to clip time; clip length is not a stride.
341const NA_GAIT_FOOT_W: i64 = 9
342const NA_GAIT_INPUT_W: i64 = 18
343const NA_GAIT_HEADER_W: i64 = 8
344const NA_GAIT_ROW_W: i64 = 3
345const NA_GAIT_DISTANCE: i64 = 1
346const NA_GAIT_CONTACT: i64 = 2
347func na_gait_sole(anim: *i64, ko: i64, nk: i64, skel: *i64, foot: *i64, t: i64, rv: *i64, out: *i64, scr: *i64) -> i64 {
348 let q: *i64 = scr
349 let d: *i64 = ((scr as i64)+32) as *i64
350 let es: *i64 = ((scr as i64)+64) as *i64
351 let rot: *i64 = ((scr as i64)+128) as *i64
352 let rs: *i64 = ((scr as i64)+160) as *i64
353 na_eval_track_ordered(anim,ko,nk,t,q,d,es)
354 if rv[5]>0 { d[0]=d[0]-(rv[0]+rv[2]*(t-rv[4])/NA_RV_FX); d[1]=d[1]-(rv[1]+rv[3]*(t-rv[4])/NA_RV_FX) }
355 let x: i64=q[0]; let y: i64=q[1]; let z: i64=q[2]; let w: i64=q[3]
356 let s: i64=foot[7]; let c: i64=foot[8]
357 q[0]=(s*w+c*x)/NA_Q12; q[1]=(c*y-s*z)/NA_Q12
358 q[2]=(s*y+c*z)/NA_Q12; q[3]=(c*w-s*x)/NA_Q12
359 nf_qnorm(q)
360 let j: i64=foot[0]
361 let bx: i64=skel[j*NA_SKEL_W+NA_SKEL_TX]
362 let by: i64=skel[j*NA_SKEL_W+NA_SKEL_TY]
363 let bz: i64=skel[j*NA_SKEL_W+NA_SKEL_TZ]
364 var p: i64=0
365 while p<2 {
366 let k: i64=1+p*3
367 nf_qrotv(q,foot[k]-bx,foot[k+1]-by,foot[k+2]-bz,rot,rs)
368 out[p*3]=rot[0]+bx+d[0]; out[p*3+1]=rot[1]+by+d[1]; out[p*3+2]=rot[2]+bz+d[2]
369 p=p+1
370 }
371 return 0
372}
373func na_gait_fit(anim: *i64,tidx: *i64,order: *i64,skel: *i64,nj: i64,rv: *i64,feet: *i64,header: *i64,table: *i64,capacity: i64,scr: *i64) -> i64 {
374 var i: i64=0
375 while i<NA_GAIT_HEADER_W {header[i]=0;i=i+1}
376 let left: i64=feet[0]; let right: i64=feet[NA_GAIT_FOOT_W]
377 if left<0||right<0||left>=nj||right>=nj||left==right {return 0-81}
378 if order[left]!=1||order[right]!=1 {return 0-82}
379 let lo: i64=tidx[left*NA_IDX_W]; let ln: i64=tidx[left*NA_IDX_W+1]
380 let ro: i64=tidx[right*NA_IDX_W]; let rn: i64=tidx[right*NA_IDX_W+1]
381 if lo<0||ro<0||ln<2||rn<2||ln>capacity {return 0-83}
382 let lp: *i64=((scr as i64)+288) as *i64
383 let rp: *i64=((scr as i64)+336) as *i64
384 let prev: *i64=((scr as i64)+384) as *i64
385 var distance: i64=0; var previous_contact: i64=0-1
386 var changes: i64=0; var supported: i64=0; var unknown: i64=0
387 i=0
388 while i<ln {
389 let t: i64=na_key_t(anim,lo,i)
390 na_gait_sole(anim,lo,ln,skel,feet,t,rv,lp,scr)
391 na_gait_sole(anim,ro,rn,skel,((feet as i64)+NA_GAIT_FOOT_W*8) as *i64,t,rv,rp,scr)
392 var l: i64=0; if lp[5]<lp[2] {l=3}
393 var r: i64=0; if rp[5]<rp[2] {r=3}
394 var contact: i64=0; if rp[r+2]<lp[l+2] {contact=1}
395 if i>0 {
396 let dl: i64=lp[l+1]-prev[l+1]
397 let dr: i64=rp[r+1]-prev[6+r+1]
398 var travel: i64=dl
399 if contact==1 {travel=dr}
400 if travel<=0 {if contact==0&&dr>0 {contact=1;travel=dr} else {if contact==1&&dl>0 {contact=0;travel=dl}}}
401 if travel>0 {distance=distance+travel;supported=supported+1} else {unknown=unknown+1;contact=2}
402 if contact<2&&previous_contact>=0&&previous_contact<2&&contact!=previous_contact {changes=changes+1}
403 }
404 table[i*NA_GAIT_ROW_W]=t;table[i*NA_GAIT_ROW_W+NA_GAIT_DISTANCE]=distance;table[i*NA_GAIT_ROW_W+NA_GAIT_CONTACT]=contact
405 var k: i64=0;while k<6 {prev[k]=lp[k];prev[6+k]=rp[k];k=k+1}
406 previous_contact=contact;i=i+1
407 }
408 if distance<=0||supported==0 {return 0-84}
409 header[0]=ln;header[1]=distance;header[2]=changes;header[3]=supported;header[4]=unknown
410 header[5]=table[(ln-1)*NA_GAIT_ROW_W]-table[0]
411 header[6]=header[5]*NA_FIG_UNITS*100/(distance*NA_FIG_BLOCKS_CENTI)
412 header[7]=1
413 return 0
414}
415func na_gait_time(table: *i64,n: i64,distance: i64) -> i64 {
416 if n<2 {return 0}
417 let total: i64=table[(n-1)*NA_GAIT_ROW_W+NA_GAIT_DISTANCE]
418 if total<=0 {return 0}
419 var d: i64=distance%total;if d<0 {d=d+total}
420 var lo: i64=0;var hi: i64=n
421 while lo<hi {let mid:i64=lo+(hi-lo)/2;if table[mid*NA_GAIT_ROW_W+NA_GAIT_DISTANCE]<=d {lo=mid+1}else{hi=mid}}
422 var a:i64=lo-1;if a<0 {a=0};var b:i64=a+1;if b>=n {b=a}
423 let da:i64=table[a*NA_GAIT_ROW_W+NA_GAIT_DISTANCE];let db:i64=table[b*NA_GAIT_ROW_W+NA_GAIT_DISTANCE]
424 let ta:i64=table[a*NA_GAIT_ROW_W];let tb:i64=table[b*NA_GAIT_ROW_W]
425 if db<=da {return ta}
426 return ta+(tb-ta)*(d-da)/(db-da)
427}