mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
#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:
1 parent
67434e9038
commit
f19f3a8a39
2 files changed
+58
-6
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
Reference in new issue
Block a user