feat: render programmable 3d scenes cameras and picking

This commit is contained in:
2026-11-10 23:19:26 +08:00
parent 040b779f5f
commit ad2c01bc29
33 changed files with 2463 additions and 42 deletions
+91
View File
@@ -665,3 +665,94 @@ fn explicit_viewport_camera_crosses_http_and_rejects_bad_anchors_without_faultin
assert!(namespace.read().unwrap().document.frame.fault.is_none());
}
}
#[test]
fn scene3d_controllers_cross_http_and_can_be_replaced_by_endor_code() {
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
let namespace = Arc::new(RwLock::new(NamespaceState {
viewport: Some(crate::layout::Size {
width: 400.,
height: 300.,
}),
..NamespaceState::default()
}));
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
let client = TestServer {
address: server.server_addr().to_ip().unwrap(),
server,
api: Some(api),
};
let source = r##"
class PanOnly {
function update(self: PanOnly, camera: Object, gesture: CameraGesture): Unit { camera.translate(gesture.dx, gesture.dy, 0) }
}
view = Viewport3D(200,100,Scene3D(Box3D(2,2,2,"#CC4455")))
page = Page()
page.render(view)
"##;
let response = client.request("PUT", "/v1/namespaces/app.main/code", source);
assert_eq!(response.0, 200, "{}", response.2);
let gesture = |kind: &str, delta: [f32; 2], point: Option<[f32; 2]>| {
let current = namespace.read().unwrap();
let frame = &current.document.frame;
serde_json::to_string(&crate::events::Input {
window: 1,
session: frame.session,
layout_version: frame.layout_version,
mount: frame
.mounts
.iter()
.find(|mount| mount.scene3d_viewport)
.unwrap()
.id,
kind: kind.into(),
delta: Some(delta),
point,
press_point: None,
tab_drop: None,
screen_position: None,
})
.unwrap()
};
let event_path = "/v1/namespaces/app.main/events";
let zoom = gesture("scene-zoom", [2., 0.], Some([100., 50.]));
let response = client.request("POST", event_path, &zoom);
assert_eq!(response.0, 200, "{}", response.2);
assert_eq!(client.request("POST", event_path, &zoom).0, 409);
let value = |expression: &str| {
let response = client.request("POST", "/v1/namespaces/app.main/repl", expression);
assert_eq!(response.0, 200, "{}", response.2);
serde_json::from_str::<serde_json::Value>(&response.2).unwrap()["value"]
.as_str()
.unwrap()
.to_owned()
};
assert_eq!(value("view.camera.z"), "2.5");
let response = client.request("POST", event_path, &gesture("scene-orbit", [20., 0.], None));
assert_eq!(response.0, 200, "{}", response.2);
let x = value("view.camera.x").parse::<f32>().unwrap();
assert!((x - 2.5 * 0.1f32.sin()).abs() < 0.0001);
value("view.camera = OrthographicCamera()");
let response = client.request(
"POST",
event_path,
&gesture("scene-zoom", [2., 0.], Some([100., 50.])),
);
assert_eq!(response.0, 200, "{}", response.2);
assert_eq!(value("view.camera.height"), "2.5");
value("view.controller = PanOnly()");
let response = client.request("POST", event_path, &gesture("scene-pan", [7., 0.], None));
assert_eq!(response.0, 200, "{}", response.2);
assert_eq!(value("view.camera.x"), "7");
let response = client.request(
"POST",
event_path,
&gesture("scene-zoom", [2., 0.], Some([250., 50.])),
);
assert_eq!(response.0, 400);
assert!(namespace.read().unwrap().document.frame.fault.is_none());
value("view.controller = \"none\"");
let response = client.request("POST", event_path, &gesture("scene-pan", [7., 0.], None));
assert_eq!(response.0, 409);
assert!(namespace.read().unwrap().document.frame.fault.is_none());
}