1use rustc_hash::FxHashSet;
4
5use crate::feature_pipeline::{
6 AddedSolid, Axis, EdgeRef, FaceRef, FeatureContext, FeatureResult, Frame, SketchProfile,
7};
8use crate::{AffineTransform, BooleanOperation, BooleanOptions, BrepSolid, NurbsCurve, Vec3, transform_brep};
9
10pub fn rotate_half_turn_about_x(solid: BrepSolid) -> Result<BrepSolid, String> {
17 let half_turn = AffineTransform::new([
18 1.0, 0.0, 0.0, 0.0,
19 0.0, -1.0, 0.0, 0.0,
20 0.0, 0.0, -1.0, 0.0,
21 0.0, 0.0, 0.0, 1.0,
22 ])?;
23 transform_brep(&solid, half_turn, false)
24}
25
26pub fn collect_face_names(solid: &BrepSolid) -> Vec<(u64, String)> {
28 let mut names = Vec::new();
29 for shell in &solid.shells {
30 for face in &shell.faces {
31 if let Some(name) = &face.name {
32 names.push((face.id, name.clone()));
33 }
34 }
35 }
36 names
37}
38
39pub fn collect_edge_names(solid: &BrepSolid) -> Vec<(u64, String)> {
41 solid
42 .edges
43 .iter()
44 .filter_map(|edge| edge.name.as_ref().map(|name| (edge.id, name.clone())))
45 .collect()
46}
47
48pub fn stamp_face_role(face_name: &str, role: &str, op_type: &str) {
54 let mut record = serde_json::Map::new();
55 record.insert("faceRole".into(), serde_json::Value::String(role.into()));
56 record.insert(
57 "operationFaceType".into(),
58 serde_json::Value::String(op_type.into()),
59 );
60 crate::feature_pipeline::scene_metadata::merge_record(face_name, &record, true);
61}
62
63pub fn stamp_sweep_roles(solid: &BrepSolid, wall_suffix: &str, start_name: &str, end_name: &str) {
69 stamp_sweep_roles_multi(
70 solid,
71 wall_suffix,
72 std::slice::from_ref(&start_name.to_string()),
73 std::slice::from_ref(&end_name.to_string()),
74 );
75}
76
77pub fn stamp_sweep_roles_multi(
81 solid: &BrepSolid,
82 wall_suffix: &str,
83 start_names: &[String],
84 end_names: &[String],
85) {
86 for face in solid.shells.iter().flat_map(|shell| shell.faces.iter()) {
87 let Some(name) = face.name.as_deref() else {
88 continue;
89 };
90 if start_names.iter().any(|start| start == name) {
91 stamp_face_role(name, "start_cap", "STARTCAP");
92 } else if end_names.iter().any(|end| end == name) {
93 stamp_face_role(name, "end_cap", "ENDCAP");
94 } else if name.ends_with(wall_suffix) {
95 stamp_face_role(name, "sidewall", "SIDEWALL");
96 }
97 }
98}
99
100pub struct CapNames {
111 pub per_loop: Vec<(String, String)>,
113 pub start_container: String,
114 pub end_container: String,
115}
116
117impl CapNames {
118 pub fn new(cap_base: &str, regions: &[Vec<crate::feature_pipeline::ProfileLoop>]) -> Self {
125 let per_loop = regions
126 .iter()
127 .map(|region| {
128 match region.first().and_then(|outer| outer.key()) {
129 Some(key) => (
130 format!("{cap_base}:{key}_START"),
131 format!("{cap_base}:{key}_END"),
132 ),
133 None => (format!("{cap_base}_START"), format!("{cap_base}_END")),
134 }
135 })
136 .collect();
137 Self {
138 per_loop,
139 start_container: format!("{cap_base}_START"),
140 end_container: format!("{cap_base}_END"),
141 }
142 }
143
144 pub fn containers(&self) -> Vec<(String, Vec<String>)> {
149 [
150 (self.start_container.clone(), self.starts()),
151 (self.end_container.clone(), self.ends()),
152 ]
153 .into_iter()
154 .filter(|(container, members)| members.as_slice() != [container.clone()])
155 .collect()
156 }
157
158 pub fn interior(&self, region_index: usize, segment: &str) -> (String, String) {
170 let (start, end) = &self.per_loop[region_index];
171 let start_base = start.strip_suffix("_START").unwrap_or(start);
172 let end_base = end.strip_suffix("_END").unwrap_or(end);
173 (
174 format!("{start_base}:{segment}_START"),
175 format!("{end_base}:{segment}_END"),
176 )
177 }
178
179 pub fn starts(&self) -> Vec<String> {
181 self.per_loop.iter().map(|(start, _)| start.clone()).collect()
182 }
183
184 pub fn ends(&self) -> Vec<String> {
186 self.per_loop.iter().map(|(_, end)| end.clone()).collect()
187 }
188}
189
190pub(super) fn parse_boolean_operation(operation: &str) -> Result<BooleanOperation, String> {
191 match operation {
192 "UNION" => Ok(BooleanOperation::Union),
193 "SUBTRACT" => Ok(BooleanOperation::Subtract),
194 "INTERSECT" => Ok(BooleanOperation::Intersect),
195 other => Err(format!("unsupported boolean operation '{other}'")),
196 }
197}
198
199struct BooleanParam {
201 operation: String,
202 targets: Vec<String>,
203 merge_coplanar_faces: bool,
204}
205
206fn read_boolean_param(ctx: &FeatureContext) -> BooleanParam {
207 let boolean = ctx.param("boolean");
208 let operation = boolean
209 .and_then(|value| value.get("operation"))
210 .and_then(|value| value.as_str())
211 .unwrap_or("NONE")
212 .to_uppercase();
213 let targets = boolean
214 .and_then(|value| value.get("targets"))
215 .and_then(|value| value.as_array())
216 .map(|array| {
217 array
218 .iter()
219 .filter_map(|entry| entry.as_str())
220 .map(|name| name.trim().to_string())
221 .filter(|name| !name.is_empty())
222 .collect()
223 })
224 .unwrap_or_default();
225 let merge_coplanar_faces = boolean
226 .and_then(|value| value.get("mergeCoplanarFaces"))
227 .and_then(|value| value.as_bool())
228 .unwrap_or(true);
229 BooleanParam {
230 operation,
231 targets,
232 merge_coplanar_faces,
233 }
234}
235
236fn collect_edge_face_names(
238 solid: &BrepSolid,
239) -> (std::collections::HashMap<u64, Vec<String>>, Vec<u64>) {
240 use std::collections::HashMap;
241 let mut edge_faces: HashMap<u64, Vec<String>> = HashMap::new();
242 let mut encounter_order: Vec<u64> = Vec::new();
243 for shell in &solid.shells {
244 for face in &shell.faces {
245 let face_name = face.name.clone().unwrap_or_default();
246 for loop_record in &face.loops {
247 for coedge in &loop_record.coedges {
248 let entry = edge_faces.entry(coedge.edge_id).or_insert_with(|| {
249 encounter_order.push(coedge.edge_id);
250 Vec::new()
251 });
252 if entry.len() < 2 && !entry.contains(&face_name) {
253 entry.push(face_name.clone());
254 }
255 }
256 }
257 }
258 }
259 (edge_faces, encounter_order)
260}
261
262pub fn stamp_derived_edge_names(solid: &mut BrepSolid) {
266 use std::collections::HashMap;
267 let (edge_faces, encounter_order) = collect_edge_face_names(solid);
268 let solid_name_fallback = "Solid".to_string();
269 let mut base_counts: HashMap<String, usize> = HashMap::new();
270 for edge_id in encounter_order {
271 let Some(edge) = solid.edges.iter_mut().find(|edge| edge.id == edge_id) else {
272 continue;
273 };
274 if edge.degenerate {
275 continue;
276 }
277 let mut faces: Vec<String> = edge_faces
278 .get(&edge_id)
279 .map(|list| list.iter().filter(|n| !n.is_empty()).cloned().collect())
280 .unwrap_or_default();
281 faces.sort();
282 let base = if faces.len() >= 2 {
283 format!("{}|{}", faces[0], faces[1])
284 } else {
285 format!(
286 "{}|BOUNDARY",
287 faces.first().unwrap_or(&solid_name_fallback)
288 )
289 };
290 let is_topology_name = edge.name.as_deref().is_none_or(|n| n.contains('|'));
292 if is_topology_name {
293 let index = base_counts.entry(base.clone()).or_insert(0);
294 edge.name = Some(format!("{base}[{index}]"));
295 *index += 1;
296 }
297 }
298}
299
300pub fn namespace_copy_names(solid: &mut BrepSolid, suffix: &str) {
304 for shell in &mut solid.shells {
305 for face in &mut shell.faces {
306 if let Some(name) = face.name.as_ref() {
307 let trimmed = name.trim();
308 if !trimmed.is_empty() {
309 face.name = Some(format!("{trimmed}::{suffix}"));
310 }
311 }
312 }
313 }
314 for edge in &mut solid.edges {
315 if let Some(name) = edge.name.as_ref() {
316 let trimmed = name.trim();
317 if !trimmed.is_empty() && !trimmed.contains('|') {
320 edge.name = Some(format!("{trimmed}::{suffix}"));
321 }
322 }
323 }
324 stamp_derived_edge_names(solid);
325}
326
327pub fn ensure_unique_face_names(solid: &mut BrepSolid) {
331 use std::collections::HashMap;
332 let mut counts: HashMap<String, usize> = HashMap::new();
333 for shell in &solid.shells {
334 for face in &shell.faces {
335 if let Some(name) = &face.name {
336 *counts.entry(name.clone()).or_insert(0) += 1;
337 }
338 }
339 }
340 let mut running: HashMap<String, usize> = HashMap::new();
341 for shell in &mut solid.shells {
342 for face in &mut shell.faces {
343 let Some(name) = face.name.clone() else { continue };
344 if counts.get(&name).copied().unwrap_or(0) > 1 {
345 let index = running.entry(name.clone()).or_insert(0);
346 face.name = Some(format!("{name}[{index}]"));
347 *index += 1;
348 }
349 }
350 }
351}
352
353pub fn register_added(base: BrepSolid, name: &str) -> AddedSolid {
358 register_added_grouped(base, name, &[])
359}
360
361pub fn register_added_grouped(
371 mut base: BrepSolid,
372 name: &str,
373 containers: &[(String, Vec<String>)],
374) -> AddedSolid {
375 ensure_unique_face_names(&mut base);
376 stamp_derived_edge_names(&mut base);
377 let face_names = collect_face_names(&base);
378 let edge_names = collect_edge_names(&base);
379 let face_groups = collect_face_groups(&face_names, containers);
380 let mut edge_groups = collect_edge_group_aliases(&base, containers);
381 collect_derived_edge_bases(&edge_names, &mut edge_groups);
382 let handle = crate::register_solid_value(base);
383 AddedSolid {
384 handle,
385 name: name.to_string(),
386 face_names,
387 edge_names,
388 face_groups,
389 edge_groups,
390 }
391}
392
393fn collect_derived_edge_bases(
412 edge_names: &[(u64, String)],
413 groups: &mut Vec<(String, Vec<String>)>,
414) {
415 use std::collections::HashSet;
416 let taken: HashSet<&str> = groups.iter().map(|(name, _)| name.as_str()).collect();
417 let exact: HashSet<&str> = edge_names.iter().map(|(_, name)| name.as_str()).collect();
418 let mut bases: Vec<(String, Vec<String>)> = Vec::new();
419 for (_, name) in edge_names {
420 let Some(base) = name.strip_suffix(']').and_then(|head| {
422 let at = head.rfind('[')?;
423 let (base, index) = (&head[..at], &head[at + 1..]);
424 (!index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
425 .then_some(base)
426 }) else {
427 continue;
428 };
429 if base.is_empty() || !base.contains('|') {
430 continue;
431 }
432 if taken.contains(base) || exact.contains(base) {
433 continue;
434 }
435 match bases.iter_mut().find(|(existing, _)| existing == base) {
436 Some((_, members)) => members.push(name.clone()),
437 None => bases.push((base.to_string(), vec![name.clone()])),
438 }
439 }
440 groups.append(&mut bases);
441}
442
443fn collect_face_groups(
446 face_names: &[(u64, String)],
447 containers: &[(String, Vec<String>)],
448) -> Vec<(String, Vec<String>)> {
449 let mut groups = Vec::new();
450 for (container, members) in containers {
451 let live: Vec<String> = members
452 .iter()
453 .filter(|member| face_names.iter().any(|(_, name)| name == *member))
454 .cloned()
455 .collect();
456 if !live.is_empty() {
457 groups.push((container.clone(), live));
458 }
459 }
460 groups
461}
462
463fn collect_edge_group_aliases(
478 solid: &BrepSolid,
479 containers: &[(String, Vec<String>)],
480) -> Vec<(String, Vec<String>)> {
481 use std::collections::HashMap;
482 if containers.is_empty() {
483 return Vec::new();
484 }
485 let mut container_of: HashMap<&str, &str> = HashMap::new();
487 for (container, members) in containers {
488 for member in members {
489 container_of.insert(member.as_str(), container.as_str());
490 }
491 }
492
493 let (edge_faces, encounter_order) = collect_edge_face_names(solid);
494
495 let solid_name_fallback = "Solid".to_string();
496 let mut base_counts: HashMap<String, usize> = HashMap::new();
497 let mut aliases: HashMap<String, Vec<String>> = HashMap::new();
498 let mut order: Vec<String> = Vec::new();
499 for edge_id in encounter_order {
500 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
501 continue;
502 };
503 if edge.degenerate {
504 continue;
505 }
506 if !edge.name.as_deref().is_none_or(|name| name.contains('|')) {
509 continue;
510 }
511 let mut faces: Vec<String> = edge_faces
512 .get(&edge_id)
513 .map(|list| list.iter().filter(|name| !name.is_empty()).cloned().collect())
514 .unwrap_or_default();
515 let substituted = faces.iter().any(|name| container_of.contains_key(name.as_str()));
518 for face in &mut faces {
519 if let Some(container) = container_of.get(face.as_str()) {
520 *face = (*container).to_string();
521 }
522 }
523 faces.sort();
524 let base = if faces.len() >= 2 {
525 format!("{}|{}", faces[0], faces[1])
526 } else {
527 format!("{}|BOUNDARY", faces.first().unwrap_or(&solid_name_fallback))
528 };
529 let index = base_counts.entry(base.clone()).or_insert(0);
530 let alias = format!("{base}[{index}]");
531 *index += 1;
532 let Some(live) = edge.name.clone() else { continue };
535 if !substituted || live == alias {
536 continue;
537 }
538 if aliases.entry(alias.clone()).or_default().is_empty() {
539 order.push(alias.clone());
540 }
541 aliases.get_mut(&alias).expect("just inserted").push(live);
542 }
543 order
544 .into_iter()
545 .map(|alias| {
546 let ids = aliases.remove(&alias).unwrap_or_default();
547 (alias, ids)
548 })
549 .filter(|(_, ids)| !ids.is_empty())
550 .collect()
551}
552
553pub fn finalize_solids(ctx: &FeatureContext, bodies: Vec<(String, BrepSolid)>) -> FeatureResult {
558 let boolean = read_boolean_param(ctx);
559 let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
560
561 let separate = |mut result: FeatureResult, bodies: Vec<(String, BrepSolid)>| {
562 for (name, solid) in bodies {
563 result.added.push(register_added(solid, &name));
564 }
565 result
566 };
567
568 if boolean.operation == "NONE" || boolean.targets.is_empty() {
569 return separate(result, bodies);
570 }
571 let operation = match parse_boolean_operation(&boolean.operation) {
572 Ok(operation) => operation,
573 Err(error) => return ctx.fail(error),
574 };
575 let resolved = resolve_solid_names(ctx.scene, &boolean.targets, &mut result.unresolved);
576 if resolved.is_empty() {
577 return separate(result, bodies);
578 }
579 let options = BooleanOptions {
580 merge_coplanar_faces: boolean.merge_coplanar_faces,
581 ..BooleanOptions::default()
582 };
583 let mut bodies = bodies.into_iter();
584 let Some((_, first)) = bodies.next() else {
585 return result; };
587 let mut current = first;
590 for (_, target_handle) in &resolved {
591 let operand = crate::register_solid_value(current);
592 let folded = crate::with_two_registered_solids(*target_handle, operand, |target, op_solid| {
593 crate::boolean_operation(target, op_solid, operation, &options)
594 });
595 crate::free_registered_solid(operand);
596 match folded {
597 Ok(solid) => current = solid,
598 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
599 }
600 }
601 for (_, body) in bodies {
602 match crate::boolean_operation(¤t, &body, operation, &options) {
603 Ok(solid) => current = solid,
604 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
605 }
606 }
607 result.added.push(register_added(current, &resolved[0].0));
608 result.removed = resolved.into_iter().map(|(name, _)| name).collect();
609 result
610}
611
612pub fn finalize_solid(ctx: &FeatureContext, base: BrepSolid, base_name: &str) -> FeatureResult {
613 finalize_solid_grouped(ctx, base, base_name, &[])
614}
615
616pub fn finalize_solid_grouped(
622 ctx: &FeatureContext,
623 base: BrepSolid,
624 base_name: &str,
625 containers: &[(String, Vec<String>)],
626) -> FeatureResult {
627 let boolean = read_boolean_param(ctx);
628 let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
629
630 if boolean.operation == "NONE" || boolean.targets.is_empty() {
631 result.added.push(register_added_grouped(base, base_name, containers));
632 return result;
633 }
634
635 let operation = match parse_boolean_operation(&boolean.operation) {
636 Ok(operation) => operation,
637 Err(error) => return ctx.fail(error),
638 };
639
640 let resolved = resolve_solid_names(ctx.scene, &boolean.targets, &mut result.unresolved);
641
642 if resolved.is_empty() {
645 result.added.push(register_added_grouped(base, base_name, containers));
646 return result;
647 }
648
649 let options = BooleanOptions {
650 merge_coplanar_faces: boolean.merge_coplanar_faces,
651 ..BooleanOptions::default()
652 };
653 let mut current = base;
657 for (_, target_handle) in &resolved {
658 let operand_handle = crate::register_solid_value(current);
659 let folded = crate::with_two_registered_solids(*target_handle, operand_handle, |target, operand| {
660 crate::boolean_operation(target, operand, operation, &options)
661 });
662 crate::free_registered_solid(operand_handle);
663 match folded {
664 Ok(solid) => current = solid,
665 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
666 }
667 }
668
669 let result_name = resolved[0].0.clone();
670 result
671 .added
672 .push(register_added_grouped(current, &result_name, containers));
673 result.removed = resolved.into_iter().map(|(name, _)| name).collect();
674 result
675}
676
677pub struct BlendTarget {
686 pub handle: u32,
687 pub name: String,
688 pub edge_points: Vec<Vec3>,
689 pub edge_names: Vec<String>,
692}
693
694pub struct BlendSelection {
696 pub target: Option<BlendTarget>,
698 pub multi_solid: bool,
699 pub unresolved: Vec<String>,
701}
702
703enum ResolvedRef {
705 Edge(u64),
706 Face(u64),
707}
708
709pub(crate) fn reference_name(value: &serde_json::Value) -> Option<String> {
713 let raw = match value {
714 serde_json::Value::String(text) => Some(text.as_str()),
715 serde_json::Value::Object(map) => map.get("name").and_then(|value| value.as_str()),
716 _ => None,
717 }?;
718 let trimmed = raw.trim();
719 (!trimmed.is_empty()).then(|| trimmed.to_string())
720}
721
722fn edge_name_or_id(solid: &BrepSolid, edge_id: u64) -> String {
728 solid
729 .edges
730 .iter()
731 .find(|edge| edge.id == edge_id)
732 .and_then(|edge| edge.name.clone())
733 .filter(|name| !name.is_empty())
734 .unwrap_or_else(|| format!("E{edge_id}"))
735}
736
737fn sample_edge_midpoint(solid: &BrepSolid, edge_id: u64) -> Result<Vec3, String> {
740 let edge = solid
741 .edges
742 .iter()
743 .find(|edge| edge.id == edge_id)
744 .ok_or_else(|| format!("dressup: edge {edge_id} not found on target solid"))?;
745 let mid = (edge.t0 + edge.t1) * 0.5;
746 edge.curve
747 .evaluate(mid)
748 .map_err(|error| format!("dressup: edge {edge_id} midpoint sample failed: {error}"))
749}
750
751fn face_boundary_edges(solid: &BrepSolid, face_id: u64) -> Result<Vec<u64>, String> {
754 let face = solid
755 .shells
756 .iter()
757 .flat_map(|shell| &shell.faces)
758 .find(|face| face.id == face_id)
759 .ok_or_else(|| format!("dressup: face {face_id} not found on target solid"))?;
760 let mut ids = Vec::new();
761 let mut seen = FxHashSet::default();
762 for loop_record in &face.loops {
763 for coedge in &loop_record.coedges {
764 let degenerate = solid
765 .edges
766 .iter()
767 .find(|edge| edge.id == coedge.edge_id)
768 .map(|edge| edge.degenerate)
769 .unwrap_or(false);
770 if degenerate {
771 continue;
772 }
773 if seen.insert(coedge.edge_id) {
774 ids.push(coedge.edge_id);
775 }
776 }
777 }
778 Ok(ids)
779}
780
781pub fn resolve_blend_selection(ctx: &FeatureContext) -> Result<BlendSelection, String> {
786 let names = reference_names(ctx.param("edges"));
787
788 let mut unresolved = Vec::new();
789 let mut resolved: Vec<(u32, ResolvedRef)> = Vec::new();
790 for name in names {
791 let edges = ctx.scene.resolve_edge_group(&name);
797 let faces = ctx.scene.resolve_face_group(&name);
798 if edges.is_empty() && faces.is_empty() {
799 unresolved.push(name);
800 continue;
801 }
802 for edge in edges {
803 resolved.push((edge.handle, ResolvedRef::Edge(edge.edge_id)));
804 }
805 for face in faces {
806 resolved.push((face.handle, ResolvedRef::Face(face.face_id)));
807 }
808 }
809
810 let mut handles: Vec<u32> = resolved.iter().map(|(handle, _)| *handle).collect();
812 handles.sort_unstable();
813 handles.dedup();
814 if handles.len() > 1 {
815 return Ok(BlendSelection {
816 target: None,
817 multi_solid: true,
818 unresolved,
819 });
820 }
821 let Some(&handle) = handles.first() else {
822 return Ok(BlendSelection {
824 target: None,
825 multi_solid: false,
826 unresolved,
827 });
828 };
829
830 let name = ctx
834 .scene
835 .solids
836 .iter()
837 .find(|(_, ®istered)| registered == handle)
838 .map(|(name, _)| name.clone())
839 .ok_or_else(|| format!("dressup: target handle {handle} has no scene name"))?;
840
841 let (edge_points, edge_names) = crate::with_registered_solid_str(handle, |solid| {
846 let mut seen = FxHashSet::default();
847 let mut points = Vec::new();
848 let mut names = Vec::new();
849 for (_, entity) in &resolved {
850 match entity {
851 ResolvedRef::Edge(edge_id) => {
852 if seen.insert(*edge_id) {
853 points.push(sample_edge_midpoint(solid, *edge_id)?);
854 names.push(edge_name_or_id(solid, *edge_id));
855 }
856 }
857 ResolvedRef::Face(face_id) => {
858 for edge_id in face_boundary_edges(solid, *face_id)? {
859 if seen.insert(edge_id) {
860 points.push(sample_edge_midpoint(solid, edge_id)?);
861 names.push(edge_name_or_id(solid, edge_id));
862 }
863 }
864 }
865 }
866 }
867 Ok((points, names))
868 })?;
869
870 Ok(BlendSelection {
871 target: Some(BlendTarget {
872 handle,
873 name,
874 edge_points,
875 edge_names,
876 }),
877 multi_solid: false,
878 unresolved,
879 })
880}
881
882pub fn blend_face_base(id: &str) -> String {
884 format!("{}:BLEND", if id.is_empty() { "F" } else { id })
885}
886
887pub fn blend_face_name(id: &str, edge_name: &str) -> String {
889 format!("{}:{}", blend_face_base(id), edge_name)
890}
891
892pub fn require_blend_direction(ctx: &FeatureContext, operation: &str) -> Result<(), String> {
894 let direction = ctx
895 .param("direction")
896 .and_then(|value| value.as_str())
897 .map(|text| text.trim().to_uppercase())
898 .filter(|text| !text.is_empty())
899 .unwrap_or_else(|| "AUTO".to_string());
900 if direction != "AUTO" && direction != "INSET" {
901 return Err(format!(
902 "{operation} direction '{direction}' belonged to the removed legacy mesh pipeline (only AUTO/INSET exist)"
903 ));
904 }
905 Ok(())
906}
907
908pub(super) fn number_or_default(ctx: &FeatureContext, key: &str, default: f64) -> f64 {
911 match ctx.param(key) {
912 None | Some(serde_json::Value::Null) => default,
913 Some(_) => match ctx.number(key) {
914 Ok(value) if value.is_finite() => value,
915 _ => default,
916 },
917 }
918}
919
920pub fn optional_number(ctx: &FeatureContext, key: &str) -> Result<Option<f64>, String> {
925 if ctx.param(key).is_some() {
926 ctx.number(key).map(Some)
927 } else {
928 Ok(None)
929 }
930}
931
932pub fn compose_trs_matrix(
943 translate: [f64; 3],
944 rotate_rad: [f64; 3],
945 scale: [f64; 3],
946 pivot: [f64; 3],
947) -> [f64; 16] {
948 let (a, b) = (rotate_rad[0].cos(), rotate_rad[0].sin());
949 let (c, d) = (rotate_rad[1].cos(), rotate_rad[1].sin());
950 let (e, f) = (rotate_rad[2].cos(), rotate_rad[2].sin());
951 let (ae, af, be, bf) = (a * e, a * f, b * e, b * f);
952 let r = [
954 [c * e, -c * f, d],
955 [af + be * d, ae - bf * d, -b * c],
956 [bf - ae * d, be + af * d, a * c],
957 ];
958 let mut rs = [[0.0f64; 3]; 3];
960 for i in 0..3 {
961 for j in 0..3 {
962 rs[i][j] = r[i][j] * scale[j];
963 }
964 }
965 let rs_pivot = [
966 rs[0][0] * pivot[0] + rs[0][1] * pivot[1] + rs[0][2] * pivot[2],
967 rs[1][0] * pivot[0] + rs[1][1] * pivot[1] + rs[1][2] * pivot[2],
968 rs[2][0] * pivot[0] + rs[2][1] * pivot[1] + rs[2][2] * pivot[2],
969 ];
970 let tx = translate[0] + pivot[0] - rs_pivot[0];
971 let ty = translate[1] + pivot[1] - rs_pivot[1];
972 let tz = translate[2] + pivot[2] - rs_pivot[2];
973 [
974 rs[0][0], rs[0][1], rs[0][2], tx, rs[1][0], rs[1][1], rs[1][2], ty, rs[2][0], rs[2][1], rs[2][2], tz, 0.0, 0.0, 0.0, 1.0,
978 ]
979}
980
981pub fn vec3_from_value(
988 env: &crate::feature_pipeline::Env,
989 value: Option<&serde_json::Value>,
990 key: &str,
991 default: [f64; 3],
992) -> Result<[f64; 3], String> {
993 let Some(value) = value.filter(|value| !value.is_null()) else {
994 return Ok(default);
995 };
996 let component = |slot: Option<&serde_json::Value>, fallback: f64| -> Result<f64, String> {
997 match slot {
998 None | Some(serde_json::Value::Null) => Ok(fallback),
999 Some(serde_json::Value::Number(number)) => number
1000 .as_f64()
1001 .ok_or_else(|| format!("param `{key}` has a non-finite component")),
1002 Some(serde_json::Value::String(source)) => {
1003 let number = env
1004 .eval(source)
1005 .map_err(|error| format!("param `{key}`: {error}"))?;
1006 if !number.is_finite() {
1011 return Err(format!(
1012 "param `{key}`: `{source}` evaluated to {number}"
1013 ));
1014 }
1015 Ok(number)
1016 }
1017 Some(other) => Err(format!(
1018 "param `{key}` component must be a number or expression, found {other}"
1019 )),
1020 }
1021 };
1022 if let Some(array) = value.as_array() {
1023 return Ok([
1024 component(array.first(), default[0])?,
1025 component(array.get(1), default[1])?,
1026 component(array.get(2), default[2])?,
1027 ]);
1028 }
1029 if value.is_object() {
1030 return Ok([
1031 component(value.get("x"), default[0])?,
1032 component(value.get("y"), default[1])?,
1033 component(value.get("z"), default[2])?,
1034 ]);
1035 }
1036 Err(format!("param `{key}` must be a vec3 array or object"))
1037}
1038
1039pub(super) fn resolve_solid_names(
1045 scene: &crate::feature_pipeline::SceneMap,
1046 names: &[String],
1047 unresolved: &mut Vec<String>,
1048) -> Vec<(String, u32)> {
1049 let mut resolved = Vec::new();
1050 for name in names {
1051 match scene.resolve_solid(name) {
1052 Some(handle) => resolved.push((name.clone(), handle)),
1053 None => unresolved.push(name.clone()),
1054 }
1055 }
1056 resolved
1057}
1058
1059pub(super) fn sheet_body_name(ctx: &FeatureContext) -> Option<String> {
1062 first_reference_name(ctx.param("sheet")).or_else(|| {
1063 let mut bodies = ctx.scene.solids.iter().filter_map(|(name, &handle)| {
1064 crate::feature_pipeline::sheet_metal::get_tree(handle).map(|_| name.clone())
1065 });
1066 match (bodies.next(), bodies.next()) {
1067 (Some(only), None) => Some(only),
1068 _ => None,
1069 }
1070 })
1071}
1072
1073pub(super) fn single_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1076 let name = match value {
1077 Some(serde_json::Value::String(text)) => Some(text.clone()),
1078 Some(serde_json::Value::Array(array)) => array.iter().find_map(|entry| {
1079 entry
1080 .as_str()
1081 .or_else(|| entry.get("name").and_then(|name| name.as_str()))
1082 .map(str::to_string)
1083 }),
1084 Some(object @ serde_json::Value::Object(_)) => {
1085 object.get("name").and_then(|name| name.as_str()).map(str::to_string)
1086 }
1087 _ => None,
1088 }?;
1089 let trimmed = name.trim();
1090 (!trimmed.is_empty()).then(|| trimmed.to_string())
1091}
1092
1093pub fn first_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1097 fn one(value: &serde_json::Value) -> Option<String> {
1098 match value {
1099 serde_json::Value::Array(items) => items.iter().find_map(one),
1100 other => reference_name(other),
1101 }
1102 }
1103 value.and_then(one)
1104}
1105
1106pub fn normalize_profile_alias(name: String) -> String {
1114 match name.strip_suffix(":FACE") {
1115 Some(base) => base.to_string(),
1116 None => name,
1117 }
1118}
1119
1120pub(crate) fn reference_name_array(value: Option<&serde_json::Value>) -> Vec<String> {
1123 value
1124 .and_then(serde_json::Value::as_array)
1125 .map(|items| items.iter().filter_map(reference_name).collect())
1126 .unwrap_or_default()
1127}
1128
1129pub fn reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1132 match value {
1133 Some(serde_json::Value::Array(items)) => items.iter().filter_map(reference_name).collect(),
1134 Some(other) => reference_name(other).into_iter().collect(),
1135 None => Vec::new(),
1136 }
1137}
1138
1139pub(super) fn unique_reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1141 let mut seen = std::collections::HashSet::new();
1142 reference_names(value)
1143 .into_iter()
1144 .filter(|name| seen.insert(name.clone()))
1145 .collect()
1146}
1147
1148pub fn consume_profile_sketch(ctx: &FeatureContext) -> bool {
1151 !matches!(
1152 ctx.param("consumeProfileSketch"),
1153 Some(serde_json::Value::Bool(false))
1154 )
1155}
1156
1157pub fn consume_sketch(ctx: &FeatureContext, reference_name: &str, result: &mut FeatureResult) {
1161 if !consume_profile_sketch(ctx) {
1162 return;
1163 }
1164 consume_sketch_always(reference_name, result);
1165}
1166
1167pub fn consume_sketch_always(reference_name: &str, result: &mut FeatureResult) {
1172 let base = reference_name
1173 .strip_suffix(":PROFILE")
1174 .unwrap_or(reference_name)
1175 .to_string();
1176 if !result.removed.contains(&base) {
1177 result.removed.push(base);
1178 }
1179}
1180
1181pub fn profile_loop_uv_curves(
1187 profile: &crate::feature_pipeline::SketchProfile,
1188 profile_loop: &crate::feature_pipeline::ProfileLoop,
1189) -> Result<Vec<NurbsCurve>, String> {
1190 let mut curves = Vec::with_capacity(profile_loop.curves.len());
1191 for curve in &profile_loop.curves {
1192 let mut control_points = Vec::with_capacity(curve.control_points.len());
1193 for cp in &curve.control_points {
1194 let delta = cp.point()?.sub(profile.origin);
1195 control_points.push(crate::Vec4::from_point(
1196 Vec3::new(delta.dot(profile.x_axis), delta.dot(profile.y_axis), 0.0),
1197 cp.w,
1198 ));
1199 }
1200 curves.push(NurbsCurve::new(
1201 curve.degree,
1202 curve.knots.clone(),
1203 control_points,
1204 )?);
1205 }
1206 Ok(curves)
1207}
1208
1209pub fn resolve_path(ctx: &FeatureContext, name: &str) -> Result<Vec<NurbsCurve>, String> {
1217 if let Some(chain) = ctx.scene.resolve_path(name) {
1218 return Ok(chain.clone());
1219 }
1220 if let Some(edge) = ctx.scene.resolve_edge(name) {
1221 return Ok(vec![edge_curve(edge)?]);
1222 }
1223 if let Some((owner, geometry)) = name.rsplit_once(':') {
1224 if geometry.len() > 1 && geometry.starts_with('G') {
1225 if let Some(chain) = ctx.scene.resolve_path(owner) {
1226 return Ok(chain.clone());
1227 }
1228 }
1229 }
1230 Err(format!(
1231 "path '{name}' not found (no sketch path or resident edge)"
1232 ))
1233}
1234
1235#[derive(Debug, Clone)]
1245pub struct PathSegment {
1246 pub name: String,
1247 pub curve: NurbsCurve,
1248}
1249
1250pub fn resolve_path_chain(
1270 ctx: &FeatureContext,
1271 names: &[String],
1272) -> Result<Vec<PathSegment>, String> {
1273 let mut segments: Vec<PathSegment> = Vec::new();
1274 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1275 for reference in names {
1276 let curves = resolve_path(ctx, reference)?;
1277 let published = ctx.scene.resolve_path_segment_names(reference);
1278 let single = curves.len() == 1;
1279 for (index, curve) in curves.into_iter().enumerate() {
1280 let name = published
1281 .and_then(|names| names.get(index).cloned().flatten())
1282 .filter(|name| !name.trim().is_empty())
1283 .unwrap_or_else(|| {
1284 if single {
1285 reference.clone()
1286 } else {
1287 format!("{reference}[{index}]")
1288 }
1289 });
1290 if seen.insert(name.clone()) {
1291 segments.push(PathSegment { name, curve });
1292 }
1293 }
1294 }
1295 if segments.is_empty() {
1296 return Err("path selection resolved to no curves".into());
1297 }
1298 chain_path_segments(segments)
1299}
1300
1301fn path_endpoints(curve: &NurbsCurve) -> Result<(Vec3, Vec3), String> {
1303 let [t0, t1] = curve.domain()?;
1304 Ok((curve.evaluate(t0)?, curve.evaluate(t1)?))
1305}
1306
1307fn chain_path_segments(segments: Vec<PathSegment>) -> Result<Vec<PathSegment>, String> {
1310 if segments.len() < 2 {
1311 return Ok(segments);
1312 }
1313 let ends: Vec<(Vec3, Vec3)> = segments
1314 .iter()
1315 .map(|segment| path_endpoints(&segment.curve))
1316 .collect::<Result<_, _>>()?;
1317 let scale = ends
1321 .iter()
1322 .flat_map(|(a, b)| [a, b])
1323 .map(|point| point.sub(ends[0].0).length())
1324 .fold(1.0_f64, f64::max);
1325 let tolerance = 1e-5 * scale;
1326 let joins = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
1327
1328 let mut used = vec![false; segments.len()];
1329 used[0] = true;
1330 let mut order: Vec<(usize, bool)> = vec![(0, false)];
1333 let mut head = ends[0].0;
1334 let mut tail = ends[0].1;
1335 loop {
1336 if order.len() > 1 && joins(tail, head) {
1339 break;
1340 }
1341 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, tail, tolerance) {
1342 used[index] = true;
1343 order.push((index, reversed));
1344 tail = far;
1345 continue;
1346 }
1347 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, head, tolerance) {
1348 used[index] = true;
1349 order.insert(0, (index, !reversed));
1351 head = far;
1352 continue;
1353 }
1354 break;
1355 }
1356 let stranded: Vec<&str> = segments
1357 .iter()
1358 .zip(&used)
1359 .filter(|(_, used)| !**used)
1360 .map(|(segment, _)| segment.name.as_str())
1361 .collect();
1362 if !stranded.is_empty() {
1363 return Err(format!(
1364 "the selected path edges do not form ONE connected chain — {} joins neither \
1365 end of the chain the other selections form (a sweep path must be a single \
1366 head-to-tail run; select connected edges, or one branch at a time)",
1367 stranded.join(", ")
1368 ));
1369 }
1370 let mut chained = Vec::with_capacity(segments.len());
1371 let mut taken: Vec<Option<PathSegment>> = segments.into_iter().map(Some).collect();
1372 for (index, reversed) in order {
1373 let mut segment = taken[index].take().expect("each segment placed once");
1374 if reversed {
1375 segment.curve = segment.curve.reversed()?;
1376 }
1377 chained.push(segment);
1378 }
1379 Ok(chained)
1380}
1381
1382fn attach_path_segment(
1385 ends: &[(Vec3, Vec3)],
1386 used: &[bool],
1387 cursor: Vec3,
1388 tolerance: f64,
1389) -> Option<(usize, bool, Vec3)> {
1390 for (index, (start, end)) in ends.iter().enumerate() {
1391 if used[index] {
1392 continue;
1393 }
1394 if start.sub(cursor).length() <= tolerance {
1395 return Some((index, false, *end));
1396 }
1397 if end.sub(cursor).length() <= tolerance {
1398 return Some((index, true, *start));
1399 }
1400 }
1401 None
1402}
1403
1404pub fn translate_curves(curves: &[NurbsCurve], offset: Vec3) -> Result<Vec<NurbsCurve>, String> {
1408 curves
1409 .iter()
1410 .map(|curve| translate_curve(curve, offset))
1411 .collect()
1412}
1413
1414pub fn sketch_plane_frame(ctx: &FeatureContext, names: &[String]) -> Option<Frame> {
1427 for name in names {
1428 if let Some(frame) = ctx.scene.resolve_frame(name) {
1429 return Some(frame);
1430 }
1431 if let Some((owner, _)) = name.rsplit_once(':') {
1432 if let Some(frame) = ctx.scene.resolve_frame(owner) {
1433 return Some(frame);
1434 }
1435 }
1436 }
1437 None
1438}
1439
1440pub fn sketch_base_name(ctx: &FeatureContext, name: &str) -> String {
1450 if let Some((owner, tail)) = name.rsplit_once(':') {
1451 if tail.len() > 1 && tail.starts_with('G') && ctx.scene.resolve_frame(owner).is_some() {
1452 return owner.to_string();
1453 }
1454 }
1455 name.to_string()
1456}
1457
1458pub fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
1463 let [start, end] = curve.domain()?;
1464 let epsilon = (1e-9 * (end - start)).max(2e-9);
1465 let mut result = curve.clone();
1466 if t0 > start + epsilon && t0 < end - epsilon {
1467 result = result.split(t0)?.1;
1468 }
1469 let domain = result.domain()?;
1470 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
1471 result = result.split(t1)?.0;
1472 }
1473 Ok(result)
1474}
1475
1476pub fn edge_curve(edge: EdgeRef) -> Result<NurbsCurve, String> {
1489 crate::with_registered_solid_str(edge.handle, |solid| {
1490 let record = solid
1491 .edges
1492 .iter()
1493 .find(|candidate| candidate.id == edge.edge_id)
1494 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1495 trimmed_curve(&record.curve, record.t0, record.t1)
1496 })
1497}
1498
1499pub fn edge_axis(edge: EdgeRef) -> Result<Axis, String> {
1503 crate::with_registered_solid_str(edge.handle, |solid| {
1504 let record = solid
1505 .edges
1506 .iter()
1507 .find(|candidate| candidate.id == edge.edge_id)
1508 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1509 let start = record.curve.evaluate(record.t0)?;
1510 let end = record.curve.evaluate(record.t1)?;
1511 let direction = end.sub(start).normalized()?;
1512 Ok(Axis {
1513 point: start,
1514 direction,
1515 })
1516 })
1517}
1518
1519pub fn translate_curve(curve: &NurbsCurve, offset: Vec3) -> Result<NurbsCurve, String> {
1534 let mut control_points = Vec::with_capacity(curve.control_points.len());
1535 for cp in &curve.control_points {
1536 let cartesian = cp.point()?;
1537 control_points.push(crate::Vec4::from_point(cartesian.add(offset), cp.w));
1538 }
1539 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
1540}
1541
1542pub struct HoleGroup {
1545 pub index: usize,
1547 pub depth: usize,
1549 pub children: Vec<HoleGroup>,
1550}
1551
1552pub fn hole_nesting(
1558 profile: &SketchProfile,
1559 region: &[crate::feature_pipeline::ProfileLoop],
1560) -> Result<Vec<HoleGroup>, String> {
1561 let count = region.len().saturating_sub(1);
1562 if count == 0 {
1563 return Ok(Vec::new());
1564 }
1565 let mut polygons: Vec<Vec<[f64; 2]>> = Vec::with_capacity(count);
1567 for hole in ®ion[1..] {
1568 let mut polygon = Vec::new();
1569 for curve in &hole.curves {
1570 let [t0, t1] = curve.domain()?;
1571 for step in 0..8 {
1572 let t = t0 + (t1 - t0) * (step as f64 / 8.0);
1573 let point = curve.evaluate(t)?;
1574 let delta = point.sub(profile.origin);
1575 polygon.push([delta.dot(profile.x_axis), delta.dot(profile.y_axis)]);
1576 }
1577 }
1578 polygons.push(polygon);
1579 }
1580 let mut contains = vec![vec![false; count]; count];
1582 for i in 0..count {
1583 let Some(representative) = polygons[i].first().copied() else {
1584 continue;
1585 };
1586 for j in 0..count {
1587 if i != j && point_in_polygon_uv(representative, &polygons[j]) {
1588 contains[i][j] = true;
1589 }
1590 }
1591 }
1592 let depths: Vec<usize> = (0..count)
1593 .map(|i| contains[i].iter().filter(|inside| **inside).count())
1594 .collect();
1595 Ok((0..count)
1596 .filter(|&i| depths[i] == 0)
1597 .map(|i| hole_group(i, &contains, &depths))
1598 .collect())
1599}
1600
1601fn hole_group(i: usize, contains: &[Vec<bool>], depths: &[usize]) -> HoleGroup {
1604 let children = (0..depths.len())
1605 .filter(|&k| contains[k][i] && depths[k] == depths[i] + 1)
1606 .map(|k| hole_group(k, contains, depths))
1607 .collect();
1608 HoleGroup {
1609 index: i + 1,
1610 depth: depths[i],
1611 children,
1612 }
1613}
1614
1615fn point_in_polygon_uv(point: [f64; 2], polygon: &[[f64; 2]]) -> bool {
1617 let count = polygon.len();
1618 if count < 3 {
1619 return false;
1620 }
1621 let mut inside = false;
1622 let mut j = count - 1;
1623 for i in 0..count {
1624 let pi = polygon[i];
1625 let pj = polygon[j];
1626 let intersects = (pi[1] > point[1]) != (pj[1] > point[1])
1627 && point[0] < (pj[0] - pi[0]) * (point[1] - pi[1]) / (pj[1] - pi[1]) + pi[0];
1628 if intersects {
1629 inside = !inside;
1630 }
1631 j = i;
1632 }
1633 inside
1634}
1635
1636pub fn name_hole_cutter_faces(cutter: &mut BrepSolid, id: &str, key: &str, segment: Option<&str>) {
1643 let name = hole_face_name(id, key, segment);
1644 for face in cutter
1645 .shells
1646 .iter_mut()
1647 .flat_map(|shell| shell.faces.iter_mut())
1648 {
1649 if face.name.is_none() {
1650 face.name = Some(name.clone());
1651 }
1652 }
1653}
1654
1655pub fn hole_face_name(id: &str, key: &str, segment: Option<&str>) -> String {
1660 match segment {
1661 Some(segment) => format!("{id}:HOLE:{key}:{segment}"),
1662 None => format!("{id}:HOLE:{key}"),
1663 }
1664}
1665
1666pub fn subtract_solid(body: BrepSolid, cutter: BrepSolid) -> Result<BrepSolid, String> {
1669 let options = BooleanOptions {
1670 merge_coplanar_faces: true,
1671 ..BooleanOptions::default()
1672 };
1673 let body_handle = crate::register_solid_value(body);
1674 let cutter_handle = crate::register_solid_value(cutter);
1675 let cut = crate::with_two_registered_solids(body_handle, cutter_handle, |body, tool| {
1676 crate::boolean_operation(body, tool, BooleanOperation::Subtract, &options)
1677 });
1678 crate::free_registered_solid(body_handle);
1679 crate::free_registered_solid(cutter_handle);
1680 cut.map_err(String::from)
1682}
1683
1684pub fn hole_prism(
1690 hole_curves: &[NurbsCurve],
1691 direction: Vec3,
1692 distance: f64,
1693 depth: usize,
1694) -> Result<BrepSolid, String> {
1695 let dir = direction.normalized()?;
1696 let span = distance.abs();
1697 let margin = span.max(1.0) * (depth as f64 + 1.0);
1698 let start_t = distance.min(0.0) - margin;
1699 let length = span + 2.0 * margin;
1700 let offset = dir.scale(start_t);
1701 let mut curves = Vec::with_capacity(hole_curves.len());
1702 for curve in hole_curves {
1703 curves.push(translate_curve(curve, offset)?);
1704 }
1705 crate::extrude_profile_brep(&curves, dir, length)
1706}
1707
1708pub fn subtract_hole_prism(
1713 feature: &str,
1714 body: BrepSolid,
1715 hole_curves: &[NurbsCurve],
1716 direction: Vec3,
1717 distance: f64,
1718 id: &str,
1719 key: &str,
1720) -> Result<BrepSolid, String> {
1721 if hole_curves.len() < 2 {
1722 return Ok(body);
1723 }
1724 let mut cutter = hole_prism(hole_curves, direction, distance, 0)?;
1725 name_hole_cutter_faces(&mut cutter, id, key, None);
1726 subtract_solid(body, cutter)
1727 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))
1728}
1729
1730pub fn subtract_region_holes(
1743 mut body: BrepSolid,
1744 profile: &SketchProfile,
1745 region: &[crate::feature_pipeline::ProfileLoop],
1746 feature: &str,
1747 id: &str,
1748 segment: Option<&str>,
1749 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, String>,
1750) -> Result<BrepSolid, String> {
1751 for group in hole_nesting(profile, region)? {
1752 let key = hole_key(®ion[group.index], group.index);
1753 let Some(cutter) = hole_cutter(region, &group, id, segment, build)
1754 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))?
1755 else {
1756 continue;
1757 };
1758 body = subtract_solid(body, cutter)
1759 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))?;
1760 }
1761 Ok(body)
1762}
1763
1764pub fn hole_key(hole: &crate::feature_pipeline::ProfileLoop, index: usize) -> String {
1768 hole.key().unwrap_or_else(|| index.to_string())
1769}
1770
1771fn hole_cutter(
1774 region: &[crate::feature_pipeline::ProfileLoop],
1775 group: &HoleGroup,
1776 id: &str,
1777 segment: Option<&str>,
1778 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, String>,
1779) -> Result<Option<BrepSolid>, String> {
1780 if region[group.index].curves.len() < 2 {
1781 return Ok(None);
1782 }
1783 let mut cutter = build(group.index, group.depth)?;
1784 name_hole_cutter_faces(
1785 &mut cutter,
1786 id,
1787 &hole_key(®ion[group.index], group.index),
1788 segment,
1789 );
1790 for child in &group.children {
1791 let Some(child_cutter) = hole_cutter(region, child, id, segment, build)? else {
1792 continue;
1793 };
1794 cutter = subtract_solid(cutter, child_cutter).map_err(|error| {
1795 format!("island {} carve from hole {} failed: {error}", child.index, group.index)
1796 })?;
1797 }
1798 Ok(Some(cutter))
1799}
1800
1801pub enum PlaneLikeRef {
1813 Face(FaceRef),
1815 Frame(Frame),
1817}
1818
1819impl PlaneLikeRef {
1820 pub fn point_normal(&self) -> Result<(Vec3, Vec3), String> {
1825 match self {
1826 PlaneLikeRef::Frame(frame) => Ok((frame.origin, frame.z_axis)),
1827 PlaneLikeRef::Face(face) => face_point_normal(*face),
1828 }
1829 }
1830
1831 pub fn frame(&self) -> Result<Frame, String> {
1834 match self {
1835 PlaneLikeRef::Frame(frame) => Ok(*frame),
1836 PlaneLikeRef::Face(face) => face_frame(*face),
1837 }
1838 }
1839}
1840
1841pub fn resolve_plane_reference(ctx: &FeatureContext, name: &str) -> Option<PlaneLikeRef> {
1846 if let Some(face) = ctx.scene.resolve_face(name) {
1847 return Some(PlaneLikeRef::Face(face));
1848 }
1849 ctx.scene.resolve_frame(name).map(PlaneLikeRef::Frame)
1850}
1851
1852pub fn face_point_normal(face: FaceRef) -> Result<(Vec3, Vec3), String> {
1857 crate::with_registered_solid_str(face.handle, |solid| {
1858 for shell in &solid.shells {
1859 for record in &shell.faces {
1860 if record.id != face.face_id {
1861 continue;
1862 }
1863 let [u0, u1] = record.surface.domain_u()?;
1864 let [v0, v1] = record.surface.domain_v()?;
1865 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
1866 let point = record.surface.evaluate(um, vm)?;
1867 let mut normal = record.surface.normal(um, vm)?;
1868 if !record.same_sense {
1869 normal = normal.scale(-1.0);
1870 }
1871 return Ok((point, normal));
1872 }
1873 }
1874 Err(format!("face {} not found on solid", face.face_id))
1875 })
1876}
1877
1878pub fn face_boundary_points(
1885 solid: &BrepSolid,
1886 record: &crate::FaceRecord,
1887) -> Result<Vec<Vec3>, String> {
1888 let mut points = Vec::new();
1889 for loop_record in &record.loops {
1890 for coedge in &loop_record.coedges {
1891 let Some(edge) = solid.edges.iter().find(|e| e.id == coedge.edge_id) else {
1892 continue;
1893 };
1894 if edge.degenerate {
1895 continue;
1896 }
1897 for step in 0..=4 {
1898 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
1899 points.push(edge.curve.evaluate(t)?);
1900 }
1901 }
1902 }
1903 Ok(points)
1904}
1905
1906pub fn bounds_of(points: &[Vec3]) -> Option<(Vec3, Vec3)> {
1908 let mut iter = points.iter();
1909 let first = *iter.next()?;
1910 let (mut min, mut max) = (first, first);
1911 for point in iter {
1912 min = Vec3::new(min.x.min(point.x), min.y.min(point.y), min.z.min(point.z));
1913 max = Vec3::new(max.x.max(point.x), max.y.max(point.y), max.z.max(point.z));
1914 }
1915 Some((min, max))
1916}
1917
1918pub fn face_frame(face: FaceRef) -> Result<Frame, String> {
1925 crate::with_registered_solid_str(face.handle, |solid| {
1926 let record = solid
1927 .shells
1928 .iter()
1929 .flat_map(|shell| &shell.faces)
1930 .find(|candidate| candidate.id == face.face_id)
1931 .ok_or_else(|| format!("face {} not found on solid", face.face_id))?;
1932
1933 let [u0, u1] = record.surface.domain_u()?;
1934 let [v0, v1] = record.surface.domain_v()?;
1935 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
1936
1937 let center_normal = record.surface.normal(um, vm)?;
1939 for (u, v) in [(u0, v0), (u1, v0), (u1, v1), (u0, v1)] {
1940 let normal = record.surface.normal(u, v)?;
1941 if normal.dot(center_normal).abs() < 1.0 - 1e-6 {
1942 return Err(
1943 "sketch/plane on a non-planar face is not yet migrated to the Rust pipeline"
1944 .into(),
1945 );
1946 }
1947 }
1948 let normal = if record.same_sense {
1950 center_normal
1951 } else {
1952 center_normal.scale(-1.0)
1953 };
1954
1955 let boundary = face_boundary_points(solid, record)?;
1957 let plane_point = record.surface.evaluate(um, vm)?;
1958 let center = match bounds_of(&boundary) {
1959 Some((min, max)) => min.add(max).scale(0.5),
1960 None => plane_point,
1961 };
1962 let unit = normal.normalized()?;
1964 let signed = center.sub(plane_point).dot(unit);
1965 let origin = center.sub(unit.scale(signed));
1966 Frame::from_origin_normal(origin, normal)
1967 })
1968}
1969
1970pub fn union_region_solids(solids: Vec<BrepSolid>) -> Result<BrepSolid, String> {
1980 union_solids_keeping(solids, &[])
1981}
1982
1983pub fn union_solids_keeping(
1997 solids: Vec<BrepSolid>,
1998 keep_unmerged: &[String],
1999) -> Result<BrepSolid, String> {
2000 let mut iter = solids.into_iter();
2001 let mut current = iter
2002 .next()
2003 .ok_or("profile produced no region solids to union")?;
2004 let options = BooleanOptions {
2005 merge_coplanar_faces: true,
2006 keep_unmerged_name_substrs: keep_unmerged.to_vec(),
2007 ..BooleanOptions::default()
2008 };
2009 for next in iter {
2010 let a = crate::register_solid_value(current);
2011 let b = crate::register_solid_value(next);
2012 let unioned = crate::with_two_registered_solids(a, b, |left, right| {
2013 crate::boolean_operation(left, right, BooleanOperation::Union, &options)
2014 });
2015 crate::free_registered_solid(a);
2016 crate::free_registered_solid(b);
2017 current = unioned.map_err(|error| format!("region union failed: {error}"))?;
2018 }
2019 Ok(current)
2020}
2021
2022pub(super) fn primitive_transform_schema() -> serde_json::Value {
2026 serde_json::json!({
2027 "type": "transform",
2028 "default_value": {
2029 "position": [
2030 0,
2031 0,
2032 0
2033 ],
2034 "rotationEuler": [
2035 0,
2036 0,
2037 0
2038 ],
2039 "scale": [
2040 1,
2041 1,
2042 1
2043 ]
2044 },
2045 "referenceSelectionFilter": [
2046 "FACE",
2047 "EDGE",
2048 "VERTEX",
2049 "PLANE",
2050 "DATUM"
2051 ],
2052 "referenceLabel": "Start Reference",
2053 "referencePlaceholder": "Select point, edge, or face…",
2054 "hint": "Select a start reference, then position, rotate, and scale the solid relative to it."
2055 })
2056}
2057
2058pub(super) fn optional_boolean_schema() -> serde_json::Value {
2060 serde_json::json!({
2061 "type": "boolean_operation",
2062 "default_value": {
2063 "targets": [],
2064 "operation": "NONE",
2065 "mergeCoplanarFaces": true
2066 },
2067 "hint": "Optional boolean operation with selected solids"
2068 })
2069}
2070
2071
2072