Files
OneCAD-Draft/src/scene/project.rs
T
Hakan SevenandClaude Opus 4.8 bddc050b3e refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.

- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
  dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
  with thin delegating arms; the 66 fattest arm bodies move to topic
  methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
  mspace,project,selection,modify,group_layer,preview}. The impl Scene
  body is split across files via inherent-impl-per-file; struct, ctor,
  fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
  The 233-arm dispatch match becomes source-ordered per-family
  dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.

Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 22:51:29 +03:00

441 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::*;
impl Scene {
/// Collect model-space wires projected into paper space for all (or one specific)
/// user viewports. `only_vp = Some(h)` restricts output to that viewport.
pub(super) fn viewport_content_wires(
&self,
paper_block: Handle,
only_vp: Option<Handle>,
exclude_vp: Option<Handle>,
) -> Vec<WireModel> {
use acadrust::entities::Viewport;
let viewports: Vec<&Viewport> = self
.document
.entities()
.filter_map(|e| {
if let EntityType::Viewport(vp) = e {
Some(vp)
} else {
None
}
})
.filter(|vp| {
self.is_content_viewport_in_layout(vp, paper_block)
&& vp.status.is_on
&& only_vp.map_or(true, |h| vp.common.handle == h)
&& exclude_vp.map_or(true, |h| vp.common.handle != h)
})
.collect();
if viewports.is_empty() {
return vec![];
}
let mut result = Vec::new();
for vp in viewports {
let vp_handle = vp.common.handle;
// ── Fast path: return cached projected wires ──────────────────
{
let cache = self.paper_projected_cache.borrow();
if let Some((cached_epoch, ref wires)) = cache.get(&vp_handle) {
if *cached_epoch == self.geometry_epoch {
result.extend_from_slice(wires);
continue;
}
}
}
// ── Cache miss: compute projection ────────────────────────────
// Use camera_for_viewport so the axes match the GPU renderer exactly.
let cam_frame = match self.camera_for_viewport(vp_handle) {
Some(c) => c,
None => continue,
};
let view_right = cam_frame.rotation * glam::Vec3::X;
let view_up = cam_frame.rotation * glam::Vec3::Y;
// Scale (paper units per model unit) comes straight from the camera
// the GPU uses: the model height shown is `2 * ortho_size`, mapped
// onto `vp.height` of paper. `camera_for_viewport` already made the
// saved-view-vs-auto-fit decision (with the twist-correct overlap
// test), so deriving scale from it keeps the CPU projection (used
// for hit-test / snap / fit) locked to the GPU render — no second,
// independently-computed scale that could disagree under a twist.
let view_height_eff = (cam_frame.ortho_size() * 2.0) as f64;
let scale = if view_height_eff > 1e-9 {
(vp.height / view_height_eff) as f32
} else {
1.0
};
let pcx = vp.center.x as f32;
let pcy = vp.center.y as f32;
let pcz = vp.center.z as f32;
let hw = (vp.width / 2.0) as f32;
let hh = (vp.height / 2.0) as f32;
// ── Use cached tessellation (model_wires_for_viewport_arc) ────
// This eliminates the per-frame tessellate_one() loop that was here
// previously; tessellation is now O(1) on navigation frames.
// Pass 0.0 for screen height — the CPU-projection / hit-test
// path wants the full-fidelity (no-LOD-stub) wire list,
// regardless of paper zoom.
let model_wires = self.model_wires_for_viewport_arc(vp_handle, 0.0);
// ── Project and clip wires into viewport ──────────────────────
let vp_x0 = pcx - hw;
let vp_x1 = pcx + hw;
let vp_y0 = pcy - hh;
let vp_y1 = pcy + hh;
// camera_dist: how far the camera is from the target plane.
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
};
let mut projected: Vec<WireModel> = Vec::new();
// Precompute precision-stable WCS-space projection inputs in
// f64. The previous f32 inner loop suffered catastrophic
// cancellation on UTM-scale drawings: `(wire_offset_rel -
// target_offset_rel).dot(view_right) - view_center` is a
// small paper offset computed by subtracting two values at
// ~5e6 magnitude — f32 ULP there is ~0.5 m, so paper output
// jittered by cm even when the actual model was clean.
//
// Do everything WCS-relative in f64; cast to f32 only at the
// final paper position.
// Display centre = the camera's target, in WCS. `camera_for_viewport`
// already folded view_center through the (twisted) view basis and
// applied the empty-WCS auto-fit, so taking its target keeps the CPU
// projection identical to the GPU renderer under any twist.
let display_center_x = cam_frame.target.x as f64 + [0.0_f64; 3][0];
let display_center_y = cam_frame.target.y as f64 + [0.0_f64; 3][1];
let display_center_z = cam_frame.target.z as f64 + [0.0_f64; 3][2];
let view_right_d = (
view_right.x as f64,
view_right.y as f64,
view_right.z as f64,
);
let view_up_d = (view_up.x as f64, view_up.y as f64, view_up.z as f64);
let view_fwd = cam_frame.rotation * glam::Vec3::Z;
let view_fwd_d = (view_fwd.x as f64, view_fwd.y as f64, view_fwd.z as f64);
let camera_dist_d = camera_dist as f64;
let scale_d = scale as f64;
let pcx_d = pcx as f64;
let pcy_d = pcy as f64;
// Project one ABSOLUTE-WCS model point (f64) onto the paper sheet.
// Shared by the polyline points, snap points and key vertices so the
// hit-test / snap geometry lands in the same paper frame the wire is
// drawn in — otherwise snaps and the click-AABB stay in model (UTM)
// space and the cursor never reaches them.
let proj_abs = |ax: f64, ay: f64, az: f64| -> [f32; 3] {
let mp_x = ax - display_center_x;
let mp_y = ay - display_center_y;
let mp_z = az - display_center_z;
let u = mp_x * view_right_d.0 + mp_y * view_right_d.1 + mp_z * view_right_d.2;
let v = mp_x * view_up_d.0 + mp_y * view_up_d.1 + mp_z * view_up_d.2;
if use_perspective {
let d_vd = mp_x * view_fwd_d.0 + mp_y * view_fwd_d.1 + mp_z * view_fwd_d.2;
let fwd = camera_dist_d - d_vd;
if fwd <= 0.001 {
return [f32::NAN; 3];
}
let factor = camera_dist_d / fwd;
[
(pcx_d + u * factor * scale_d) as f32,
(pcy_d + v * factor * scale_d) as f32,
pcz,
]
} else {
[(pcx_d + u * scale_d) as f32, (pcy_d + v * scale_d) as f32, pcz]
}
};
let in_vp = |x: f32, y: f32| x >= vp_x0 && x <= vp_x1 && y >= vp_y0 && y <= vp_y1;
for wire in model_wires.iter() {
let projected_pts: Vec<[f32; 3]> = wire
.points
.iter()
.enumerate()
.map(|(pi, &[mx, my, mz])| {
if mx.is_nan() || my.is_nan() || mz.is_nan() {
return [f32::NAN; 3];
}
// Reconstruct absolute WCS from the double-single high
// (`points`) + low (`points_low`) pair — the high f32
// alone is ~0.5 m off at UTM scale.
let lo = wire.points_low.get(pi).copied().unwrap_or([0.0; 3]);
proj_abs(mx as f64 + lo[0] as f64, my as f64 + lo[1] as f64, mz as f64 + lo[2] as f64)
})
.collect();
// Fast AABB pre-reject.
let any_near = projected_pts.iter().any(|&[x, y, _]| {
x.is_finite()
&& y.is_finite()
&& x >= vp_x0 - 1.0
&& x <= vp_x1 + 1.0
&& y >= vp_y0 - 1.0
&& y <= vp_y1 + 1.0
});
let (min_x, max_x, min_y, max_y) =
projected_pts.iter().filter(|p| p[0].is_finite()).fold(
(
f32::INFINITY,
f32::NEG_INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
),
|(mnx, mxx, mny, mxy), &[x, y, _]| {
(mnx.min(x), mxx.max(x), mny.min(y), mxy.max(y))
},
);
let aabb_hits =
max_x >= vp_x0 && min_x <= vp_x1 && max_y >= vp_y0 && min_y <= vp_y1;
if !any_near && !aabb_hits {
continue;
}
let clipped =
clip_polyline_to_rect(&projected_pts, vp_x0, vp_y0, vp_x1, vp_y1, pcz);
if clipped.is_empty() {
continue;
}
// Paper-space AABB of the clipped polyline — the cloned model
// (UTM) AABB would make click_hit's screen-projected pre-reject
// discard the wire (box selection has no pre-reject, which is why
// it kept working while picking didn't).
let mut pmnx = f32::INFINITY;
let mut pmny = f32::INFINITY;
let mut pmxx = f32::NEG_INFINITY;
let mut pmxy = f32::NEG_INFINITY;
for &[x, y, _] in clipped.iter().filter(|p| p[0].is_finite()) {
pmnx = pmnx.min(x);
pmny = pmny.min(y);
pmxx = pmxx.max(x);
pmxy = pmxy.max(y);
}
// Project snap points + key vertices into the same paper frame,
// keeping only those inside the viewport rect, so endpoint /
// midpoint / centre snaps land on the visible sheet geometry
// instead of the model's UTM coordinates.
let snap_pts: Vec<(glam::DVec3, model::wire_model::SnapHint)> = wire
.snap_pts
.iter()
.filter_map(|(w, h)| {
let p = proj_abs(w.x, w.y, w.z);
(p[0].is_finite() && in_vp(p[0], p[1]))
.then(|| (glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), *h))
})
.collect();
let key_vertices: Vec<[f64; 3]> = wire
.key_vertices
.iter()
.filter_map(|&[kx, ky, kz]| {
let p = proj_abs(kx, ky, kz);
(p[0].is_finite() && in_vp(p[0], p[1]))
.then(|| [p[0] as f64, p[1] as f64, p[2] as f64])
})
.collect();
let adapted = view::render::adapt_to_bg(wire.color, self.paper_bg_color);
let [r, g, b, a] = adapted;
let mut out = wire.clone();
out.points = clipped;
// Paper coordinates are small sheet units — no relative-to-eye
// residual is needed, and keeping the model wire's points_low
// here would add a model-scale offset to the paper points.
out.points_low = Vec::new();
out.snap_pts = snap_pts;
out.key_vertices = key_vertices;
// Tangent geometry is in model space and can't be trivially
// re-expressed in paper coords — drop it (no tangent snap on
// projected viewport content) rather than snap to UTM.
out.tangent_geoms = Vec::new();
out.aabb = if pmnx.is_finite() {
[pmnx, pmny, pmxx, pmxy]
} else {
WireModel::UNBOUNDED_AABB
};
out.color = [r * 0.80, g * 0.80, b * 0.80, a * 0.85];
out.line_weight_px = wire.line_weight_px;
// Wire's pattern was sized for model-space coords during
// tessellation; we just projected points into paper coords
// (× scale), so rescale the dash pattern by the same factor
// to keep dimensional consistency in the GPU shader.
out.pattern_length = wire.pattern_length * scale;
out.pattern = wire.pattern.map(|v| v * scale);
out.vp_scissor = Some([vp_x0, vp_y0, vp_x1, vp_y1]);
projected.push(out);
}
// Store in cache, then extend result.
self.paper_projected_cache
.borrow_mut()
.insert(vp_handle, (self.geometry_epoch, projected.clone()));
result.extend(projected);
}
result
}
}
// ── Paper boundary wire ────────────────────────────────────────────────────
// ── Cohen-Sutherland line clipping ───────────────────────────────────────
/// Clip a single segment (x0,y0)→(x1,y1) against the axis-aligned rectangle
/// [xmin,xmax]×[ymin,ymax]. Returns the clipped endpoints or `None` if the
/// segment is entirely outside.
fn cs_clip(
mut x0: f32,
mut y0: f32,
mut x1: f32,
mut y1: f32,
xmin: f32,
ymin: f32,
xmax: f32,
ymax: f32,
) -> Option<(f32, f32, f32, f32)> {
const LEFT: u8 = 1;
const RIGHT: u8 = 2;
const BOTTOM: u8 = 4;
const TOP: u8 = 8;
let code = |x: f32, y: f32| -> u8 {
let mut c = 0u8;
if x < xmin {
c |= LEFT;
} else if x > xmax {
c |= RIGHT;
}
if y < ymin {
c |= BOTTOM;
} else if y > ymax {
c |= TOP;
}
c
};
let mut c0 = code(x0, y0);
let mut c1 = code(x1, y1);
loop {
if c0 | c1 == 0 {
return Some((x0, y0, x1, y1));
}
if c0 & c1 != 0 {
return None;
}
let cout = if c0 != 0 { c0 } else { c1 };
let (x, y);
if cout & TOP != 0 {
x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0);
y = ymax;
} else if cout & BOTTOM != 0 {
x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0);
y = ymin;
} else if cout & RIGHT != 0 {
y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0);
x = xmax;
} else {
y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0);
x = xmin;
}
if cout == c0 {
x0 = x;
y0 = y;
c0 = code(x0, y0);
} else {
x1 = x;
y1 = y;
c1 = code(x1, y1);
}
}
}
/// Clip a projected polyline (NaN-separated segments) to the viewport rectangle.
/// Returns a new points vec with proper NaN separators at clip boundaries.
fn clip_polyline_to_rect(
pts: &[[f32; 3]],
xmin: f32,
ymin: f32,
xmax: f32,
ymax: f32,
z: f32,
) -> Vec<[f32; 3]> {
const NAN3: [f32; 3] = [f32::NAN, f32::NAN, f32::NAN];
let mut result: Vec<[f32; 3]> = Vec::new();
let mut i = 0;
while i < pts.len() {
// Skip NaN separators.
if pts[i][0].is_nan() || pts[i][1].is_nan() {
i += 1;
continue;
}
// Gather contiguous run of finite points.
let start = i;
while i < pts.len() && pts[i][0].is_finite() && pts[i][1].is_finite() {
i += 1;
}
let seg = &pts[start..i];
if seg.len() < 2 {
continue;
}
// Clip each edge and track pen state to insert NaN on lift.
let mut pen_down = false;
for j in 0..seg.len() - 1 {
let [x0, y0, _] = seg[j];
let [x1, y1, _] = seg[j + 1];
match cs_clip(x0, y0, x1, y1, xmin, ymin, xmax, ymax) {
None => {
pen_down = false;
}
Some((cx0, cy0, cx1, cy1)) => {
if !pen_down {
if !result.is_empty() {
result.push(NAN3);
}
result.push([cx0, cy0, z]);
pen_down = true;
} else if let Some(&[lx, ly, _]) = result.last() {
if (lx - cx0).abs() > 1e-4 || (ly - cy0).abs() > 1e-4 {
result.push(NAN3);
result.push([cx0, cy0, z]);
}
}
result.push([cx1, cy1, z]);
// If the exit point was clipped, lift pen.
if (cx1 - x1).abs() > 1e-4 || (cy1 - y1).abs() > 1e-4 {
pen_down = false;
}
}
}
}
}
// Remove trailing NaN.
while result
.last()
.map(|p: &[f32; 3]| p[0].is_nan())
.unwrap_or(false)
{
result.pop();
}
result
}