Skip to main content

brep_render/sketch/
mod.rs

1//! Sketch documents, constraint solving, interaction state, and overlay geometry.
2
3use crate::json_support::vec3_or as read_vec3;
4use crate::geometry3d::{cross3 as cross, dot3 as dot, sub3 as sub};
5
6pub mod constraint_glyphs;
7pub mod dimensions;
8pub mod doc;
9pub mod external_ref;
10pub mod handdraw;
11pub mod infer;
12pub mod session;
13pub mod solve;
14pub mod spline;
15pub mod tessellate;
16pub mod trim;
17
18pub use doc::{SketchConstraint, SketchDiagnostics, SketchDoc, SketchGeometry, SketchPoint};
19pub use external_ref::{classify_uv, EdgeLink, ExternalRef};
20pub use session::{
21    constraint_ref, entity_ref_eq, geometry_ref, point_ref, refs_equal, SketchSession,
22};
23pub use solve::SketchSolverSettings;
24pub use tessellate::SketchTessellation;
25
26/// An orthonormal placement frame for a sketch plane — origin + in-plane `x`/`y`
27/// axes + the `z` normal, all in world space (`f64`). A plane `(u, v)` coordinate
28/// maps to world `origin + u·x + v·y` (via [`to_world`](Self::to_world)).
29///
30/// Uses double precision to match the sketch solver's coordinates.
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct PlaneFrame {
33    pub origin: [f64; 3],
34    pub x_axis: [f64; 3],
35    pub y_axis: [f64; 3],
36    pub z_axis: [f64; 3],
37}
38
39impl PlaneFrame {
40    /// The world XY plane (identity frame): `u → +x`, `v → +y`, normal `+z`.
41    pub fn xy() -> Self {
42        Self {
43            origin: [0.0, 0.0, 0.0],
44            x_axis: [1.0, 0.0, 0.0],
45            y_axis: [0.0, 1.0, 0.0],
46            z_axis: [0.0, 0.0, 1.0],
47        }
48    }
49
50    /// The world XZ base plane (`datum.rs` normal `(0, -1, 0)`), resolved through
51    /// the same worldUp convention the kernel uses (see [`from_normal`](Self::from_normal)).
52    pub fn xz() -> Self {
53        Self::from_normal([0.0, 0.0, 0.0], [0.0, -1.0, 0.0])
54    }
55
56    /// The world YZ base plane (`datum.rs` normal `(1, 0, 0)`), resolved through
57    /// the same worldUp convention the kernel uses (see [`from_normal`](Self::from_normal)).
58    pub fn yz() -> Self {
59        Self::from_normal([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])
60    }
61
62    /// Derive an orthonormal frame from an `origin` + plane `normal`, a faithful
63    /// port of `feature_pipeline::Frame::from_origin_normal` — the kernel's SINGLE
64    /// source of truth for how a plane reference becomes in-plane axes:
65    ///
66    /// ```text
67    /// refUp = |n·(0,1,0)| > 0.9 ? (1,0,0) : (0,1,0)
68    /// x = norm(refUp × n);  y = norm(n × x);  z = n
69    /// ```
70    ///
71    /// A degenerate (zero / non-finite) normal — or a normal collinear with the
72    /// picked `refUp` — returns the XY identity axes (at `origin`) rather than
73    /// erroring, so callers always get a usable frame.
74    pub fn from_normal(origin: [f64; 3], normal: [f64; 3]) -> Self {
75        let identity = Self {
76            origin,
77            ..Self::xy()
78        };
79        let Some(z) = normalize(normal) else {
80            return identity;
81        };
82        let world_up = [0.0, 1.0, 0.0];
83        let ref_up = if dot(z, world_up).abs() > 0.9 {
84            [1.0, 0.0, 0.0]
85        } else {
86            world_up
87        };
88        let Some(x) = normalize(cross(ref_up, z)) else {
89            return identity;
90        };
91        let Some(y) = normalize(cross(z, x)) else {
92            return identity;
93        };
94        Self {
95            origin,
96            x_axis: x,
97            y_axis: y,
98            z_axis: z,
99        }
100    }
101
102    /// Read a persisted `persistentData.basis` object (`{origin, x, y, z}`, each a
103    /// `[x, y, z]` array) into a frame, mirroring the kernel's `persisted_basis_frame`
104    /// (`features/sketch.rs`). Missing keys default to the identity components, so a
105    /// partial / absent basis still yields a usable XY-ish frame.
106    pub fn from_basis_json(basis: &serde_json::Value) -> Self {
107        Self {
108            origin: read_vec3(basis.get("origin"), [0.0, 0.0, 0.0]),
109            x_axis: read_vec3(basis.get("x"), [1.0, 0.0, 0.0]),
110            y_axis: read_vec3(basis.get("y"), [0.0, 1.0, 0.0]),
111            z_axis: read_vec3(basis.get("z"), [0.0, 0.0, 1.0]),
112        }
113    }
114
115    /// Map a plane `(u, v)` coordinate to world `[x, y, z]`.
116    pub fn to_world(&self, u: f64, v: f64) -> [f64; 3] {
117        [
118            self.origin[0] + self.x_axis[0] * u + self.y_axis[0] * v,
119            self.origin[1] + self.x_axis[1] * u + self.y_axis[1] * v,
120            self.origin[2] + self.x_axis[2] * u + self.y_axis[2] * v,
121        ]
122    }
123
124    /// Project a world point onto the plane's `(u, v)` frame — the inverse of
125    /// [`to_world`](Self::to_world). With orthonormal axes this is plain dot
126    /// products against the offset from the origin (`d = world − origin`;
127    /// `u = d·x_axis`, `v = d·y_axis`); a point off the plane projects orthogonally
128    /// (its normal component is dropped). Mirrors the previous sketcher's
129    /// world→UV projection.
130    pub fn to_uv(&self, world: [f64; 3]) -> (f64, f64) {
131        let d = [
132            world[0] - self.origin[0],
133            world[1] - self.origin[1],
134            world[2] - self.origin[2],
135        ];
136        (dot(d, self.x_axis), dot(d, self.y_axis))
137    }
138}
139
140impl Default for PlaneFrame {
141    fn default() -> Self {
142        Self::xy()
143    }
144}
145
146/// Intersect a world-space ray (`origin` + `dir`) with a sketch `plane` and return
147/// the hit's in-plane `(u, v)` coordinate, or `None` when the ray is parallel to
148/// the plane (`|dir·n| < 1e-9`) or the hit is behind the ray origin (`t <= 0`).
149///
150/// This is the pure pixel→plane math behind
151/// [`EngineState::sketch_uv_at`](crate::engine_state::EngineState::sketch_uv_at):
152/// the caller supplies the camera ray (`camera.pick_ray(x, y)`); the plane's axes
153/// are assumed orthonormal, so the world→uv projection is plain dot products.
154pub fn ray_plane_uv(plane: &PlaneFrame, origin: [f64; 3], dir: [f64; 3]) -> Option<(f64, f64)> {
155    let n = plane.z_axis;
156    let denom = dot(dir, n);
157    if denom.abs() < 1e-9 {
158        return None; // ray parallel to the plane
159    }
160    let t = dot(sub(plane.origin, origin), n) / denom;
161    if t <= 0.0 {
162        return None; // plane is behind the ray origin
163    }
164    let hit = [
165        origin[0] + t * dir[0],
166        origin[1] + t * dir[1],
167        origin[2] + t * dir[2],
168    ];
169    let w = sub(hit, plane.origin);
170    Some((dot(w, plane.x_axis), dot(w, plane.y_axis)))
171}
172
173/// The BASE overlay color for one constraint's annotation — the shared constraint
174/// green, or the conflict red while the solver names this constraint in a conflict.
175/// The ONE place that decision is made, so a glyph and a dimension leader in the
176/// same conflict are never colored differently. Selection / hover emphasis is
177/// layered ON TOP of this by
178/// [`tessellate::interaction_color`](crate::sketch::tessellate), so a picked
179/// conflicting constraint still reads as picked.
180pub(crate) fn constraint_base_color(
181    colors: &crate::style::SketchColors,
182    diag: &SketchDiagnostics,
183    id: &serde_json::Value,
184) -> u32 {
185    if diag.constraint_conflicting(id) {
186        colors.conflict
187    } else {
188        colors.constraint
189    }
190}
191
192/// Normalize `v`, or `None` when it is (near) zero / non-finite.
193fn normalize(v: [f64; 3]) -> Option<[f64; 3]> {
194    let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
195    if len.is_finite() && len > 1e-12 {
196        Some([v[0] / len, v[1] / len, v[2] / len])
197    } else {
198        None
199    }
200}
201
202// BREP private tests: f02820da9e62fcac