#875: layout views draw camera and light wireframes

Problem: in a 2- or 4-view layout the Top / Front / Right views drew no
camera or light wireframes, so moving a camera showed nothing there
(reported on Discord).

Fix: draw_rigs takes the view camera explicitly and a pane name for its
automation ids; aux_views draws the wireframes of each extra view through
that view's camera, clipped to the pane, with ids
viewer.view.<view>.camera.<id> / .light.<id>.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
vangel76andClaude Opus 5.5 committed 2026-10-11 18:24:57 +02:00
1 parent 67434e9038
commit f19f3a8a39
2 files changed
+58 -6

No files matched your search

+30 -6
View File
@@ -545,6 +545,12 @@ fn aux_views(
let (cw, ch) = (comp.width as f32, comp.height as f32);
let views = app.session.state.views3d.get(&cid).cloned().unwrap_or_default();
let share = app.session.state.share_view_options;
// The camera and light wireframes of each view (as in After Effects, where every view of the
// layout shows them), evaluated on a snapshot of the project.
let snap_project = app.session.project.clone();
let snap_expr = app.session.expr.clone();
let ectx = EvalCtx { project: &snap_project, comp_id: cid, comp, time: t, expr: snap_expr.as_deref(), footage: None };
let selected = app.session.state.selected_layers.clone();
for (i, (r, v)) in aux.into_iter().enumerate() {
p.rect_filled(r, 0.0, bg);
let fit = ((r.width() - 20.0) / cw).min((r.height() - 20.0) / ch).max(0.01);
@@ -578,6 +584,10 @@ fn aux_views(
p.rect_stroke(Rect::from_center_size(cr.center(), cr.size() * k), 0.0, Stroke::new(1.0, Color32::from_white_alpha(120)), StrokeKind::Middle);
}
}
if comp.has_3d() {
let map = ViewerMap { origin: cr.min, zoom: fit, comp: [cw, ch], area: r };
draw_rigs(app, &p.with_clip_rect(r), &map, &ectx, &selected, cam, false, Some(v.id()));
}
let label = if comp.has_3d() { v.label() } else { crate::i18n::tr("Active Camera") };
p.text(r.left_bottom() + vec2(8.0, -8.0), Align2::LEFT_BOTTOM, label, Tokens::ui(11.0), Color32::from_white_alpha(200));
app.auto.add(&format!("viewer.view.{}", v.id()), r, label);
@@ -1050,7 +1060,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Cameras and lights (wireframes) and the 3D view name.
if comp.has_3d() {
draw_rigs(app, &painter, &map, &ectx, &selected);
let through_active = VIEW_CAM.with(|c| c.get()).is_none();
draw_rigs(app, &painter, &map, &ectx, &selected, cam_state(&ectx), through_active, None);
let view = app.session.state.views3d.get(&cid).map(|v| v.current).unwrap_or_default();
let label = match (view, comp.active_camera(time)) {
(View3D::ActiveCamera, Some(c)) => format!("Active Camera ({})", c.name),
@@ -1858,13 +1869,26 @@ fn drag_axes(ctx: &EvalCtx, l: &Layer) -> ([f64; 3], [f64; 3]) {
/// Wireframes of cameras (frustum, point of interest) and lights (position, direction) as seen
/// through the current 3D view.
fn draw_rigs(app: &mut EffectcraftApp, painter: &egui::Painter, map: &ViewerMap, ectx: &EvalCtx, selected: &[LayerId]) {
/// Camera and light wireframes seen through `cam` (`through_active`: `cam` is the active camera,
/// which is then not drawn). `pane` names the automation ids: `None` for the main view
/// (`viewer.camera.<id>`), `Some(view)` for a view of a 2- or 4-view layout
/// (`viewer.view.<view>.camera.<id>`).
#[allow(clippy::too_many_arguments)]
fn draw_rigs(
app: &mut EffectcraftApp,
painter: &egui::Painter,
map: &ViewerMap,
ectx: &EvalCtx,
selected: &[LayerId],
cam: CameraState,
through_active: bool,
pane: Option<&str>,
) {
use effectcraft_engine::geom::Vec3;
use effectcraft_engine::project::{LayerSource, LightKind};
let cam = cam_state(ectx);
let (w, h) = (ectx.comp.width as f64, ectx.comp.height as f64);
let active = ectx.comp.active_camera(ectx.time).map(|c| c.id);
let through_active = VIEW_CAM.with(|c| c.get()).is_none();
let prefix = pane.map(|v| format!("viewer.view.{v}.")).unwrap_or_else(|| "viewer.".to_string());
let proj = |p: Vec3| cam.project(w, h, p).map(|v| map.to_screen([v.x, v.y]));
let seg = |a: Vec3, b: Vec3, stroke: Stroke| {
if let (Some(x), Some(y)) = (proj(a), proj(b)) {
@@ -1901,7 +1925,7 @@ fn draw_rigs(app: &mut EffectcraftApp, painter: &egui::Painter, map: &ViewerMap,
}
}
if let Some(s) = proj(eye) {
app.auto.add(&format!("viewer.camera.{}", l.id.0), Rect::from_center_size(s, vec2(14.0, 14.0)), &l.name);
app.auto.add(&format!("{prefix}camera.{}", l.id.0), Rect::from_center_size(s, vec2(14.0, 14.0)), &l.name);
}
} else {
let LayerSource::Light { kind } = l.source else { continue };
@@ -1922,7 +1946,7 @@ fn draw_rigs(app: &mut EffectcraftApp, painter: &egui::Painter, map: &ViewerMap,
painter.circle_stroke(q, 3.0, stroke);
}
}
app.auto.add(&format!("viewer.light.{}", l.id.0), Rect::from_center_size(s, vec2(16.0, 16.0)), &l.name);
app.auto.add(&format!("{prefix}light.{}", l.id.0), Rect::from_center_size(s, vec2(16.0, 16.0)), &l.name);
}
}
}
+28
View File
@@ -195,3 +195,31 @@ fn extended_viewer_renders_past_the_comp_frame() {
h.step();
assert!(h.state().ui.viewer.extended.is_none());
}
/// #875: the Top / Front / Right views of a 4-view layout draw the camera wireframe (they drew
/// none, so moving the camera showed nothing there), and it follows the camera's Position.
#[test]
fn layout_views_draw_the_camera_and_follow_its_position() {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "C", "width": 640, "height": 360, "duration": 2})).unwrap();
let solid = s.execute("layer.newSolid", json!({"color": "#ff0000", "width": 200, "height": 200})).unwrap()["layer"].as_u64().unwrap();
s.execute("layer.setSwitch", json!({"layer": solid, "switch": "threeD", "value": true})).unwrap();
let cam = s.execute("layer.newCamera", json!({"position": [320, 180, -300], "poi": [320, 180, 0]})).unwrap()["layer"].as_u64().unwrap();
s.execute("view.layout", json!({"views": 4})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
settle(&mut h);
let centre = |h: &Harness<'_, EffectcraftApp>, view: &str| {
let id = format!("viewer.view.{view}.camera.{cam}");
let [x, y, w, hh] = h.state().auto.find(&id).unwrap_or_else(|| panic!("{id} is registered")).rect;
(x + w / 2.0, y + hh / 2.0)
};
for v in ["top", "front", "right"] {
centre(&h, v);
}
let before = centre(&h, "top");
h.state_mut().session.execute("prop.set", json!({"layer": cam, "path": "transform/position", "value": [480, 180, -300]})).unwrap();
settle(&mut h);
let after = centre(&h, "top");
assert!(after.0 - before.0 > 10.0, "the camera moves right in the Top view: {before:?} -> {after:?}");
assert!((after.1 - before.1).abs() < 1.0, "{before:?} -> {after:?}");
}