From 5ad2f4a8cb9d4a1ab47fd2df6d91cd8a1d023beb Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 1 Apr 2026 00:22:02 +0300 Subject: [PATCH] =?UTF-8?q?Feat:=20Phase=203=20=E2=80=94=20perspective=20p?= =?UTF-8?q?rojection=20and=20per-viewport=20UCS=20properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/entities/viewport.rs | 31 +++++++++++++++++++++++++++++++ src/scene/mod.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/entities/viewport.rs b/src/entities/viewport.rs index be325d03f..5fafdb694 100644 --- a/src/entities/viewport.rs +++ b/src/entities/viewport.rs @@ -168,6 +168,23 @@ fn properties(vp: &Viewport) -> PropSection { }, edit("Target X", "vtgt_x", vp.view_target.x), 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" }; 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_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, _ => {} } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1050b1616..ea1ac59eb 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -661,6 +661,17 @@ impl Scene { let vp_y0 = 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 { // Project 3-D model points onto view plane → paper space. 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 u = mp.dot(view_right); 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(); // Fast AABB pre-reject: skip entirely if no finite point is