Files
OneCAD-Draft/src/scene/convert/tess_util.rs
T
Hakan SevenandClaude Opus 4.8 dd1f2e49bb fix(render): UTM-precise UCS icon + fallback outlines (hatch etc.)
Two UTM-scale rendering defects.

- UCS icon jittered: it projects only axis DIRECTIONS but went through the
  full view_proj, whose ~5.7e6 translation cancels catastrophically in f32.
  Feed it the rotation-only view_proj_rte instead (translation dropped).

- Fallback outline geometry (hatch boundary, viewport/insert/ole2frame
  frames, Solid3D edge wires) built absolute f32 points with an empty
  points_low, so at UTM the outline sat ~0.5 m off its own fill (the hatch
  fill renders precisely via the world_origin-relative HatchModel pipeline).
  FallbackGeometry now carries absolute f64 points + key vertices; the
  tessellate dispatcher splits them into the double-single high/low pair the
  relative-to-eye wire pass needs, so outlines land exactly on their fills.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 10:09:19 +03:00

104 lines
4.8 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.
// Shared helpers used by per-entity tessellation impls in `crate::entities`
// and the dispatcher in `crate::scene::convert::tessellate`.
//
// Anything cross-entity (colour resolution, arc sampling, chord tolerance)
// lives here. Entity-specific helpers (dim format strings, mleader path
// types, etc.) stay with their entity file.
use acadrust::types::Color as AcadColor;
use glam::Vec3;
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
/// Output of the fallback per-entity geometry path used by entities not
/// covered by the truck topology pipeline (Viewport, Insert, Hatch
/// outline, Ole2Frame). Tuple form preserved to avoid touching every
/// callsite when the dispatcher wraps these into a WireModel.
///
/// Layout: `(points, snap_pts, tangent_geoms, key_vertices)`.
///
/// `points` are ABSOLUTE world coordinates in f64 — the dispatcher splits them
/// into the double-single high/low pair the relative-to-eye renderer needs, so
/// fallback outlines (hatch boundary, viewport/insert/ole2frame frames) stay
/// glued to their fills at UTM scale instead of quantizing ~0.5 m in f32.
pub type FallbackGeometry = (
Vec<[f64; 3]>,
Vec<(Vec3, SnapHint)>,
Vec<TangentGeom>,
Vec<[f64; 3]>,
);
// ── Arc tessellation helpers ─────────────────────────────────────────────
/// Convert hatch-boundary arc `(start, end, ccw)` into a
/// `(start, signed_span)` ready for the sampling loop. Matches the
/// legacy `(TAU - sa, TAU - ea)` flip used here for years — direction
/// semantics are preserved on real files. (Wrap-through-2π is a known
/// edge case in that convention; do not "fix" it without a wider audit
/// of how upstream writers emit CW boundary arcs.)
///
/// VERIFIED against real AutoCAD output (KSR-039 DWG, CW arc edges with
/// line neighbours): the stored angles of a `ccw = false` arc edge are
/// MIRRORED — the true point is `center + r·(cos(TAU-θ), sin(TAU-θ))`.
/// Sampling the mirrored parameter directly (this function's flip) gives
/// exact endpoint continuity with the adjacent edges (Δ = 0.000000),
/// while interpreting them as true angles lands tens of units away. Any
/// code that *produces* CW boundary arcs (mirror transforms, explode)
/// must therefore store mirrored angles, not geometric ones.
pub fn arc_signed_span(start: f64, end: f64, ccw: bool) -> (f64, f64) {
const TAU: f64 = std::f64::consts::TAU;
let (sa, ea) = if ccw { (start, end) } else { (TAU - start, TAU - end) };
(sa, ea - sa)
}
/// Segment count for an arc, targeting `chord_tol_world` chord-height
/// error in world units. Floor 8, cap 512.
///
/// Two production callers:
/// - hatch fill polygon (built at load / on edit) passes a radius-
/// relative tol via [`fill_chord_tol`] — ~0.1% radius, zoom-free so
/// the polygon stays sharp at extreme zoom-in without re-tessellation.
/// - hatch wire outline (re-tessellated every frame inside the render
/// scope) passes a zoom-adaptive tol via [`wire_chord_tol`] — pulls
/// from the per-frame override set by `Scene::wires_for_block` so
/// far-out arcs collapse to a handful of segments.
pub fn arc_segments(radius: f64, span_abs: f64, chord_tol_world: f64) -> u32 {
if span_abs < 1e-9 || radius < 1e-9 {
return 1;
}
let tol = chord_tol_world.max(1e-9).min(radius * 0.99);
// θ where r * (1 - cos(θ/2)) = tol → θ = 2 * acos(1 - tol/r).
let max_step = (2.0 * (1.0 - tol / radius).acos()).max(1e-6);
((span_abs / max_step).ceil() as u32).clamp(8, 512)
}
/// Chord tolerance for the load-time fill polygon: 0.1% of radius,
/// floor 1 µm so degenerate radii still produce a workable count.
pub fn fill_chord_tol(radius: f64) -> f64 {
(radius * 0.001).max(1e-6)
}
/// Chord tolerance for the per-frame wire outline: pulls the active
/// `truck_tess::set_curve_tol_override` value (Scene sets it to
/// `world_per_pixel × 0.5` so curves stay at ~half-pixel chord error at
/// the current zoom). When no override is active (snap / hit-test
/// passes, load-time builds), falls back to [`fill_chord_tol`] so we
/// never under-sample.
pub fn wire_chord_tol(radius: f64) -> f64 {
match crate::scene::convert::truck_tess::active_curve_tol() {
Some(t) => t.min(fill_chord_tol(radius)).max(1e-9),
None => fill_chord_tol(radius),
}
}
// ── Colour helper ──────────────────────────────────────────────────────────
/// Convert an acadrust Color (ACI index or true-color) to a GPU RGBA value.
pub fn aci_to_rgba(color: &AcadColor) -> [f32; 4] {
if let Some((r, g, b)) = color.rgb() {
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
} else {
WireModel::WHITE
}
}