diff --git a/Cargo.lock b/Cargo.lock index aaaa6aff5..718fbb6c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -935,7 +935,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#39083307452ba14b3968bdb0738e2665d3d18e62" +source = "git+https://github.com/HakanSeven12/cadkernel.git#9406868b1bedd57d121b11fc75658bf9eaa2c764" dependencies = [ "cavalier_contours", ] diff --git a/src/entities/arc.rs b/src/entities/arc.rs index b254f8c14..f55aa8e46 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -26,9 +26,6 @@ fn to_truck(arc: &Arc) -> TruckEntity { // Compute OCS basis vectors for this entity's normal. let (ax, ay) = crate::scene::view::transform::ocs_axes(normal); - let ccw_end = if ea >= sa { ea } else { ea + TAU }; - let mid_a = sa + (ccw_end - sa) * 0.5; - // Arc centre in WCS. let (cwx, cwy, cwz) = crate::scene::view::transform::ocs_point_to_wcs((cx, cy, cz), normal); @@ -45,7 +42,8 @@ fn to_truck(arc: &Arc) -> TruckEntity { // Centre, ends, arc-length midpoint and the quadrants the sweep actually // covers, all from the entity's own curve. Circles and ellipses (closed // curves) deliberately emit no midpoint; see #34. - let snap = crate::entities::curve::snap_from(&crate::entities::curve::arc_curve(arc)); + let curve = crate::entities::curve::arc_curve(arc); + let snap = crate::entities::curve::snap_from(&curve); let tangent = TangentGeom::Circle { center: [cwx as f32, cwy as f32, cwz as f32], radius: r as f32, @@ -87,9 +85,14 @@ fn to_truck(arc: &Arc) -> TruckEntity { }; } + // Kept as a truck edge rather than a point list: EXTRUDE, REVOLVE and + // SWEEP take their profile and path from `Curve` / `Contour` and have no + // arm for `Lines`, so handing them one would quietly stop an arc being + // usable as either. let p_start = arc_pt(sa); let p_end = arc_pt(ea); - let p_mid = arc_pt(mid_a); + let ccw_end = if ea >= sa { ea } else { ea + TAU }; + let p_mid = arc_pt(sa + (ccw_end - sa) * 0.5); let v_start = builder::vertex(p_start); let v_end = builder::vertex(p_end); let edge = builder::circle_arc(&v_start, &v_end, p_mid); diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 983aa473b..cfc65bcb1 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -23,8 +23,8 @@ fn to_truck(circle: &Circle) -> TruckEntity { let rf = r as f32; // Centre and quadrants come from the entity's own curve, so the same // definition answers here, in the tessellation and in a trim. - let snap = crate::entities::curve::snap_from(&crate::entities::curve::circle_curve(circle)); - let snap_pts = snap.snap_pts; + let curve = crate::entities::curve::circle_curve(circle); + let snap_pts = crate::entities::curve::snap_from(&curve).snap_pts; let tangent = TangentGeom::Circle { center: [cwx as f32, cwy as f32, cwz as f32], radius: rf, @@ -74,34 +74,16 @@ fn to_truck(circle: &Circle) -> TruckEntity { }; } - // Tessellate directly as a cos/sin polyline rather than a truck + // Sampled from the entity's own curve rather than as a truck // `circle_arc` (arc-through-three-points). At large WCS coordinates // (e.g. −1.2M UTM) the three-point fit cancels catastrophically — the // circle comes back with a ~3% radius wobble and uneven segment lengths, // which then throws off a dashed linetype's dash spacing. Direct - // evaluation only adds a small ±r term to the centre, so it stays precise; - // the `Lines` path RTE-splits the absolute-f64 points into the + // evaluation only adds a small ±r term to the centre, so it stays + // precise; the `Lines` path RTE-splits the absolute-f64 points into the // double-single the shader reconstructs. - let tol = crate::scene::convert::truck_tess::current_curve_tol(); - // Chord-height tolerance → segment count: sag = r·(1 − cos(π/N)). - let n = if r > tol { - (std::f64::consts::PI / (1.0 - tol / r).clamp(-1.0, 1.0).acos()) - .ceil() - .clamp(16.0, 4096.0) as usize - } else { - 16 - }; - let tau = std::f64::consts::TAU; - let mut pts: Vec<[f64; 3]> = Vec::with_capacity(n + 1); - for i in 0..=n { - let a = i as f64 * tau / n as f64; - let (s, c) = a.sin_cos(); - pts.push([ - cwx + r * (c * ax.0 + s * ay.0), - cwy + r * (c * ax.1 + s * ay.1), - cwz + r * (c * ax.2 + s * ay.2), - ]); - } + let pts = crate::entities::curve::curve_points(&curve); + TruckEntity { pick_tris: Vec::new(), object: TruckObject::Lines(pts), diff --git a/src/entities/curve.rs b/src/entities/curve.rs index 12182c71c..5c0102223 100644 --- a/src/entities/curve.rs +++ b/src/entities/curve.rs @@ -331,6 +331,22 @@ pub fn entity_curve_xy(entity: &EntityType) -> Option { }) } +/// World-space wire points for a curve, cut to the render pass's current +/// chord tolerance. +/// +/// The density comes from how far a chord may sag from the curve, not from a +/// segment count: an arc a metre across and one a kilometre across need +/// different numbers of points to look equally round, and the tolerance the +/// render pass carries is already zoom-adaptive. +/// +/// Everything stays `f64` to the end. The narrowing belongs at the GPU +/// boundary, where the residual is kept as the low half of a double-single +/// pair; casting local coordinates on the way in throws away precision the +/// shader was built to reconstruct. +pub fn curve_points(curve: &PlanarCurve) -> Vec<[f64; 3]> { + curve.tessellate_within(crate::scene::convert::truck_tess::current_curve_tol()) +} + /// The snap candidates an entity's curve offers, in the two channels the /// wire model carries them in. #[derive(Debug, Default, Clone)] diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 3552433ba..8363d0da8 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -1,7 +1,9 @@ use acadrust::entities::Ellipse; -use crate::t; use truck_modeling::{builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire}; +use crate::entities::curve::CurveSnap; +use crate::t; + use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, @@ -10,118 +12,93 @@ use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; -const TAU: f64 = std::f64::consts::TAU; - fn to_truck(ell: &Ellipse) -> TruckEntity { - let normal = (ell.normal.x, ell.normal.y, ell.normal.z); - let (nx, ny, nz) = normal; - // ELLIPSE is one of the few WCS entities in DXF: `center` (code 10) and // `major_axis` (code 11) are world coordinates already — unlike ARC / - // CIRCLE, whose centers are OCS. (This used to run both through the - // arbitrary-axis OCS, which misplaced any ellipse whose normal isn't - // Z-up — e.g. the (0,0,-1) result of a mirrored-block explode.) - let (cwx, cwy, cwz) = (ell.center.x, ell.center.y, ell.center.z); - let wcs_maj = glam::Vec3::new( - ell.major_axis.x as f32, - ell.major_axis.y as f32, - ell.major_axis.z as f32, - ); - let r_major = wcs_maj.length() as f64; - let r_minor = r_major * ell.minor_axis_ratio; - let t0 = ell.start_parameter; - let mut t1 = ell.end_parameter; - if t1 <= t0 { - t1 += TAU; - } - let u = if r_major > 1e-9 { - wcs_maj / wcs_maj.length() - } else { - glam::Vec3::X - }; - // Minor axis direction: WCS_normal × u (both unit vectors, always perpendicular). - let wcs_normal = glam::Vec3::new(nx as f32, ny as f32, nz as f32); - let v_axis = wcs_normal.cross(u); - let is_closed = (t1 - t0 - TAU).abs() < 1e-6; - // Centre, axis ends and — for an open arc — its own two ends and middle, - // all read off the entity's curve rather than rebuilt from these locals. - let snap = crate::entities::curve::ellipse_curve(ell) - .map(|curve| crate::entities::curve::snap_from(&curve)) + // CIRCLE, whose centers are OCS. The converter knows that; this used to + // run both through the arbitrary-axis OCS, which misplaced any ellipse + // whose normal isn't Z-up — e.g. the (0,0,-1) result of a mirrored-block + // explode. + let curve = crate::entities::curve::ellipse_curve(ell); + let snap = curve + .as_ref() + .map(crate::entities::curve::snap_from) .unwrap_or_default(); - if is_closed { - let n = 16usize; - let pts_upper: Vec = (0..=n) - .map(|i| { - let t = (i as f64 / n as f64) * std::f64::consts::PI; - let lx = (r_major * t.cos()) as f32; - let lz = (r_minor * t.sin()) as f32; - Point3::new( - cwx + (lx * u.x + lz * v_axis.x) as f64, - cwy + (lx * u.y + lz * v_axis.y) as f64, - cwz + (lx * u.z + lz * v_axis.z) as f64, - ) - }) - .collect(); - let pts_lower: Vec = (0..=n) - .map(|i| { - let t = std::f64::consts::PI + (i as f64 / n as f64) * std::f64::consts::PI; - let lx = (r_major * t.cos()) as f32; - let lz = (r_minor * t.sin()) as f32; - Point3::new( - cwx + (lx * u.x + lz * v_axis.x) as f64, - cwy + (lx * u.y + lz * v_axis.y) as f64, - cwz + (lx * u.z + lz * v_axis.z) as f64, - ) - }) - .collect(); - let v_pos = builder::vertex(*pts_upper.first().unwrap()); - let v_neg = builder::vertex(*pts_upper.last().unwrap()); - let kv_u = KnotVec::uniform_knot(1, n); - let kv_l = KnotVec::uniform_knot(1, n); - let spl_u = BSplineCurve::new(kv_u, pts_upper); - let spl_l = BSplineCurve::new(kv_l, pts_lower); - let edge_upper = Edge::new(&v_pos, &v_neg, Curve::BSplineCurve(spl_u)); - let edge_lower = Edge::new(&v_neg, &v_pos, Curve::BSplineCurve(spl_l)); - let wire: Wire = [edge_upper, edge_lower].into_iter().collect(); - let snap_pts = snap.snap_pts.clone(); - TruckEntity { - pick_tris: Vec::new(), - object: TruckObject::Contour(wire), - snap_pts, - tangent_geoms: vec![], - key_vertices: vec![], - fill_tris: vec![], - } - } else { - let n = 32usize; - let ctrl_pts: Vec = (0..=n) - .map(|i| { - let t = t0 + (t1 - t0) * (i as f64 / n as f64); - let lx = (r_major * t.cos()) as f32; - let lz = (r_minor * t.sin()) as f32; - Point3::new( - cwx + (lx * u.x + lz * v_axis.x) as f64, - cwy + (lx * u.y + lz * v_axis.y) as f64, - cwz + (lx * u.z + lz * v_axis.z) as f64, - ) - }) - .collect(); - let kv = KnotVec::uniform_knot(1, n); - let bspline = BSplineCurve::new(kv, ctrl_pts.clone()); - let v_start = builder::vertex(*ctrl_pts.first().unwrap()); - let v_end = builder::vertex(*ctrl_pts.last().unwrap()); - let edge = Edge::new(&v_start, &v_end, Curve::BSplineCurve(bspline)); - - TruckEntity { - pick_tris: Vec::new(), - object: TruckObject::Curve(edge), - snap_pts: snap.snap_pts.clone(), - tangent_geoms: vec![], - key_vertices: vec![], - fill_tris: vec![], - } + // The points come from the entity's own curve, cut to the render pass's + // chord tolerance. The two fixed counts this replaced — sixteen per half + // for a full ellipse, thirty-two for an arc — faceted visibly on + // anything large, and the local coordinates were built in `f32`, which + // threw away precision the double-single vertex path exists to keep. + // + // What does not change is the shape of the object. EXTRUDE, REVOLVE and + // SWEEP read their profile out of `Contour` / `Curve` and have no arm for + // a bare point list, so an ellipse handed over as `Lines` would stop + // being usable as either. + let empty = |snap: CurveSnap| TruckEntity { + pick_tris: Vec::new(), + object: TruckObject::Lines(Vec::new()), + snap_pts: snap.snap_pts, + tangent_geoms: vec![], + key_vertices: vec![], + fill_tris: vec![], + }; + let Some(planar) = curve else { + return empty(snap); + }; + let points: Vec = crate::entities::curve::curve_points(&planar) + .into_iter() + .map(|p| Point3::new(p[0], p[1], p[2])) + .collect(); + if points.len() < 2 { + return empty(snap); } + + // Closure decided on the points rather than on the stored parameters. A + // file that writes an end parameter a hair short of a full turn still + // draws a closed ellipse, and an open edge whose two vertices sit on top + // of each other is not something a plane can be attached to. + let ends_meet = { + let (first, last) = (points[0], *points.last().expect("checked non-empty")); + let span = first.x.abs().max(first.y.abs()).max(first.z.abs()).max(1.0); + let gap = first - last; + (gap.x * gap.x + gap.y * gap.y + gap.z * gap.z).sqrt() <= 1e-9 * span + }; + let object = if planar.is_closed() || ends_meet { + // Two half-edges rather than one closed edge: a single edge whose + // two vertices coincide is not a wire truck will attach a plane to. + let split = points.len() / 2; + let upper = points[..=split].to_vec(); + let lower = points[split..].to_vec(); + let v_pos = builder::vertex(points[0]); + let v_neg = builder::vertex(points[split]); + let edge_upper = Edge::new(&v_pos, &v_neg, polyline_curve(upper)); + let edge_lower = Edge::new(&v_neg, &v_pos, polyline_curve(lower)); + TruckObject::Contour([edge_upper, edge_lower].into_iter().collect::()) + } else { + let v_start = builder::vertex(points[0]); + let v_end = builder::vertex(*points.last().expect("checked non-empty")); + TruckObject::Curve(Edge::new(&v_start, &v_end, polyline_curve(points))) + }; + + TruckEntity { + pick_tris: Vec::new(), + object, + snap_pts: snap.snap_pts, + tangent_geoms: vec![], + key_vertices: vec![], + fill_tris: vec![], + } +} + +/// A truck curve that passes through every one of `points` in order. +/// +/// Degree one, so the control polygon *is* the curve — the same thing the +/// hand-built sampling produced, without restating how a B-spline is put +/// together at each site. +fn polyline_curve(points: Vec) -> Curve { + let spans = points.len().saturating_sub(1); + Curve::BSplineCurve(BSplineCurve::new(KnotVec::uniform_knot(1, spans), points)) } fn grips(ell: &Ellipse) -> Vec {