Skip to main content

brep_kernel/feature_pipeline/assembly/
constraints.rs

1//! The nine assembly-constraint definitions (build-spec §4) — ONE MODULE PER
2//! CONSTRAINT, the feature-module pattern: each `constraints/<type>.rs` owns
3//! everything about its type — its [`ConstraintTypeDef`] row (names, element
4//! count, duplicate family, the `applicable` selection-context predicate), its
5//! schema entry, and (for the mate-mapped types) its `map` function. This root
6//! only AGGREGATES them in the spec §4 table order (`feature_pipeline/schema.rs`
7//! pattern) and serves the catalogue, which joins the kernel schema export
8//! under its own namespace (`feature_schemas_json` → `assemblyConstraints`) so
9//! the app dialog engine renders constraint dialogs exactly like feature
10//! dialogs. `fixed` has no `map` — grounding is handled by [`super::lifecycle`]
11//! before body building.
12
13use crate::feature_pipeline::SelectionProbe;
14
15pub(crate) mod angle;
16pub(crate) mod coincident;
17pub(crate) mod concentric;
18pub(crate) mod distance;
19pub(crate) mod fixed;
20pub(crate) mod parallel;
21pub(crate) mod perpendicular;
22pub(crate) mod tangent;
23pub(crate) mod touch_align;
24
25/// One constraint type's static definition.
26pub struct ConstraintTypeDef {
27    /// Canonical `type` string (lowercase snake — the persisted key).
28    pub type_id: &'static str,
29    /// Id-mint prefix (`CONC` → `CONC3`) and the panel's short label.
30    pub short_name: &'static str,
31    /// Display label (glyph-prefixed, matching the retired panel's names).
32    pub long_name: &'static str,
33    /// Plain label for messages ("Duplicate … conflicts with Distance
34    /// constraint DIST4.").
35    pub label: &'static str,
36    /// Required `elements` count (fixed takes 1, everything else 2).
37    pub element_count: usize,
38    /// Member of the duplicate-detection family (overlapping selection pairs
39    /// conflict across these types — requirements §4.3).
40    pub duplicate_family: bool,
41    /// Selection-context applicability (the feature `context_applicable`
42    /// pattern, [`crate::feature_pipeline::context_offer`]): does the current
43    /// selection make creating this constraint meaningful? Every element must
44    /// be component geometry ([`super::mapping`]'s `resolve_element` rejects
45    /// everything else), and the counted kinds are the ones the app's element
46    /// seeder can actually produce (faces/edges, solids as COMPONENT refs —
47    /// never vertices, whose `@`-refs only the modal picker builds).
48    pub applicable: fn(&SelectionProbe) -> bool,
49}
50
51/// Spec §4 table order — also the panel's `+` dropdown order.
52pub const CONSTRAINT_TYPES: [ConstraintTypeDef; 9] = [
53    fixed::DEF,
54    coincident::DEF,
55    touch_align::DEF,
56    parallel::DEF,
57    distance::DEF,
58    angle::DEF,
59    concentric::DEF,
60    perpendicular::DEF,
61    tangent::DEF,
62];
63
64/// Look up a type definition by its canonical `type` string.
65pub fn constraint_type(type_id: &str) -> Option<&'static ConstraintTypeDef> {
66    CONSTRAINT_TYPES.iter().find(|def| def.type_id == type_id)
67}
68
69/// The nine constraint schemas, spec §4 order. `number` params are
70/// expression-capable against the part's expression scope (the dialog engine's
71/// established convention).
72pub fn constraint_schema_catalogue() -> serde_json::Value {
73    serde_json::Value::Array(vec![
74        fixed::schema(),
75        coincident::schema(),
76        touch_align::schema(),
77        parallel::schema(),
78        distance::schema(),
79        angle::schema(),
80        concentric::schema(),
81        perpendicular::schema(),
82        tangent::schema(),
83    ])
84}
85
86/// A two-element pairing across TWO distinct components (a pair on one rigid
87/// body cannot move anything), counting `seedable` selected elements — the
88/// shared shape behind every pairing type's `applicable`.
89pub(super) fn pair_applicable(probe: &SelectionProbe, seedable: usize) -> bool {
90    probe.all_component && probe.components == 2 && seedable == 2
91}
92
93// --- shared schema-field builders (each module's `schema()` composes these) --
94
95pub(super) fn id_field() -> serde_json::Value {
96    serde_json::json!({
97        "type": "string",
98        "default_value": null,
99        "hint": "Unique identifier for the constraint"
100    })
101}
102
103pub(super) fn elements_field(filter: &[&str], count: usize, hint: &str) -> serde_json::Value {
104    serde_json::json!({
105        "type": "reference_selection",
106        "selectionFilter": filter,
107        "multiple": count > 1,
108        "minSelections": count,
109        "maxSelections": count,
110        "default_value": null,
111        "hint": hint
112    })
113}
114
115pub(super) fn schema_entry(
116    def: &ConstraintTypeDef,
117    params: serde_json::Value,
118) -> serde_json::Value {
119    serde_json::json!({
120        "type": def.type_id,
121        "shortName": def.short_name,
122        "longName": def.long_name,
123        "inputParamsSchema": params,
124    })
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn probe(
132        solids: usize,
133        faces: usize,
134        edges: usize,
135        components: usize,
136        all_component: bool,
137    ) -> SelectionProbe {
138        SelectionProbe {
139            solids,
140            faces,
141            edges,
142            components,
143            all_component,
144            ..Default::default()
145        }
146    }
147
148    /// Fixed wants exactly ONE component, selected via member solid(s);
149    /// the two-element types want a pair across TWO distinct components.
150    #[test]
151    fn constraint_applicability_shapes() {
152        let one_component = probe(1, 0, 0, 1, true);
153        let two_faces_two_components = probe(0, 2, 0, 2, true);
154        let face_edge_two_components = probe(0, 1, 1, 2, true);
155        let two_faces_one_component = probe(0, 2, 0, 1, true);
156        let non_component = probe(0, 2, 0, 0, false);
157
158        let applicable = |type_id: &str, p: &SelectionProbe| {
159            (constraint_type(type_id).expect(type_id).applicable)(p)
160        };
161
162        assert!(applicable("fixed", &one_component));
163        assert!(!applicable("fixed", &two_faces_two_components));
164
165        for pair in [
166            "coincident",
167            "touch_align",
168            "parallel",
169            "distance",
170            "angle",
171            "concentric",
172            "perpendicular",
173        ] {
174            assert!(applicable(pair, &two_faces_two_components), "{pair}: 2 faces / 2 comps");
175            assert!(applicable(pair, &face_edge_two_components), "{pair}: face+edge / 2 comps");
176            // A pair on ONE rigid body cannot move anything.
177            assert!(!applicable(pair, &two_faces_one_component), "{pair}: same component");
178            // Non-component geometry cannot resolve (`mapping::resolve_element`).
179            assert!(!applicable(pair, &non_component), "{pair}: non-component");
180        }
181
182        // Tangent is face-only: a face+edge pair does not qualify.
183        assert!(applicable("tangent", &two_faces_two_components));
184        assert!(!applicable("tangent", &face_edge_two_components));
185
186        // Coincident also accepts whole components via member solids.
187        assert!(applicable("coincident", &probe(2, 0, 0, 2, true)));
188    }
189}