Feat: Phase 3 — perspective projection and per-viewport UCS properties
- viewport_content_wires(): conditional perspective divide using camera_dist = view_height * lens_length / 24.0 (35mm-film equiv.); points behind the camera are mapped to NaN for safe clipping - viewport.rs: expose lens_length, ucs_per_viewport, ucs_origin, ucs_x_axis, ucs_y_axis as editable properties; handle all new fields in apply_geom_prop() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1aa44ac987
commit
5ad2f4a8cb
2 changed files with 57 additions and 1 deletions
|
|
@ -168,6 +168,23 @@ fn properties(vp: &Viewport) -> PropSection {
|
||||||
},
|
},
|
||||||
edit("Target X", "vtgt_x", vp.view_target.x),
|
edit("Target X", "vtgt_x", vp.view_target.x),
|
||||||
edit("Target Z", "vtgt_z", vp.view_target.z),
|
edit("Target Z", "vtgt_z", vp.view_target.z),
|
||||||
|
// Perspective lens length.
|
||||||
|
edit("Lens Length (mm)", "vp_lens", vp.lens_length),
|
||||||
|
// Per-Viewport UCS.
|
||||||
|
Property {
|
||||||
|
label: "UCS Per Viewport".into(),
|
||||||
|
field: "vp_ucs_per_vp",
|
||||||
|
value: PropValue::BoolToggle { field: "vp_ucs_per_vp", value: vp.ucs_per_viewport },
|
||||||
|
},
|
||||||
|
edit("UCS Origin X", "vp_ucs_ox", vp.ucs_origin.x),
|
||||||
|
edit("UCS Origin Y", "vp_ucs_oy", vp.ucs_origin.y),
|
||||||
|
edit("UCS Origin Z", "vp_ucs_oz", vp.ucs_origin.z),
|
||||||
|
edit("UCS X-Axis X", "vp_ucs_xx", vp.ucs_x_axis.x),
|
||||||
|
edit("UCS X-Axis Y", "vp_ucs_xy", vp.ucs_x_axis.y),
|
||||||
|
edit("UCS X-Axis Z", "vp_ucs_xz", vp.ucs_x_axis.z),
|
||||||
|
edit("UCS Y-Axis X", "vp_ucs_yx", vp.ucs_y_axis.x),
|
||||||
|
edit("UCS Y-Axis Y", "vp_ucs_yy", vp.ucs_y_axis.y),
|
||||||
|
edit("UCS Y-Axis Z", "vp_ucs_yz", vp.ucs_y_axis.z),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +235,10 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
||||||
vp.ucs_icon_visible = if value == "toggle" { !vp.ucs_icon_visible } else { value == "true" };
|
vp.ucs_icon_visible = if value == "toggle" { !vp.ucs_icon_visible } else { value == "true" };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
"vp_ucs_per_vp" => {
|
||||||
|
vp.ucs_per_viewport = if value == "toggle" { !vp.ucs_per_viewport } else { value == "true" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,6 +305,16 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
||||||
}
|
}
|
||||||
"vtgt_x" => vp.view_target.x = v,
|
"vtgt_x" => vp.view_target.x = v,
|
||||||
"vtgt_z" => vp.view_target.z = v,
|
"vtgt_z" => vp.view_target.z = v,
|
||||||
|
"vp_lens" if v > 0.0 => vp.lens_length = v,
|
||||||
|
"vp_ucs_ox" => vp.ucs_origin.x = v,
|
||||||
|
"vp_ucs_oy" => vp.ucs_origin.y = v,
|
||||||
|
"vp_ucs_oz" => vp.ucs_origin.z = v,
|
||||||
|
"vp_ucs_xx" => vp.ucs_x_axis.x = v,
|
||||||
|
"vp_ucs_xy" => vp.ucs_x_axis.y = v,
|
||||||
|
"vp_ucs_xz" => vp.ucs_x_axis.z = v,
|
||||||
|
"vp_ucs_yx" => vp.ucs_y_axis.x = v,
|
||||||
|
"vp_ucs_yy" => vp.ucs_y_axis.y = v,
|
||||||
|
"vp_ucs_yz" => vp.ucs_y_axis.z = v,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -661,6 +661,17 @@ impl Scene {
|
||||||
let vp_y0 = pcy - hh;
|
let vp_y0 = pcy - hh;
|
||||||
let vp_y1 = pcy + hh;
|
let vp_y1 = pcy + hh;
|
||||||
|
|
||||||
|
// ── Perspective setup ─────────────────────────────────────────
|
||||||
|
// camera_dist: how far the camera is from the target plane.
|
||||||
|
// Derived from lens_length (mm, 35mm-film equiv.) and view_height:
|
||||||
|
// tan(fov_v/2) = 12 / lens_length → camera_dist = view_height * lens_length / 24
|
||||||
|
let use_perspective = vp.status.perspective && vp.lens_length > 1.0;
|
||||||
|
let camera_dist = if use_perspective {
|
||||||
|
(vp.view_height as f32 * vp.lens_length as f32 / 24.0).max(0.001)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
for wire in &model_wires {
|
for wire in &model_wires {
|
||||||
// Project 3-D model points onto view plane → paper space.
|
// Project 3-D model points onto view plane → paper space.
|
||||||
let projected_pts: Vec<[f32; 3]> = wire.points.iter().map(|&[mx, my, mz]| {
|
let projected_pts: Vec<[f32; 3]> = wire.points.iter().map(|&[mx, my, mz]| {
|
||||||
|
|
@ -670,7 +681,21 @@ impl Scene {
|
||||||
let mp = glam::Vec3::new(mx, my, mz) - target;
|
let mp = glam::Vec3::new(mx, my, mz) - target;
|
||||||
let u = mp.dot(view_right);
|
let u = mp.dot(view_right);
|
||||||
let v = mp.dot(view_up);
|
let v = mp.dot(view_up);
|
||||||
[pcx + u * scale, pcy + v * scale, pcz]
|
if use_perspective {
|
||||||
|
// vd points from target toward camera, so depth along vd is the
|
||||||
|
// distance from the target plane toward the camera.
|
||||||
|
let d_vd = mp.dot(vd);
|
||||||
|
// Forward distance from camera (positive = in front of camera).
|
||||||
|
let fwd = camera_dist - d_vd;
|
||||||
|
if fwd <= 0.001 {
|
||||||
|
// Point is behind or at the camera — discard.
|
||||||
|
return [f32::NAN; 3];
|
||||||
|
}
|
||||||
|
let factor = camera_dist / fwd;
|
||||||
|
[pcx + u * factor * scale, pcy + v * factor * scale, pcz]
|
||||||
|
} else {
|
||||||
|
[pcx + u * scale, pcy + v * scale, pcz]
|
||||||
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
// Fast AABB pre-reject: skip entirely if no finite point is
|
// Fast AABB pre-reject: skip entirely if no finite point is
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue