rscad_core/feature/mod.rs
1//! Sub-object (face/edge) topology for direct modeling.
2//!
3//! CSG trees have no persistent face/edge identity: meshes are regenerated
4//! wholesale and carry no provenance. This module derives that identity
5//! *analytically* from the tree itself — faces are named per-primitive
6//! ([`FaceTag`]), leaves are addressed by structural [`LeafPath`]s, and
7//! [`geom`] evaluates rays/membership against the tree exactly, independent
8//! of the active render backend (mesh or SDF ray-march).
9
10pub mod edge;
11pub mod geom;
12pub mod lower;
13
14pub use edge::*;
15pub use geom::*;
16pub use lower::*;
17
18/// A face of a supported primitive, named in the primitive's local frame.
19///
20/// Tags are stable under parameter edits (resizing a cube does not change
21/// which face is `CubePosX`), which is what makes stored face references
22/// parametric rather than positional.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub enum FaceTag {
26 CubePosX,
27 CubeNegX,
28 CubePosY,
29 CubeNegY,
30 CubePosZ,
31 CubeNegZ,
32 /// Curved side wall of a cylinder.
33 CylSide,
34 /// Cylinder cap at `y = height`.
35 CylTop,
36 /// Cylinder cap at `y = 0`.
37 CylBottom,
38 /// Slanted side of a cone.
39 ConeSide,
40 /// Cone base disk at `y = 0`.
41 ConeBottom,
42 /// The single surface of a sphere.
43 Sphere,
44 /// Face of an AABB proxy standing in for an unsupported subtree
45 /// (sketch/extrude/revolve based solids). Participates in boolean
46 /// occlusion but is never selectable or editable.
47 Unknown,
48}
49
50impl FaceTag {
51 /// Cube face tag for `axis` (0=X, 1=Y, 2=Z) on the positive or negative side.
52 pub fn cube(axis: usize, positive: bool) -> Self {
53 match (axis, positive) {
54 (0, true) => Self::CubePosX,
55 (0, false) => Self::CubeNegX,
56 (1, true) => Self::CubePosY,
57 (1, false) => Self::CubeNegY,
58 (2, true) => Self::CubePosZ,
59 (2, false) => Self::CubeNegZ,
60 _ => Self::Unknown,
61 }
62 }
63
64 /// Whether this face can ever be offered for selection/editing.
65 pub fn is_selectable(self) -> bool {
66 !matches!(self, Self::Unknown)
67 }
68}
69
70/// Structural address of a leaf inside a subtree, independent of node ids.
71///
72/// One index is consumed at each node with **more than one** child on the way
73/// down; single-child wrappers (transforms, colors, modifiers) are traversed
74/// implicitly. This keeps paths valid when an editing gesture lazily inserts
75/// a `Translate`/`Rotate` wrapper above a leaf — only inserting, removing, or
76/// reordering *siblings* under a boolean invalidates a path (callers must
77/// treat resolution as fallible).
78#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80pub struct LeafPath(pub Vec<u16>);
81
82/// Which feature an `EdgeFeature` node applies to its edges.
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub enum EdgeFeatureKind {
86 /// Flat cut replacing the edge.
87 Chamfer,
88 /// Round blend replacing the edge.
89 Fillet,
90}
91
92impl EdgeFeatureKind {
93 pub fn display_name(self) -> &'static str {
94 match self {
95 Self::Chamfer => "Chamfer",
96 Self::Fillet => "Fillet",
97 }
98 }
99}
100
101/// A face of a specific leaf, addressed structurally (survives node-id churn
102/// and scene save/load).
103#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105pub struct FaceRef {
106 pub path: LeafPath,
107 pub face: FaceTag,
108}
109
110/// An edge, identified as the intersection curve of two faces. Stored in
111/// canonical (sorted) order so the same edge always compares equal.
112#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114pub struct EdgeSel {
115 pub a: FaceRef,
116 pub b: FaceRef,
117}
118
119impl EdgeSel {
120 /// Canonicalize: the lexicographically smaller face ref becomes `a`.
121 pub fn new(x: FaceRef, y: FaceRef) -> Self {
122 if x <= y {
123 Self { a: x, b: y }
124 } else {
125 Self { a: y, b: x }
126 }
127 }
128}
129
130/// Payload of an `EdgeFeature` node: which edges to feature and how. Edges
131/// are re-resolved against the current child geometry every evaluation, so
132/// the feature follows parameter edits; unresolvable edges are skipped.
133#[derive(Clone, Debug, PartialEq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
135pub struct EdgeFeatureData {
136 pub kind: EdgeFeatureKind,
137 /// Chamfer setback / fillet radius, in the child's local units.
138 pub size: f64,
139 /// Tessellation for revolved fillet masks (unused by chamfers).
140 pub segments: usize,
141 pub edges: Vec<EdgeSel>,
142}