feat(perception): integrate calibrated operator pipeline
Add calibrated K1 projection, recorded and near-live perception qualification, unified Rerun operator layers, bounded replay admission, audited viewer controls, worker experiments, and lab evidence.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# NODE.DC Rerun web viewer 0.34.1
|
||||
|
||||
This directory contains the audited Mission Core camera-controller override for
|
||||
`@rerun-io/web-viewer` 0.34.1. It changes only the native orbital zoom behavior:
|
||||
|
||||
- the pointer ray selects an anchor on the current focus plane;
|
||||
- eye position and look target scale around that anchor, so the point under the
|
||||
cursor remains under the cursor;
|
||||
- after Rerun's `0.02 m` near-plane safety radius is reached, excess zoom becomes
|
||||
a cursor-directed dolly scaled by the scene's navigation speed instead of
|
||||
silently ignoring the wheel or moving by imperceptible millimeters;
|
||||
- first-person movement, orbit rotation, panning, WASD and Rerun's zoom-out cap
|
||||
are unchanged.
|
||||
|
||||
## Source identity
|
||||
|
||||
- Upstream: `rerun-io/rerun`
|
||||
- Tag: `0.34.1`
|
||||
- Commit: `4efb18f17f6f0e41985cda99a2bdcd012febc8d5`
|
||||
- Patched file: `crates/viewer/re_view_spatial/src/eye.rs`
|
||||
- Patch: `NODEDC_ZOOM_TO_CURSOR.patch`
|
||||
- Rust: `1.92.0`
|
||||
- Binaryen / `wasm-opt`: `117` (the version pinned by Rerun's `pixi.lock`)
|
||||
- Build image: `rust:1.92-bookworm`
|
||||
- Build image digest:
|
||||
`sha256:e90e846de4124376164ddfbaab4b0774c7bdeef5e738866295e5a90a34a307a2`
|
||||
- Build date: `2026-07-22` (`Europe/Moscow`)
|
||||
|
||||
## Reproduction
|
||||
|
||||
Apply the patch to the exact upstream commit, then run Rerun's own builder:
|
||||
|
||||
```sh
|
||||
git apply NODEDC_ZOOM_TO_CURSOR.patch
|
||||
cargo test -p re_view_spatial --lib eye::tests:: -- --nocapture
|
||||
cargo run -p re_dev_tools -- build-web-viewer \
|
||||
--release -g \
|
||||
--target no-modules-base \
|
||||
--no-default-features \
|
||||
--features map_view \
|
||||
-o rerun_js/web-viewer
|
||||
```
|
||||
|
||||
The container also needs Binaryen `117` for Rerun's final `wasm-opt -O2` step.
|
||||
Binaryen `108` from Debian 12 must not be used: it produced a module whose
|
||||
`externref` table could not grow during initialization. The
|
||||
generated `re_viewer.js` is transformed with
|
||||
`scripts/transform-rerun-web-viewer-glue.mjs`, which mirrors Rerun 0.34.1's
|
||||
`rerun_js/web-viewer/build-wasm.mjs` no-modules wrapper and teardown guards.
|
||||
|
||||
## Verified artifacts
|
||||
|
||||
| Artifact | SHA-256 |
|
||||
| --- | --- |
|
||||
| `re_viewer_bg.nodedc.wasm` | `38d19bac06b7c3b8e549489469cf7c4a24f319ec953849e4656b5827a6c105bb` |
|
||||
| raw generated `re_viewer.js` | `cc196a93c5be972c801d46be4dc9934f7f042eb62941f0aa0678f1c8416c6874` |
|
||||
| `re_viewer.nodedc.js` | `0f7b76c9f24cbd8437021b5d37499894aeadc586183e422ebc82ef556d7b8339` |
|
||||
|
||||
The focused Rust suite completed with `5 passed, 0 failed`, including the
|
||||
orbit-to-dolly boundary case. A Node `initSync`
|
||||
smoke test completed successfully, including the `externref` table-growth step.
|
||||
The Mission Core Node suite also checks both artifact hashes and verifies that
|
||||
every `wasm.*` reference in the JavaScript glue exists in the paired WASM
|
||||
exports.
|
||||
|
||||
`scripts/patch-rerun-web-viewer.mjs` installs the pair after npm extracts the
|
||||
official package. It accepts only the known published or NODE.DC hashes and
|
||||
fails closed on any other package contents.
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
diff --git a/crates/viewer/re_view_spatial/src/eye.rs b/crates/viewer/re_view_spatial/src/eye.rs
|
||||
index e59b311..3f33259 100644
|
||||
--- a/crates/viewer/re_view_spatial/src/eye.rs
|
||||
+++ b/crates/viewer/re_view_spatial/src/eye.rs
|
||||
@@ -512,8 +512,85 @@ impl EyeController {
|
||||
}
|
||||
}
|
||||
|
||||
+ /// Returns the world-space ray under the pointer.
|
||||
+ ///
|
||||
+ /// Keeping this calculation local to the eye controller lets orbital zoom use the pointer
|
||||
+ /// synchronously. GPU picking arrives a frame later and would make the zoom pivot visibly lag.
|
||||
+ fn pointer_ray_direction(&self, rect: Rect, pointer: egui::Pos2) -> Option<Vec3> {
|
||||
+ if !rect.contains(pointer) || rect.width() <= 0.0 || rect.height() <= 0.0 {
|
||||
+ return None;
|
||||
+ }
|
||||
+
|
||||
+ let fov_y = self.fov_y.unwrap_or(Eye::DEFAULT_FOV_Y);
|
||||
+ let aspect_ratio = rect.width() / rect.height();
|
||||
+ let focal_scale = (fov_y * 0.5).tan();
|
||||
+ let x = (2.0 * (pointer.x - rect.left()) / rect.width() - 1.0)
|
||||
+ * focal_scale
|
||||
+ * aspect_ratio;
|
||||
+ let y = (1.0 - 2.0 * (pointer.y - rect.top()) / rect.height()) * focal_scale;
|
||||
+
|
||||
+ (self.rotation() * vec3(x, y, -1.0)).try_normalize()
|
||||
+ }
|
||||
+
|
||||
+ /// Zoom an orbital eye around the point under the pointer on the current focus plane.
|
||||
+ ///
|
||||
+ /// The position and look target are scaled around the same anchor. This preserves the
|
||||
+ /// projected pointer position while retaining the existing orbit direction and controls.
|
||||
+ /// Once the near-plane safety radius is reached, excess zoom becomes a cursor-directed dolly
|
||||
+ /// measured in the scene's navigation speed instead of near-plane millimeters.
|
||||
+ fn zoom_orbit_towards_pointer(
|
||||
+ &mut self,
|
||||
+ zoom_factor: f32,
|
||||
+ max_radius: f32,
|
||||
+ rect: Rect,
|
||||
+ pointer: Option<egui::Pos2>,
|
||||
+ ) {
|
||||
+ let radius = self.radius();
|
||||
+ if !radius.is_finite() || radius <= f32::MIN_POSITIVE {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ let requested_radius = radius / zoom_factor;
|
||||
+ let new_radius = requested_radius.clamp(Self::MIN_ORBIT_DISTANCE, max_radius);
|
||||
+ let scale = new_radius / radius;
|
||||
+
|
||||
+ let ray_direction = pointer.and_then(|pointer| self.pointer_ray_direction(rect, pointer));
|
||||
+ if let Some(ray_direction) = ray_direction {
|
||||
+ let forward = self.fwd();
|
||||
+ let denominator = ray_direction.dot(forward);
|
||||
+ if denominator > 1.0e-4 {
|
||||
+ let anchor = self.pos + ray_direction * (radius / denominator);
|
||||
+ self.pos = anchor + (self.pos - anchor) * scale;
|
||||
+ self.look_target = anchor + (self.look_target - anchor) * scale;
|
||||
+
|
||||
+ if requested_radius < Self::MIN_ORBIT_DISTANCE {
|
||||
+ // Shrinking the remaining 2 cm orbit radius consumes only part of this input.
|
||||
+ // Hand the logarithmic remainder to the same scene-scaled speed used by WASD
|
||||
+ // and first-person scroll. Basing this on the near-plane remainder itself made
|
||||
+ // each wheel event move by millimeters and felt indistinguishable from a hard
|
||||
+ // zoom limit on building- and map-scale recordings.
|
||||
+ let orbit_zoom_factor = (radius / Self::MIN_ORBIT_DISTANCE).max(1.0);
|
||||
+ let remaining_zoom_factor = zoom_factor / orbit_zoom_factor;
|
||||
+ if remaining_zoom_factor > 1.0 && remaining_zoom_factor.is_finite() {
|
||||
+ let dolly = remaining_zoom_factor.ln() * self.speed as f32;
|
||||
+ self.pos += ray_direction * dolly;
|
||||
+ self.look_target += ray_direction * dolly;
|
||||
+ }
|
||||
+ }
|
||||
+ self.did_interact = true;
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Pointer data can be absent for synthetic zoom events. Preserve Rerun's centered zoom
|
||||
+ // behavior in that case instead of dropping the input.
|
||||
+ self.pos = self.look_target - self.fwd() * new_radius;
|
||||
+ self.did_interact = true;
|
||||
+ }
|
||||
+
|
||||
/// Handle zoom/scroll input.
|
||||
- fn handle_zoom(&mut self, egui_ctx: &egui::Context, scene_bounding_box: &macaw::BoundingBox) {
|
||||
+ fn handle_zoom(&mut self, response: &egui::Response, scene_bounding_box: &macaw::BoundingBox) {
|
||||
+ let egui_ctx = &response.ctx;
|
||||
let zoom_factor = egui_ctx.input(|input| {
|
||||
// egui's default horizontal_scroll_modifier is shift, which is also our speed-up modifier.
|
||||
// This means that a user who wants to speed up scroll-to-zoom will generate a horizontal scroll delta.
|
||||
@@ -528,22 +605,12 @@ impl EyeController {
|
||||
|
||||
match self.kind {
|
||||
Eye3DKind::Orbital => {
|
||||
- let radius = self.pos.distance(self.look_target);
|
||||
-
|
||||
// Cap zoom-out against the scene bounding box. If we're already past the cap
|
||||
// (e.g. right after loading) use the current radius instead — no snap-back.
|
||||
+ let radius = self.radius();
|
||||
let max_radius = max_orbital_radius(scene_bounding_box).max(radius);
|
||||
- let new_radius = (radius / zoom_factor).clamp(Self::MIN_ORBIT_DISTANCE, max_radius);
|
||||
-
|
||||
- // The user may be scrolling to move the camera closer, but are not realizing
|
||||
- // the radius is now tiny.
|
||||
- // TODO(emilk): inform the users somehow that scrolling won't help, and that they should use WSAD instead.
|
||||
- // It might be tempting to start moving the camera here on scroll, but that would is bad for other reasons.
|
||||
-
|
||||
- if f32::MIN_POSITIVE < new_radius {
|
||||
- self.pos = self.look_target - self.fwd() * new_radius;
|
||||
- self.did_interact = true;
|
||||
- }
|
||||
+ let pointer = response.ctx.pointer_latest_pos();
|
||||
+ self.zoom_orbit_towards_pointer(zoom_factor, max_radius, response.rect, pointer);
|
||||
}
|
||||
Eye3DKind::FirstPerson => {
|
||||
// Move along the forward axis when zooming in first person mode.
|
||||
@@ -687,7 +754,7 @@ impl EyeController {
|
||||
self.handle_drag(response, drag_threshold);
|
||||
|
||||
if response.hovered() {
|
||||
- self.handle_zoom(&response.ctx, scene_bounding_box);
|
||||
+ self.handle_zoom(response, scene_bounding_box);
|
||||
}
|
||||
|
||||
if response.has_focus() {
|
||||
@@ -1249,3 +1316,103 @@ impl EyeState {
|
||||
Ok(eye)
|
||||
}
|
||||
}
|
||||
+
|
||||
+#[cfg(test)]
|
||||
+mod tests {
|
||||
+ use super::*;
|
||||
+
|
||||
+ fn orbital_controller(pos: Vec3, look_target: Vec3) -> EyeController {
|
||||
+ EyeController {
|
||||
+ pos,
|
||||
+ look_target,
|
||||
+ kind: Eye3DKind::Orbital,
|
||||
+ speed: 1.0,
|
||||
+ eye_up: Vec3::Z,
|
||||
+ fov_y: Some(Eye::DEFAULT_FOV_Y),
|
||||
+ did_interact: false,
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ fn test_rect() -> Rect {
|
||||
+ Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))
|
||||
+ }
|
||||
+
|
||||
+ fn assert_vec3_close(actual: Vec3, expected: Vec3) {
|
||||
+ assert!(
|
||||
+ actual.abs_diff_eq(expected, 1.0e-5),
|
||||
+ "actual={actual:?}, expected={expected:?}"
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn centered_pointer_keeps_the_orbit_target() {
|
||||
+ let rect = test_rect();
|
||||
+ let mut controller = orbital_controller(vec3(0.0, -10.0, 0.0), Vec3::ZERO);
|
||||
+
|
||||
+ controller.zoom_orbit_towards_pointer(2.0, 100.0, rect, Some(rect.center()));
|
||||
+
|
||||
+ assert_vec3_close(controller.pos, vec3(0.0, -5.0, 0.0));
|
||||
+ assert_vec3_close(controller.look_target, Vec3::ZERO);
|
||||
+ assert!(controller.did_interact);
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn off_center_pointer_stays_on_the_same_view_ray() {
|
||||
+ let rect = test_rect();
|
||||
+ let pointer = egui::pos2(650.0, 240.0);
|
||||
+ let mut controller = orbital_controller(vec3(0.0, -10.0, 0.0), Vec3::ZERO);
|
||||
+ let original_ray = controller
|
||||
+ .pointer_ray_direction(rect, pointer)
|
||||
+ .expect("test pointer must produce a ray");
|
||||
+ let forward = controller.fwd();
|
||||
+ let anchor = controller.pos
|
||||
+ + original_ray * (controller.radius() / original_ray.dot(forward));
|
||||
+
|
||||
+ controller.zoom_orbit_towards_pointer(2.0, 100.0, rect, Some(pointer));
|
||||
+
|
||||
+ assert!((controller.radius() - 5.0).abs() < 1.0e-5);
|
||||
+ let ray_after = (anchor - controller.pos).normalize();
|
||||
+ assert!(ray_after.dot(original_ray) > 0.99999);
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn near_limit_hands_excess_zoom_to_cursor_directed_dolly() {
|
||||
+ let rect = test_rect();
|
||||
+ let radius = EyeController::MIN_ORBIT_DISTANCE;
|
||||
+ let mut controller = orbital_controller(vec3(0.0, -radius, 0.0), Vec3::ZERO);
|
||||
+ let old_pos = controller.pos;
|
||||
+ let old_target = controller.look_target;
|
||||
+
|
||||
+ controller.zoom_orbit_towards_pointer(2.0, 100.0, rect, Some(rect.center()));
|
||||
+
|
||||
+ assert!((controller.radius() - radius).abs() < 1.0e-6);
|
||||
+ assert_vec3_close(controller.pos, old_pos + Vec3::Y * 2.0_f32.ln());
|
||||
+ assert_vec3_close(
|
||||
+ controller.look_target,
|
||||
+ old_target + Vec3::Y * 2.0_f32.ln(),
|
||||
+ );
|
||||
+ assert_vec3_close(controller.look_target - controller.pos, Vec3::Y * radius);
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn crossing_near_limit_preserves_unconsumed_scene_scaled_zoom() {
|
||||
+ let rect = test_rect();
|
||||
+ let mut controller = orbital_controller(vec3(0.0, -0.03, 0.0), Vec3::ZERO);
|
||||
+
|
||||
+ controller.zoom_orbit_towards_pointer(3.0, 100.0, rect, Some(rect.center()));
|
||||
+
|
||||
+ assert!((controller.radius() - EyeController::MIN_ORBIT_DISTANCE).abs() < 1.0e-6);
|
||||
+ assert_vec3_close(controller.pos, vec3(0.0, 2.0_f32.ln() - 0.02, 0.0));
|
||||
+ assert_vec3_close(controller.look_target, vec3(0.0, 2.0_f32.ln(), 0.0));
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn zoom_out_respects_scene_radius_cap() {
|
||||
+ let rect = test_rect();
|
||||
+ let mut controller = orbital_controller(vec3(0.0, -10.0, 0.0), Vec3::ZERO);
|
||||
+
|
||||
+ controller.zoom_orbit_towards_pointer(0.1, 25.0, rect, Some(rect.center()));
|
||||
+
|
||||
+ assert!((controller.radius() - 25.0).abs() < 1.0e-5);
|
||||
+ }
|
||||
+}
|
||||
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
Reference in New Issue
Block a user