@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,571 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * three-camera-geometry.js — the three.js camera's PURE GEOMETRY authority: the
5
+ * orbit bag ⇄ camera basis maps, the screen-relative rotation/translation
6
+ * primitives, and the analytic pinhole-camera ray math.
7
+ *
8
+ * ## Why this is its own file
9
+ *
10
+ * It is the pure half of the camera authority: plain functions of an orbit bag
11
+ * and a scene size, returning plain `{x,y,z}` data, with no THREE, no WebGL and
12
+ * no live provider state. `three-provider.js` (the live half) resolves it and
13
+ * every camera surface it exposes — `_applyOrbit`, `screenToWorld`,
14
+ * `getCameraControl()`'s `setOrbit`/`look`/`move`/carry methods — is a THIN
15
+ * wrapper over the functions below. **All camera geometry lives HERE**: the
16
+ * provider positions a camera, it does not re-derive a basis, and no widget or
17
+ * host above it does either.
18
+ *
19
+ * Its sibling `three-camera-contract.js` is the DECLARATIVE half (the preset
20
+ * table + the pose-axis field descriptors). Three files, one camera: declarative
21
+ * contract / pure geometry / live renderer.
22
+ *
23
+ * Keeping geometry separate also holds `three-provider.js` under the per-file
24
+ * size ceiling that `sdk/_build/verify-publish-surface.mjs` enforces
25
+ * on the copied-verbatim dist directories — that gate's own instruction is to
26
+ * SPLIT at a natural seam rather than raise the ceiling, and "pure orbit
27
+ * geometry vs live renderer" is that seam.
28
+ *
29
+ * ## The orbit bag
30
+ *
31
+ * `{ azimuth, elevation, distance, roll, target }`
32
+ *
33
+ * Generalized coordinates, all angles in DEGREES, `distance` in ENGINE PIXELS:
34
+ * - `azimuth` — orbit angle around the world vertical.
35
+ * - `elevation` — orbit angle above (+) / below (−) the horizontal, |el| ≤ 90.
36
+ * - `distance` — how far the eye sits from the orbit centre.
37
+ * - `roll` — rotation about the VIEW AXIS (the horizon tilt). WRAPS
38
+ * (mod 360); it has no pole, so it is never clamped.
39
+ * - `target` — the point the orbit revolves around, or `null` = the scene
40
+ * centre (`orbitCenter` is the ONE resolver).
41
+ *
42
+ * The bag is BOTH the declarative contract (a rung's stored camera, a saved
43
+ * project, a drone waypoint) and the provider's stored state. The camera's
44
+ * up-VECTOR is never stored — it is DERIVED as `orbitCameraBasis().up`. The bag
45
+ * carries the free coordinate; the geometry derives the vector.
46
+ *
47
+ * ## The two maps (compose ⇄ decompose)
48
+ *
49
+ * `orbitCameraBasis(bag, w, h)` → `{pos, forward, right, up}` — compose
50
+ * `decomposeOrbitBasis(basis)` → `{azimuth, elevation, roll}` — decompose
51
+ *
52
+ * They are exact adjoints (see `decomposeOrbitBasis`'s own docstring for the
53
+ * pole convention), which is what lets `rotateOrbit` express an INTERACTIVE,
54
+ * screen-relative rotation (a LOOK drag) as: compose → rotate the orthonormal
55
+ * frame → decompose back to the bag. Azimuth is therefore an OUTPUT on the
56
+ * interactive path while remaining a writable coordinate on the declarative one
57
+ * — the same coordinate, two directions through the same adjoint pair.
58
+ *
59
+ * ## Node
60
+ *
61
+ * Named ES-module exports, so every function here is testable in plain
62
+ * Node with no THREE instance — see `providers/test/camera-roll-geometry.test.mjs`,
63
+ * `camera-carry.test.mjs`, `screen-to-world.test.mjs`.
64
+ */
65
+
66
+
67
+
68
+ // ── Constants ───────────────────────────────────────────────────────────
69
+
70
+ /** The ONE vertical-FOV constant (degrees). `three-provider.js`'s `_init`
71
+ * constructs `new THREE.PerspectiveCamera(...)` with THIS — never a second
72
+ * literal `50` — so the analytic ray math below and the live camera share
73
+ * one frustum by construction. */
74
+ var CAMERA_FOV_DEG = 50;
75
+
76
+ /** Ray/plane ANGLE guard (`dir.z` IS a cosine for a unit `dir`): reject any
77
+ * ray within ~2.9° of parallel-to-the-plane, never just "exactly" parallel.
78
+ * See `intersectRayWithZPlane` for the regression this closes. */
79
+ var MIN_RAY_PLANE_COS = 0.05;
80
+ /** Reject an unprojected result beyond 20× the scene's own width/height. */
81
+ var SANITY_EXTENT_FACTOR = 20;
82
+
83
+ /**
84
+ * The FIXED world-up HINT the orbit basis is built from — physics y-DOWN, so
85
+ * it matches the render camera's own `up` before any roll. It appears in
86
+ * EXACTLY ONE place in the repo (here): `orbitCameraBasis` is the sole
87
+ * consumer, and every other camera surface (the live `_applyOrbit`, MOVE
88
+ * strafe, `screenToWorld`, the carry pin, `rayDirForNdc`) consumes the BASIS
89
+ * this produces. Three hand-copies of this hint used to exist — the roll
90
+ * capability is exactly what a fourth would have broken, since a rolled
91
+ * picture with an UNROLLED strafe/drag/carry basis is the schema-mirror
92
+ * failure signature (one datum, two enumerations, breaks in one consumer).
93
+ */
94
+ var UP_HINT = { x: 0, y: -1, z: 0 };
95
+
96
+ /**
97
+ * Pole band (dimensionless): |cos(elevation)| below this and the
98
+ * (azimuth, roll) split is a GAUGE — see `decomposeOrbitBasis`. Both the
99
+ * compose and the decompose branch on the SAME quantity with the SAME
100
+ * threshold, which is what makes the round trip exact at the pole.
101
+ */
102
+ var POLE_EPS = 1e-9;
103
+
104
+ var DEG = Math.PI / 180;
105
+ var RAD = 180 / Math.PI;
106
+
107
+ // ── Small vector helpers (private) ──────────────────────────────────────
108
+
109
+ function _cross(a, b) {
110
+ return {
111
+ x: a.y * b.z - a.z * b.y,
112
+ y: a.z * b.x - a.x * b.z,
113
+ z: a.x * b.y - a.y * b.x
114
+ };
115
+ }
116
+ function _dot(a, b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
117
+ function _norm(v) {
118
+ var l = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) || 1;
119
+ return { x: v.x / l, y: v.y / l, z: v.z / l };
120
+ }
121
+ /** Rodrigues rotation of `v` about the UNIT axis `k` by `rad`. */
122
+ function _rotateAbout(v, k, rad) {
123
+ var c = Math.cos(rad), s = Math.sin(rad);
124
+ var kxv = _cross(k, v);
125
+ var kdv = _dot(k, v) * (1 - c);
126
+ return {
127
+ x: v.x * c + kxv.x * s + k.x * kdv,
128
+ y: v.y * c + kxv.y * s + k.y * kdv,
129
+ z: v.z * c + kxv.z * s + k.z * kdv
130
+ };
131
+ }
132
+ /** Wrap an angle to (-180, 180] — roll and azimuth WRAP, they never clamp. */
133
+ function _wrapDeg(d) {
134
+ var w = ((d + 180) % 360 + 360) % 360 - 180;
135
+ return (w === -180) ? 180 : w;
136
+ }
137
+
138
+ // ── The orbit centre ────────────────────────────────────────────────────
139
+
140
+ /**
141
+ * The ONE resolver for the point the orbit revolves around (and the camera
142
+ * looks at). Default = the scene centre `(width/2, height/2, 0)`; an orbit
143
+ * carrying a `target:{x,y,z}` (set by `move()` — the FPS walk) revolves
144
+ * around THAT instead. Every consumer of the camera geometry —
145
+ * `orbitCameraBasis`, and through it `screenToWorld` and the carry
146
+ * transforms — resolves the centre HERE, so a walked camera keeps
147
+ * click-to-place and carry registered instead of silently desyncing.
148
+ */
149
+ function _orbitCenter(orbit, width, height) {
150
+ var t = orbit && orbit.target;
151
+ if (t && typeof t.x === 'number' && typeof t.y === 'number' && typeof t.z === 'number') {
152
+ return { x: t.x, y: t.y, z: t.z };
153
+ }
154
+ return { x: width / 2, y: height / 2, z: 0 };
155
+ }
156
+
157
+ // ── COMPOSE: bag → camera position + orthonormal basis ──────────────────
158
+
159
+ /**
160
+ * Camera position + orthonormal basis (forward/right/up) for the given orbit
161
+ * bag, around the orbit's resolved centre (`_orbitCenter` — the scene centre,
162
+ * or a walked `target`). This is the EXACT geometry `three-provider.js`'s
163
+ * `_applyOrbit` positions the live THREE camera with, and the ONE camera
164
+ * authority every other surface consumes.
165
+ *
166
+ * `pos = centre − distance · forward` exactly, so a ROLL (a rotation about
167
+ * `forward`) never moves the eye and never changes the radius — a LOOK is
168
+ * fixed-radius by construction, and only MOVE translates.
169
+ *
170
+ * Basis convention: `right`/`up` ARE screen-right/screen-up by construction,
171
+ * because they are derived from the SAME `UP_HINT` the render camera's own
172
+ * `up` comes from (see UP_HINT). At the default pose (az=0, el=0, roll=0):
173
+ * `forward = (0,0,-1)` INTO the screen (viewer on +z), `right = (-1,0,0)`
174
+ * = world −x = SCREEN-RIGHT, `up = (0,-1,0)` = world −y = physics-UP =
175
+ * SCREEN-UP. That is what makes `screenToWorld`, the MOVE strafe, the carry
176
+ * pin and the drive change-of-basis screen-consistent regardless of the
177
+ * world-axis SIGN convention.
178
+ *
179
+ * ROLL enters HERE and nowhere else: after `right`/`up` are built from the
180
+ * fixed hint, both are rotated about `forward` by `roll` (right-handed about
181
+ * `forward`), which on screen spins the PICTURE counter-clockwise for a
182
+ * positive roll. `forward` is untouched, so roll changes the horizon tilt and
183
+ * nothing else.
184
+ *
185
+ * TOTAL at the poles: at |elevation| = 90 the hint is parallel to `forward`
186
+ * and the cross product vanishes, so the unrolled `right` is taken as the
187
+ * analytic LIMIT of the off-pole formula, `(−cos az, 0, sin az)`. Pitching
188
+ * over the top is therefore legal and continuous — no clamp, no NaN basis.
189
+ *
190
+ * @param {{azimuth:number,elevation:number,distance:number,roll?:number,target?:object}} orbit
191
+ * @param {number} width scene width in engine pixels
192
+ * @param {number} height scene height in engine pixels
193
+ * @returns {{pos:{x,y,z}, forward:{x,y,z}, right:{x,y,z}, up:{x,y,z}}}
194
+ */
195
+ function _orbitCameraBasis(orbit, width, height) {
196
+ var _c = _orbitCenter(orbit, width, height);
197
+ var cx = _c.x, cy = _c.y, cz = _c.z;
198
+ var az = orbit.azimuth * DEG;
199
+ var el = orbit.elevation * DEG;
200
+ var d = orbit.distance;
201
+ var cosEl = Math.cos(el), sinEl = Math.sin(el);
202
+ var pos = {
203
+ x: cx + d * cosEl * Math.sin(az),
204
+ y: cy + d * sinEl,
205
+ z: cz + d * cosEl * Math.cos(az)
206
+ };
207
+ // forward = normalize(centre - pos) = -(cosEl sinAz, sinEl, cosEl cosAz)
208
+ var forward = { x: -cosEl * Math.sin(az), y: -sinEl, z: -cosEl * Math.cos(az) };
209
+
210
+ // Unrolled screen-right = normalize(forward × UP_HINT). |forward × UP_HINT|
211
+ // is exactly |cosEl|, so the pole test below is the SAME quantity (and the
212
+ // SAME POLE_EPS) `decomposeOrbitBasis` branches on — that is what makes the
213
+ // two exact adjoints at the pole as well as away from it.
214
+ var right;
215
+ if (Math.abs(cosEl) < POLE_EPS) {
216
+ // POLE: the analytic limit of normalize(forward × UP_HINT) as el → ±90.
217
+ //
218
+ // DEFENSIVE GUARD, deliberately not a behavioural gate. `Math.cos` never
219
+ // returns exactly 0 for a double (cos(90°) is ~6.1e-17), so the generic
220
+ // branch below already normalizes cleanly at the pole and produces this
221
+ // very vector — verified by mutation: disabling this branch moves NO test.
222
+ // It exists so that a genuinely zero cross product could never reach
223
+ // `_norm`'s `|| 1` fallback and yield a SILENT zero basis vector, which is
224
+ // the one failure mode here that would not announce itself. If you are
225
+ // hunting for the test that covers this line: there isn't one, and that is
226
+ // recorded on purpose rather than papered over with a vacuous assertion.
227
+ right = { x: -Math.cos(az), y: 0, z: Math.sin(az) };
228
+ } else {
229
+ right = _norm(_cross(forward, UP_HINT));
230
+ }
231
+ var up = _cross(right, forward);
232
+
233
+ var rollDeg = (typeof orbit.roll === 'number') ? orbit.roll : 0;
234
+ if (rollDeg) {
235
+ var r = rollDeg * DEG;
236
+ right = _rotateAbout(right, forward, r);
237
+ up = _rotateAbout(up, forward, r);
238
+ }
239
+ return { pos: pos, forward: forward, right: right, up: up };
240
+ }
241
+
242
+ // ── DECOMPOSE: camera basis → bag angles (the exported adjoint) ──────────
243
+
244
+ /**
245
+ * The adjoint of `_orbitCameraBasis`: recover `{azimuth, elevation, roll}`
246
+ * (degrees) from an orthonormal camera basis. Colocated with its inverse on
247
+ * purpose — a decomposition that drifts from the composition is the whole
248
+ * bug class this pair exists to prevent.
249
+ *
250
+ * `distance` and `target` are NOT recovered: a rotation about the eye or the
251
+ * centre changes neither, so `rotateOrbit` carries them through untouched.
252
+ *
253
+ * elevation = asin(−forward.y) (always in [−90, 90])
254
+ * azimuth = atan2(−forward.x, −forward.z)
255
+ * roll = the signed angle from the UNROLLED right/up frame to the
256
+ * basis's own right, measured about `forward`.
257
+ *
258
+ * Because elevation is always returned in [−90, 90], pitching OVER the pole
259
+ * re-charts automatically as the coordinated (azimuth+180, elevation
260
+ * reflected, roll+180) transition — the ORIENTATION is continuous; only its
261
+ * coordinates change chart. This is what makes "any view can be achieved"
262
+ * true without an elevation clamp anywhere.
263
+ *
264
+ * ## Pole convention (documented, STATELESS, total — and it never throws)
265
+ *
266
+ * Inside the band |cos(elevation)| < POLE_EPS the (azimuth, roll) split is
267
+ * ill-conditioned — but the ill-conditioned direction IS the gauge direction:
268
+ * `(az+δ, roll+δ)` composes to the SAME orientation. So the APPLIED VIEW is
269
+ * stable to floating-point epsilon straight through the pole; only the
270
+ * REPORTED azimuth/roll numbers may jump along the gauge.
271
+ *
272
+ * The gauge is fixed statelessly: **roll := 0, and azimuth absorbs the whole
273
+ * screen-spin**, read off `right`'s horizontal projection —
274
+ * `azimuth = atan2(right.z, −right.x)`. That is exactly the off-pole formula's
275
+ * own limit at roll = 0, so the convention is continuous with the generic
276
+ * branch rather than an arbitrary snap, and `_orbitCameraBasis` composes it
277
+ * back exactly (its pole branch uses the same limit).
278
+ *
279
+ * A chart singularity is not a contract violation and a throw mid-drag is a
280
+ * grenade, so this function is TOTAL and never throws. FAIL-LOUD lives at the
281
+ * INPUTS instead: `setOrbit` / `look` / `move` reject non-finite values.
282
+ *
283
+ * @param {{forward:{x,y,z}, right:{x,y,z}, up:{x,y,z}}} basis
284
+ * @returns {{azimuth:number, elevation:number, roll:number}} degrees
285
+ */
286
+ function _decomposeOrbitBasis(basis) {
287
+ var f = basis.forward, rt = basis.right;
288
+ var sinEl = -f.y;
289
+ if (sinEl > 1) { sinEl = 1; }
290
+ if (sinEl < -1) { sinEl = -1; }
291
+ var elevation = Math.asin(sinEl) * RAD;
292
+ var horiz = Math.sqrt(f.x * f.x + f.z * f.z); // = |cos(elevation)|
293
+
294
+ if (horiz < POLE_EPS) {
295
+ // POLE: gauge-fixed — roll 0, azimuth carries the screen-spin.
296
+ return {
297
+ azimuth: _wrapDeg(Math.atan2(rt.z, -rt.x) * RAD),
298
+ elevation: elevation,
299
+ roll: 0
300
+ };
301
+ }
302
+
303
+ var azimuth = Math.atan2(-f.x, -f.z) * RAD;
304
+ // The UNROLLED reference frame at this (azimuth, elevation).
305
+ var right0 = _norm(_cross(f, UP_HINT));
306
+ var up0 = _cross(right0, f);
307
+ // right = cos(roll)·right0 − sin(roll)·up0 (right-handed about forward)
308
+ var roll = Math.atan2(-_dot(rt, up0), _dot(rt, right0)) * RAD;
309
+ return { azimuth: _wrapDeg(azimuth), elevation: elevation, roll: _wrapDeg(roll) };
310
+ }
311
+
312
+ // ── The interactive rotation: compose → rotate → decompose ──────────────
313
+
314
+ /**
315
+ * `rotateOrbit(bag, {spin, pitch}, w, h)` → a NEW bag (pure; mutates nothing).
316
+ *
317
+ * The SCREEN-RELATIVE, INCREMENTAL rotation a LOOK drag produces — the peer
318
+ * of `_orbitTranslate` (which is what a MOVE drag produces). Both deltas are
319
+ * in DEGREES and both are applied in the camera's OWN frame:
320
+ *
321
+ * `spin` — rotate about `forward` (the view axis): the picture rolls.
322
+ * `pitch` — rotate about `right` (screen-horizontal): the view tilts up
323
+ * /down, orbiting the centre at a FIXED radius.
324
+ *
325
+ * Successive screen-relative rotations do NOT compose the way three Euler
326
+ * angles applied in a fixed order do — which is exactly why this goes through
327
+ * the orthonormal frame: compose the bag into a basis, rotate the FRAME, then
328
+ * decompose back. `azimuth` is an OUTPUT of that decomposition (the CEO's
329
+ * "azimuth gets computed as a function of the two"), while remaining a
330
+ * writable declarative coordinate through `setOrbit`. `distance` and `target`
331
+ * ride through untouched — a LOOK is never a translation.
332
+ *
333
+ * Roll WRAPS (mod 360). It has no pole, so it is never clamped: a clamp would
334
+ * make the picture stop rolling part-way, which reads as "roll is broken at
335
+ * the extremes".
336
+ *
337
+ * @param {object} orbit the current bag
338
+ * @param {{spin?:number, pitch?:number}} delta degrees
339
+ * @param {number} width scene width in engine pixels
340
+ * @param {number} height scene height in engine pixels
341
+ * @returns {object} a new bag `{azimuth, elevation, distance, roll, target}`
342
+ */
343
+ function _rotateOrbit(orbit, delta, width, height) {
344
+ var spin = (delta && typeof delta.spin === 'number') ? delta.spin : 0;
345
+ var pitch = (delta && typeof delta.pitch === 'number') ? delta.pitch : 0;
346
+ var b = _orbitCameraBasis(orbit, width, height);
347
+ var forward = b.forward, right = b.right, up = b.up;
348
+
349
+ if (spin) {
350
+ var s = spin * DEG;
351
+ right = _rotateAbout(right, forward, s);
352
+ up = _rotateAbout(up, forward, s);
353
+ }
354
+ if (pitch) {
355
+ var p = pitch * DEG;
356
+ forward = _rotateAbout(forward, right, p);
357
+ up = _rotateAbout(up, right, p);
358
+ }
359
+ // Re-orthonormalize: many small incremental rotations accumulate FP drift.
360
+ forward = _norm(forward);
361
+ right = _norm(right);
362
+ var ang = _decomposeOrbitBasis({ forward: forward, right: right, up: up });
363
+ return {
364
+ azimuth: ang.azimuth,
365
+ elevation: ang.elevation,
366
+ distance: orbit.distance,
367
+ roll: ang.roll,
368
+ target: orbit.target || null
369
+ };
370
+ }
371
+
372
+ // ── The interactive translation (the FPS walk) ──────────────────────────
373
+
374
+ /**
375
+ * SCREEN-RELATIVE translation of the orbit TARGET — the FPS walk, in the
376
+ * camera's own frame. PURE (returns a new target; mutates nothing) so it is
377
+ * Node-testable with no live THREE instance, and so the one piece of orbit
378
+ * math a MOVE pad needs lives HERE at the camera authority rather than being
379
+ * re-derived inside a widget.
380
+ *
381
+ * `forward` walks along the FULL view direction (positive = into the picture);
382
+ * `strafe` slides along screen-right. Both in ENGINE PIXELS, both taken from
383
+ * the ONE basis, so a positive strafe reads as rightward on screen at every
384
+ * azimuth AND at every roll.
385
+ *
386
+ * **MOVE is ALWAYS screen-relative** (CEO 2026-07-28). The walk is NOT pinned
387
+ * to a ground plane: walking forward while pitched down DESCENDS — this is a
388
+ * fly control, not an FPS ground walk. (The earlier `y: centre.y` pin was the
389
+ * algebraic residue of a retired ground-plane promise; keeping it would have
390
+ * silently zeroed the vertical motion, and at roll≠0 it would have made
391
+ * "screen-right" and "world-horizontal" contradict each other.)
392
+ */
393
+ function _orbitTranslate(orbit, width, height, forward, strafe) {
394
+ var c = _orbitCenter(orbit, width, height);
395
+ var b = _orbitCameraBasis(orbit, width, height);
396
+ return {
397
+ x: c.x + forward * b.forward.x + strafe * b.right.x,
398
+ y: c.y + forward * b.forward.y + strafe * b.right.y,
399
+ z: c.z + forward * b.forward.z + strafe * b.right.z
400
+ };
401
+ }
402
+
403
+ // ── Camera-relative CARRY transforms (Q-A5-CAP) ─────────────────────────
404
+
405
+ /** Camera-relative CARRY transforms (Q-A5-CAP) — the world↔camera-space pair, PURE
406
+ * free functions off _orbitCameraBasis (the ONE camera authority) so they are Node-
407
+ * testable with no live THREE/WebGL instance (mirrors _screenToWorldThree). The
408
+ * getCameraControl handle's cameraOffsetFromWorld/worldFromCameraOffset methods are
409
+ * thin closures over these with the live _orbit/_width/_height. Units = engine px.
410
+ * They read the ROLLED basis, so a carried object stays pinned in view through a
411
+ * roll exactly as it does through an azimuth change. */
412
+ function _cameraOffsetFromWorld(orbit, width, height, worldPoint) {
413
+ if (!worldPoint) { return null; }
414
+ var b = _orbitCameraBasis(orbit, width, height);
415
+ var dx = worldPoint[0] - b.pos.x, dy = worldPoint[1] - b.pos.y, dz = worldPoint[2] - b.pos.z;
416
+ return {
417
+ right: dx * b.right.x + dy * b.right.y + dz * b.right.z,
418
+ up: dx * b.up.x + dy * b.up.y + dz * b.up.z,
419
+ forward: dx * b.forward.x + dy * b.forward.y + dz * b.forward.z
420
+ };
421
+ }
422
+ function _worldFromCameraOffset(orbit, width, height, off) {
423
+ if (!off) { return null; }
424
+ var b = _orbitCameraBasis(orbit, width, height);
425
+ var r = off.right || 0, u = off.up || 0, f = off.forward || 0;
426
+ return [
427
+ b.pos.x + r * b.right.x + u * b.up.x + f * b.forward.x,
428
+ b.pos.y + r * b.right.y + u * b.up.y + f * b.forward.y,
429
+ b.pos.z + r * b.right.z + u * b.up.z + f * b.forward.z
430
+ ];
431
+ }
432
+
433
+ // ── screenToWorld() analytic camera math (OPTIONAL capability, S5b) ──────
434
+ //
435
+ // WS Studio Controls Realignment §7 r2 item 4 / §2.5.7 "3D drag": "three:
436
+ // raycast against the object's plane [at its z]". Rather than reach for a
437
+ // LIVE `THREE.Raycaster`/`THREE.Plane` (which would make this untestable in
438
+ // plain Node — makeThreeProvider() itself throws fail-loud without a real
439
+ // WebGL/THREE global), this is a PURE re-derivation of the exact same
440
+ // pinhole-camera geometry `_applyOrbit` uses to POSITION the camera — the
441
+ // SAME basis function, the same fixed vertical FOV — so the two are
442
+ // geometrically identical by construction, while this half stays
443
+ // THREE-instance-free and unit-testable with plain `{x,y,z}` objects (see
444
+ // `test/screen-to-world.test.mjs`). The provider's own `screenToWorld`
445
+ // method is a THIN wrapper calling these free functions with its live
446
+ // `_orbit`/`_width`/`_height` — it does not re-derive the math.
447
+
448
+ /** World-space ray direction (unit vector) for a normalized-device-coordinate
449
+ * screen point `(ndcX, ndcY)` (both in [-1,1], y-up) through `basis`'s camera,
450
+ * given the fixed vertical FOV and the canvas aspect ratio. Standard pinhole-
451
+ * camera ray generation — the SAME math `THREE.Raycaster.setFromCamera` performs
452
+ * internally for a `PerspectiveCamera`, re-derived here without a THREE instance. */
453
+ function _rayDirForNdc(basis, ndcX, ndcY, fovDeg, aspect) {
454
+ var tanHalfFov = Math.tan((fovDeg * DEG) / 2);
455
+ var dx = ndcX * tanHalfFov * aspect;
456
+ var dy = ndcY * tanHalfFov;
457
+ var dirX = basis.forward.x + dx * basis.right.x + dy * basis.up.x;
458
+ var dirY = basis.forward.y + dx * basis.right.y + dy * basis.up.y;
459
+ var dirZ = basis.forward.z + dx * basis.right.z + dy * basis.up.z;
460
+ var len = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ) || 1;
461
+ return { x: dirX / len, y: dirY / len, z: dirZ / len };
462
+ }
463
+
464
+ // S5b tester-found BLOCKING regression (fix-loop): a live down-barrel drag
465
+ // (the DEFAULT 3D camera preset — looks ~straight down +X) exploded a
466
+ // dragged LO's rect to ~1e15. Root cause: `dir` is a UNIT vector, so
467
+ // `dir.z` IS `cos(angle between the ray and the plane normal)`; the
468
+ // ORIGINAL `Math.abs(dir.z) < 1e-9` guard only caught the ray being
469
+ // EXACTLY parallel (dir.z ~ 1e-17, floating-point zero) — a genuinely
470
+ // common NEAR-parallel case (down-barrel's screen-centre column has
471
+ // dir.z passing smoothly through small-but-not-tiny values like 1e-4 as
472
+ // the mouse crosses the vertical centreline) produced a t = numerator/
473
+ // dir.z with dir.z just above that razor-thin threshold, giving a huge
474
+ // but FINITE (never NaN/Infinity, so never itself caught) t — and the
475
+ // resulting world point blew up proportionally. Two independent guards
476
+ // now close this, per the tester's own fix recipe:
477
+ // (1) MIN_RAY_PLANE_COS — an ANGLE threshold (dir.z is literally a
478
+ // cosine), not a near-zero-only threshold: reject any ray within
479
+ // ~3° of parallel-to-the-plane, never just "exactly" parallel.
480
+ // (2) A scene-scale-aware SANITY BOUND at the `_screenToWorldThree`
481
+ // orchestration layer (which alone knows width/height) — reject
482
+ // ANY result implausibly far beyond the scene extent, so a
483
+ // degenerate point can NEVER reach `updateObjectProperty` even if
484
+ // a future edge case slipped past guard (1).
485
+
486
+ /** Intersect a ray (`origin`,`dir`) with the world plane `z = planeZ`.
487
+ * Returns `{x,y}` (z === planeZ by construction) or `null` when the ray is
488
+ * NEAR-parallel to the plane (see MIN_RAY_PLANE_COS above — this is an
489
+ * ANGLE guard, not merely an exact-zero check) or the intersection is
490
+ * BEHIND the camera (t < 0). Pure math — no scene-scale awareness here
491
+ * (that lives in `_screenToWorldThree`'s own sanity-bound check below,
492
+ * which is the layer that actually knows width/height). */
493
+ function _intersectRayWithZPlane(origin, dir, planeZ) {
494
+ if (Math.abs(dir.z) < MIN_RAY_PLANE_COS) { return null; }
495
+ var t = (planeZ - origin.z) / dir.z;
496
+ // T_EPS tolerates floating-point noise right at t~0 (e.g. `origin.z`
497
+ // for a down-barrel orbit is `distance * cos(el) * cos(90°)`, and
498
+ // `Math.cos(Math.PI/2)` is NOT exactly 0 in IEEE-754 — it is
499
+ // ~6.12e-17 — so a worldZ=0 target from a down-barrel camera can
500
+ // resolve to a genuinely-in-front intersection with `t` a few
501
+ // e-13 NEGATIVE purely from that noise). Only a MEANINGFULLY
502
+ // negative t (the intersection is actually behind the camera) is
503
+ // rejected; a noise-scale negative t is treated as ~0.
504
+ if (t < -1e-6) { return null; }
505
+ return { x: origin.x + dir.x * t, y: origin.y + dir.y * t };
506
+ }
507
+
508
+ /**
509
+ * screenToWorld — the full pure pipeline: canvas CSS pixel → NDC → world-ray
510
+ * → intersection with the z=worldZ plane → scene-scale sanity bound.
511
+ * Exported (alongside the three sub-steps above) so the LIVE provider
512
+ * method is a one-line call and the geometry is independently
513
+ * unit-testable without a THREE global. Returns `null` on EITHER a
514
+ * near-parallel/behind-camera ray (see `_intersectRayWithZPlane`) OR a
515
+ * result beyond `SANITY_EXTENT_FACTOR` × the scene's own width/height —
516
+ * the caller (AuthorView.jsx's overlay drag) treats a `null` result as
517
+ * "skip this frame's write" (never a fallback to a DIFFERENT, WRONG
518
+ * coordinate transform, and never a write of the degenerate point).
519
+ *
520
+ * It reads the ROLLED basis, so a drag under a rolled camera resolves along
521
+ * the screen axes the user is actually looking at.
522
+ */
523
+ // OPT-IN EXTENSION (`fovYDeg`, trailing, defaulted to the module's own CAMERA_FOV_DEG):
524
+ // byte-identical for every existing caller — the live provider's camera IS at
525
+ // CAMERA_FOV_DEG, so omitting the argument reproduces the previous behaviour exactly.
526
+ // It exists so the OFFLINE unproject (`unprojectStill.js`) can pass a captured scene's
527
+ // `projection.fovY` as DATA through this SAME composition rather than re-deriving the
528
+ // pinhole. One mapping authority: a consumer's hand-written unproject drifting from the
529
+ // provider's is the schema-mirror bug applied to MATH.
530
+ function _screenToWorldThree(orbit, width, height, canvasCssX, canvasCssY, canvasCssWidth, canvasCssHeight, worldZ, fovYDeg) {
531
+ var cw = canvasCssWidth || width;
532
+ var ch = canvasCssHeight || height;
533
+ var fov = (typeof fovYDeg === 'number' && isFinite(fovYDeg) && fovYDeg > 0) ? fovYDeg : CAMERA_FOV_DEG;
534
+ var ndcX = (canvasCssX / cw) * 2 - 1;
535
+ var ndcY = -((canvasCssY / ch) * 2 - 1);
536
+ var basis = _orbitCameraBasis(orbit, width, height);
537
+ var dir = _rayDirForNdc(basis, ndcX, ndcY, fov, width / height);
538
+ var hit = _intersectRayWithZPlane(basis.pos, dir, (typeof worldZ === 'number') ? worldZ : 0);
539
+ if (!hit) { return null; }
540
+ var bound = SANITY_EXTENT_FACTOR * Math.max(width, height, 1);
541
+ if (!isFinite(hit.x) || !isFinite(hit.y) || Math.abs(hit.x) > bound || Math.abs(hit.y) > bound) {
542
+ return null;
543
+ }
544
+ return hit;
545
+ }
546
+
547
+ // ── Export ──────────────────────────────────────────────────────────────
548
+ //
549
+ // The `_`-prefixed names are preserved verbatim from their previous home
550
+ // inside three-provider.js: this was a MOVE, not a rewrite, and renaming
551
+ // during a relocation makes the relocation unreviewable. They read as
552
+ // "renderer-internal geometry primitives", which is exactly what they are —
553
+ // the PUBLIC camera surface is the `getCameraControl()` handle.
554
+
555
+ export {
556
+ CAMERA_FOV_DEG,
557
+ MIN_RAY_PLANE_COS,
558
+ SANITY_EXTENT_FACTOR,
559
+ UP_HINT,
560
+ POLE_EPS,
561
+ _orbitCenter,
562
+ _orbitCameraBasis,
563
+ _decomposeOrbitBasis,
564
+ _rotateOrbit,
565
+ _orbitTranslate,
566
+ _cameraOffsetFromWorld,
567
+ _worldFromCameraOffset,
568
+ _rayDirForNdc,
569
+ _intersectRayWithZPlane,
570
+ _screenToWorldThree
571
+ };