brep_kernel/feature_pipeline/assembly/constraints.rs
1//! The ten 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 range, 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 center;
17pub(crate) mod coincident;
18pub(crate) mod concentric;
19pub(crate) mod distance;
20pub(crate) mod fixed;
21pub(crate) mod parallel;
22pub(crate) mod perpendicular;
23pub(crate) mod tangent;
24pub(crate) mod touch_align;
25
26/// One constraint type's static definition.
27pub struct ConstraintTypeDef {
28 /// Canonical `type` string (lowercase snake — the persisted key).
29 pub type_id: &'static str,
30 /// Id-mint prefix (`CONC` → `CONC3`).
31 pub short_name: &'static str,
32 /// The type's ONE icon — a single catalogued glyph character (the app's
33 /// `assets/glyphs` catalog, where Coincident shares `≡` with the sketch
34 /// solver's coincident). Every surface that stands for the type by picture
35 /// alone — the workbench toolbar button, the viewport chip, the tree row's
36 /// glyph column — reads THIS field; nothing derives it from `long_name`.
37 pub icon: &'static str,
38 /// Display label: `"{icon} {label}"` (the context bar draws the leading
39 /// glyph as artwork). Pinned to the other two by a test below.
40 pub long_name: &'static str,
41 /// Plain label for messages ("Duplicate … conflicts with Distance
42 /// constraint DIST4.").
43 pub label: &'static str,
44 /// The accepted `elements` count range, inclusive: fixed takes exactly 1,
45 /// the pairing types exactly 2, center 3 or 4 (two width faces plus a one-
46 /// or two-element tab). The lifecycle marks a count outside the range
47 /// `incomplete`; the schema's `minSelections`/`maxSelections` mirror it.
48 pub min_elements: usize,
49 pub max_elements: usize,
50 /// Member of the duplicate-detection family (overlapping selection pairs
51 /// conflict across these types — requirements §4.3).
52 pub duplicate_family: bool,
53 /// Selection-context applicability (the feature `context_applicable`
54 /// pattern, [`crate::feature_pipeline::context_offer`]): does the current
55 /// selection make creating this constraint meaningful? Every element must
56 /// be component geometry ([`super::mapping`]'s `resolve_element` rejects
57 /// everything else), and the counted kinds are the ones the app's element
58 /// seeder can actually produce (faces/edges, solids as COMPONENT refs —
59 /// never vertices, whose `@`-refs only the modal picker builds).
60 pub applicable: fn(&SelectionProbe) -> bool,
61}
62
63/// Spec §4 table order — also the panel's `+` dropdown order.
64pub const CONSTRAINT_TYPES: [ConstraintTypeDef; 10] = [
65 fixed::DEF,
66 coincident::DEF,
67 touch_align::DEF,
68 parallel::DEF,
69 distance::DEF,
70 angle::DEF,
71 concentric::DEF,
72 perpendicular::DEF,
73 tangent::DEF,
74 center::DEF,
75];
76
77/// Look up a type definition by its canonical `type` string.
78pub fn constraint_type(type_id: &str) -> Option<&'static ConstraintTypeDef> {
79 CONSTRAINT_TYPES.iter().find(|def| def.type_id == type_id)
80}
81
82/// The ten constraint schemas, spec §4 order. `number` params are
83/// expression-capable against the part's expression scope (the dialog engine's
84/// established convention).
85pub fn constraint_schema_catalogue() -> serde_json::Value {
86 serde_json::Value::Array(vec![
87 fixed::schema(),
88 coincident::schema(),
89 touch_align::schema(),
90 parallel::schema(),
91 distance::schema(),
92 angle::schema(),
93 concentric::schema(),
94 perpendicular::schema(),
95 tangent::schema(),
96 center::schema(),
97 ])
98}
99
100/// A two-element pairing across TWO distinct components (a pair on one rigid
101/// body cannot move anything), counting `seedable` selected elements — the
102/// shared shape behind every pairing type's `applicable`.
103pub(super) fn pair_applicable(probe: &SelectionProbe, seedable: usize) -> bool {
104 probe.all_component && probe.components == 2 && seedable == 2
105}
106
107// --- shared schema-field builders (each module's `schema()` composes these) --
108
109pub(super) fn id_field() -> serde_json::Value {
110 serde_json::json!({
111 "type": "string",
112 "default_value": null,
113 "hint": "Unique identifier for the constraint"
114 })
115}
116
117/// The `elements` reference field: `min..=max` picks from `filter` (the type
118/// def's `min_elements..=max_elements` — a test below pins the two together).
119pub(super) fn elements_field(
120 filter: &[&str],
121 min: usize,
122 max: usize,
123 hint: &str,
124) -> serde_json::Value {
125 serde_json::json!({
126 "type": "reference_selection",
127 "selectionFilter": filter,
128 "multiple": max > 1,
129 "minSelections": min,
130 "maxSelections": max,
131 "default_value": null,
132 "hint": hint
133 })
134}
135
136pub(super) fn schema_entry(
137 def: &ConstraintTypeDef,
138 params: serde_json::Value,
139) -> serde_json::Value {
140 serde_json::json!({
141 "type": def.type_id,
142 "shortName": def.short_name,
143 "longName": def.long_name,
144 "label": def.label,
145 "icon": def.icon,
146 "inputParamsSchema": params,
147 })
148}
149
150// BREP private tests: 82430eaff3489bbf