nx_vpick_lib.nx
buildroot/runtime/nx_vpick_lib.nx
about
nx_vpick_lib.nx -- DC4 of the /compare/dcc board: RAY-PICK and a TRANSFORM GIZMO for the sovereign
browser viewport. nx_meshview_wasm already RENDERS and ORBITS a real indexed mesh through our own
software rasteriser compiled to WASM (zero WebGL, zero Emscripten, zero third-party code). Nothing in
that viewport can be SELECTED or DRAGGED, so it is a viewer and not an editor. This library is the
missing half: turn a click into a triangle, turn a drag into an axis-constrained translation, and hand
that translation to the operation stack as an OP instead of writing it into the vertex buffer.
THE GIZMO NEVER MUTATES GEOMETRY. vp_gizmo_delta takes no mesh pointer at all -- it cannot write a
vertex even by accident -- and vp_apply_drag pushes ES_OP_ADD / ES_OP_RADD onto an nx_editstack_lib
stack. That is the whole point of the rung: a drag becomes an entry in the log, so it is undoable,
redoable, replayable bit-exactly, and re-evaluable at depth, exactly like every other edit. A gizmo
that wrote vertices directly would be a gizmo whose work cannot be undone without a snapshot.
CHECK-BEFORE-BUILD. nx_capsearch was asked first (corpus_complete=1 over 6892 tools and 5517 libs) and
nx_vpick is absent from the tree (nx_shelltool find, corpus_complete=1 over 61581 files). THREE
incumbents were then READ, not guessed, and each is named with the measured reason it is not the answer:
* mtk_ray_tri (nx_meshthick.nx) IS a Moller-Trumbore ray/triangle test and is the closest thing the
estate had. It works in fx256, it REQUIRES a unit-length direction, it returns ONLY a scalar
distance, and it collapses parallel, behind, outside-the-triangle and no-intersection into a single
0-1 return -- a negative answer that cannot say which negative it is. A picker needs the barycentric
coordinates (to place a manipulator on the hit), needs to tell a DEGENERATE triangle from a parallel
ray, and must not be forced to normalise a screen ray: normalisation costs an isqrt and discards the
sub-unit precision a pixel-accurate pick depends on. Its own consumer nx_castview.nx already carries
a standing warning about converting Q14 rays into its fx256 scale.
* r3_ray (nx_recon3d.nx) back-projects a pixel to a ray, but from a 3x3 BASIS plus a focal length, and
normalises the result. The viewport camera is not that model: it is yaw + orbit distance + half-fov
through rc_perspective_cs. Feeding one convention's rays to the other camera is how a pick lands on
the wrong triangle.
* nx_raycast_voxel.nx is DDA traversal of a voxel grid, not an indexed triangle mesh.
THE CAMERA CONVENTION IS COPIED, NOT INVENTED. Everything below is the exact inverse of what
mv_render_loaded_cam and mv_proj1 do in nx_meshview_wasm:
model -> eye eye = T(0,0,-dist) * Ry(yaw) * model (roty exactly as the renderer builds it)
eye -> clip rc_perspective_cs(cos(fovh), sin(fovh), w*Q/h, Q, far)
clip -> screen sx = (ndcx + 1) * w / 2 , sy = (1 - ndcy) * h / 2
so screen -> model is: undo the viewport map, divide out f = cot(fovh) and the aspect, set eye-space z
to -1, then apply Ry(yaw) TRANSPOSED (a rotation's transpose is its inverse) and place the eye at
Ry(yaw)^T * (0,0,dist). The trig is rc_sin_q14 / rc_cos_q14 -- the renderer's OWN Bhaskara
approximation, COMPOSED rather than re-implemented, so a pick can never drift from the picture it was
taken on. The scale unit is RC_Q, imported from nx_render_core for the same reason: one Q in the estate.
dependencies 2 imports · 1 importers
imports: nx_render_core.nxnx_editstack_lib.nx
imported by: nx_vpick_gate.nx
structs
| none |
consts
| 97 | const VP_RAY_OX: i64 = 0 |
| 98 | const VP_RAY_OY: i64 = 1 |
| 99 | const VP_RAY_OZ: i64 = 2 |
| 100 | const VP_RAY_DX: i64 = 3 |
| 101 | const VP_RAY_DY: i64 = 4 |
| 102 | const VP_RAY_DZ: i64 = 5 |
| 103 | const VP_RAY_WORDS: i64 = 6 |
| 107 | const VP_CAM_W: i64 = 0 |
| 108 | const VP_CAM_H: i64 = 1 |
| 109 | const VP_CAM_YAW: i64 = 2 // degrees, fed straight to rc_sin_q14 like the renderer does |
| 110 | const VP_CAM_DIST: i64 = 3 // Q14 orbit distance, the renderer's `dist` |
| 111 | const VP_CAM_FOVH: i64 = 4 // half field-of-view in degrees, the renderer's FOVH |
| 112 | const VP_CAM_PX: i64 = 5 |
| 113 | const VP_CAM_PY: i64 = 6 |
| 114 | const VP_CAM_PZ: i64 = 7 |
| 115 | const VP_CAM_WORDS: i64 = 8 |
| 118 | const VP_R_T: i64 = 0 // ray parameter, Q14: hit = origin + t*dir/Q |
| 119 | const VP_R_U: i64 = 1 // barycentric weight of v1, Q14 |
| 120 | const VP_R_V: i64 = 2 // barycentric weight of v2, Q14 |
| 121 | const VP_R_W: i64 = 3 // barycentric weight of v0, Q14 (Q - u - v) |
| 122 | const VP_R_HX: i64 = 4 |
| 123 | const VP_R_HY: i64 = 5 |
| 124 | const VP_R_HZ: i64 = 6 |
| 125 | const VP_R_WORDS: i64 = 7 |
| 128 | const VP_G_DX: i64 = 0 |
| 129 | const VP_G_DY: i64 = 1 |
| 130 | const VP_G_DZ: i64 = 2 |
| 131 | const VP_G_SCALAR: i64 = 3 // signed magnitude along the chosen axis; equals the one nonzero component |
| 132 | const VP_G_WORDS: i64 = 4 |
| 135 | const VP_DRAG_AX: i64 = 0 |
| 136 | const VP_DRAG_AY: i64 = 1 |
| 137 | const VP_DRAG_BX: i64 = 2 |
| 138 | const VP_DRAG_BY: i64 = 3 |
| 139 | const VP_DRAG_WORDS: i64 = 4 |
| 143 | const VP_SC_RAY_A: i64 = 0 |
| 144 | const VP_SC_RAY_B: i64 = 6 |
| 145 | const VP_SC_HIT_A: i64 = 12 |
| 146 | const VP_SC_HIT_B: i64 = 15 |
| 147 | const VP_SCRATCH_WORDS: i64 = 18 |
| 149 | const VP_AXIS_X: i64 = 0 |
| 150 | const VP_AXIS_Y: i64 = 1 |
| 151 | const VP_AXIS_Z: i64 = 2 |
| 155 | const VP_HIT: i64 = 1 |
| 156 | const VP_NOHIT_DEGENERATE: i64 = 0 - 1 // zero-area triangle: e1 x e2 is exactly the zero vector |
| 157 | const VP_NOHIT_PARALLEL: i64 = 0 - 2 // ray parallel to a genuinely non-degenerate triangle |
| 158 | const VP_NOHIT_OUTSIDE: i64 = 0 - 3 // plane hit, but the barycentric point is outside the triangle |
| 159 | const VP_NOHIT_BEHIND: i64 = 0 - 4 // intersection is behind the ray origin |
| 165 | const VP_OK: i64 = 0 |
| 166 | const VP_MISS_NONE: i64 = 0 - 16 // NO triangle was intersected. Deliberately not 0 and not -1. |
| 167 | const VP_E_TRI_IDX: i64 = 0 - 17 |
| 168 | const VP_E_ZERO_DIR: i64 = 0 - 18 |
| 169 | const VP_E_SCREEN: i64 = 0 - 19 |
| 170 | const VP_E_BOUND: i64 = 0 - 20 |
| 171 | const VP_E_VIEWPORT: i64 = 0 - 21 |
| 172 | const VP_E_AXIS: i64 = 0 - 22 |
| 173 | const VP_E_ARGS: i64 = 0 - 23 |
| 174 | const VP_E_CELL: i64 = 0 - 24 |
| 175 | const VP_E_FOV: i64 = 0 - 25 |
| 176 | const VP_E_PLANE: i64 = 0 - 26 |
| 179 | const VP_VERT_MAX: i64 = 262144 // 16.0 in Q14 |
| 180 | const VP_ORIGIN_MAX: i64 = 524288 // 32.0 in Q14 == DV_DIST_MAX in nx_meshview_wasm |
| 181 | const VP_DIR_MAX: i64 = 262144 // 16.0 in Q14 |
| 184 | const VP_WORLD_MAX: i64 = 786432 // VP_ORIGIN_MAX + VP_VERT_MAX |
| 188 | const VP_RAYT_MAX: i64 = 1099511627776 |
| 190 | const VP_THREE: i64 = 3 // components per vertex in the estate's AoS mesh buffers |
| 191 | const VP_TWO: i64 = 2 |
| 196 | const VP_TRI_I: i64 = 3 // vertex indices per triangle in the estate's index buffers |
| 197 | const VP_TI_A: i64 = 0 |
| 198 | const VP_TI_B: i64 = 1 |
| 199 | const VP_TI_C: i64 = 2 |
| 635 | const VP_NS_PER_US: i64 = 1000 // nanoseconds in a microsecond: a unit conversion, never a tunable |
| 636 | const VP_PERMIL: i64 = 1000 // parts per thousand |
functions
| 206 | func vp_abs(a: i64) -> i64 { if a < 0 { return 0 - a } return a } |
| 208 | func vp_within(v: i64, m: i64) -> i64 |
| 216 | func vp_code_name(c: i64) -> *u8 |
| 237 | func vp_axis_ok(axis: i64) -> i64 |
| 244 | func vp_dir_is_zero(ray: *i64, rbase: i64) -> i64 |
| 251 | func vp_ray_within_bounds(ray: *i64, rbase: i64) -> i64 |
| 261 | func vp_vert_within_bounds(mesh: *i64, v: i64) -> i64 |
| 274 | func vp_ray_from_screen(cam: *i64, px: i64, py: i64, ray: *i64, base: i64) -> i64 |
| 337 | func vp_tri_hit(mesh: *i64, nverts: i64, i0: i64, i1: i64, i2: i64, ray: *i64, rbase: i64, out: *i64) -> i64 |
| 419 | func vp_tri_hit_at(mesh: *i64, nverts: i64, idx: *i64, ntris: i64, tri: i64, ray: *i64, rbase: i64, out: *i64) -> i64 |
| 437 | func vp_pick(mesh: *i64, nverts: i64, idx: *i64, ntris: i64, ray: *i64, rbase: i64, out: *i64) -> i64 |
| 472 | func vp_pick_screen(cam: *i64, px: i64, py: i64, mesh: *i64, nverts: i64, idx: *i64, ntris: i64, scratch: *i64, out: *i64) -> i64 |
| 488 | func vp_plane_hit(cam: *i64, px: i64, py: i64, scratch: *i64, rbase: i64, out: *i64, obase: i64) -> i64 |
| 526 | func vp_gizmo_delta(cam: *i64, drag: *i64, axis: i64, scratch: *i64, out: *i64) -> i64 |
| 561 | func vp_cell(axis: i64, vert: i64, nverts: i64) -> i64 { return axis * nverts + vert } |
| 562 | func vp_cells_needed(nverts: i64) -> i64 { return VP_THREE * nverts } |
| 566 | func vp_seed_stack(st: *i64, mesh: *i64, nverts: i64) -> i64 |
| 581 | func vp_vert_component(st: *i64, vert: i64, axis: i64, nverts: i64) -> i64 |
| 591 | func vp_apply_drag(st: *i64, cam: *i64, drag: *i64, axis: i64, first_vert: i64, count: i64, nverts: i64, scratch: *i64, out: *i64) -> i64 |
| 640 | func vp_bench_verts(g: i64) -> i64 { return (g + 1) * (g + 1) } |
| 641 | func vp_bench_tris(g: i64) -> i64 { return VP_TWO * g * g } |
| 653 | func vp_bench_grid(mesh: *i64, idx: *i64, g: i64) -> i64 |
| 702 | func vp_bench_run(cam: *i64, px: i64, py: i64, mesh: *i64, nverts: i64, idx: *i64, ntris: i64, scratch: *i64, out: *i64, reps: i64) -> i64 |
| 718 | func vp_ns_per_op(elapsed_us: i64, reps: i64) -> i64 |
| 728 | func vp_cost_per_tri(ns_per_op: i64, ntris: i64) -> i64 |
| 737 | func vp_frame_permil(ns_per_op: i64, frame_us: i64) -> i64 |
| 746 | func vp_frame_ppm(ns_per_op: i64, frame_us: i64) -> i64 |
| 771 | func vp_growth_ok(n_lo: i64, k_lo: i64, n_hi: i64, k_hi: i64) -> i64 |
| 784 | func vp_growth_permil(k_lo: i64, k_hi: i64) -> i64 |