From 2a1a8f4b94e6a584a48720948c15ccf9d75714cd Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 6 Aug 2026 22:00:22 +0300 Subject: [PATCH] fix(solid3d): read cone radius from the major axis The cone-surface record's trailing real is a parameter scale, not geometry. It equals the radius on many bodies, so cylinders generally came out right, but where the two diverge the lateral surface was built at the wrong size while the flat caps stayed on their own rims -- a gauge dial drew as a ring around its own face, its band at 43.2 against caps at 34.56. The major axis is the radius vector at the reference cross-section, so its length is the radius. Route every read through one helper and keep the record's field only as a fallback for a degenerate major axis, so the band, the silhouette generators, the isolines and the coaxial-rim check all size themselves the same way. Taper is unchanged: the major axis is still the h=0 radius that radius + h*tan(a) extends. Co-Authored-By: Claude Opus 5 --- src/scene/convert/acis_to_truck.rs | 10 +++++----- src/scene/convert/solid3d_tess.rs | 32 +++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/scene/convert/acis_to_truck.rs b/src/scene/convert/acis_to_truck.rs index 3e7f85c3d..173cc6666 100644 --- a/src/scene/convert/acis_to_truck.rs +++ b/src/scene/convert/acis_to_truck.rs @@ -30,9 +30,9 @@ use acadrust::entities::acis::{ }; use crate::scene::convert::solid3d_tess::{ - body_transform, collect_face_loops, cone_axis_span, finalize_mesh, tess_cone_face, - tess_plane_face, tess_sphere_face, tess_torus_face, LodConfig, BOUNDARY_CHORD_FRAC, - TRUCK_CHORD_FRAC, + body_transform, collect_face_loops, cone_axis_span, cone_radius, finalize_mesh, + tess_cone_face, tess_plane_face, tess_sphere_face, tess_torus_face, LodConfig, + BOUNDARY_CHORD_FRAC, TRUCK_CHORD_FRAC, }; use crate::scene::model::mesh_model::MeshLodSet; use rustc_hash::FxHashMap; @@ -268,7 +268,7 @@ fn build_face_group( } "cone-surface" => { let cone = SatConeSurface::from_record(surf_rec)?; - let tol = curve_tol(cone.radius()); + let tol = curve_tol(cone_radius(&cone)); let (faces, out) = cone_faces(sat, face, &cone)?; Some((faces, out, tol)) } @@ -385,7 +385,7 @@ fn cone_faces( let (cx, cy, cz) = cone.center(); let (ax, ay, az) = cone.axis(); let (ux, uy, uz) = cone.major_axis(); - let radius = cone.radius(); + let radius = cone_radius(cone); let sin = cone.sin_half_angle(); let cos = cone.cos_half_angle(); diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 1d4781a26..2246ac2f3 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -398,7 +398,7 @@ fn cone_face_geom(sat: &SatDocument, face: &SatFace) -> Option { let (cx, cy, cz) = cone.center(); let (ax, ay, az) = cone.axis(); let (ux, uy, uz) = cone.major_axis(); - let radius = cone.radius(); + let radius = cone_radius(&cone); let sin_a = cone.sin_half_angle(); let cos_a = cone.cos_half_angle(); let axis = norm3([ax, ay, az]); @@ -802,6 +802,24 @@ fn sphere_cap_window( } } +/// Reference cross-section radius of a cone/cylinder surface. +/// +/// The record's trailing real is a parameter scale, not geometry — it coincides +/// with the radius on many bodies but diverges on others, which draws the +/// lateral surface at the wrong size while the rims stay put (a gauge dial +/// rendering as a ring around its own face). The major axis *is* the radius +/// vector at the reference cross-section, so its length is the radius; fall back +/// to the record's field only when that vector is missing. +pub(crate) fn cone_radius(cone: &SatConeSurface) -> f64 { + let (x, y, z) = cone.major_axis(); + let len = (x * x + y * y + z * z).sqrt(); + if len > 1e-12 { + len + } else { + cone.radius() + } +} + /// Any unit vector square to `axis`, for completing a frame. fn perp_axis(axis: [f64; 3]) -> [f64; 3] { let seed = if axis[0].abs() < 0.9 { @@ -2058,7 +2076,7 @@ pub(crate) fn tess_cone_face( let (cx, cy, cz) = cone.center(); let (ax, ay, az) = cone.axis(); // axis direction (unit) let (ux, uy, uz) = cone.major_axis(); // u=0 direction - let radius = cone.radius(); + let radius = cone_radius(cone); let sin_a = cone.sin_half_angle(); let cos_a = cone.cos_half_angle(); // ≈1 for cylinder, <1 for cone @@ -2328,7 +2346,7 @@ pub(crate) fn cone_axis_span( let major = e.major_axis(); let curve_radius = dot3([major.0, major.1, major.2], [major.0, major.1, major.2]) .sqrt(); - let expected_radius = (cone.radius() + h * tangent).abs(); + let expected_radius = (cone_radius(cone) + h * tangent).abs(); let scale = curve_radius.max(expected_radius).max(1.0); if radial_len < scale * 1e-6 && n_dot > 0.999 @@ -2355,8 +2373,8 @@ pub(crate) fn cone_axis_span( d[2] - h * axis[2], ]; let radial_len = dot3(radial, radial).sqrt(); - let expected = (cone.radius() + h * tangent).abs(); - let tolerance = expected.max(cone.radius().abs()).max(1.0) * 1e-5; + let expected = (cone_radius(cone) + h * tangent).abs(); + let tolerance = expected.max(cone_radius(cone).abs()).max(1.0) * 1e-5; if (radial_len - expected).abs() <= tolerance { heights.push(h); } @@ -2383,7 +2401,7 @@ pub(crate) fn cone_axis_span( return Some((anchor.min(mate), anchor.max(mate))); } if sin_a.abs() > 1e-6 { - let apex = -cone.radius() * cos_a / sin_a; + let apex = -cone_radius(cone) * cos_a / sin_a; if (apex - anchor).abs() >= 1e-9 { return Some((anchor.min(apex), anchor.max(apex))); } @@ -2396,7 +2414,7 @@ pub(crate) fn cone_axis_span( // True cone with a single rim: close the surface at its apex (r = 0). if sin_a.abs() > 1e-6 && heights.len() <= 1 { - let apex = -cone.radius() * cos_a / sin_a; + let apex = -cone_radius(cone) * cos_a / sin_a; h_min = h_min.min(apex); h_max = h_max.max(apex); }