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_multi(
76 solid: &BrepSolid,
77 wall_suffix: &str,
78 start_names: &[String],
79 end_names: &[String],
80) {
81 for face in solid.shells.iter().flat_map(|shell| shell.faces.iter()) {
82 let Some(name) = face.name.as_deref() else {
83 continue;
84 };
85 if start_names.iter().any(|start| start == name) {
86 stamp_face_role(name, "start_cap", "STARTCAP");
87 } else if end_names.iter().any(|end| end == name) {
88 stamp_face_role(name, "end_cap", "ENDCAP");
89 } else if name.ends_with(wall_suffix) {
90 stamp_face_role(name, "sidewall", "SIDEWALL");
91 }
92 }
93}
94
95pub struct CapNames {
106 pub per_loop: Vec<(String, String)>,
108 pub start_container: String,
109 pub end_container: String,
110}
111
112impl CapNames {
113 pub fn new(cap_base: &str, regions: &[Vec<crate::feature_pipeline::ProfileLoop>]) -> Self {
120 let per_loop = regions
121 .iter()
122 .map(|region| {
123 match region.first().and_then(|outer| outer.key()) {
124 Some(key) => (
125 format!("{cap_base}:{key}_START"),
126 format!("{cap_base}:{key}_END"),
127 ),
128 None => (format!("{cap_base}_START"), format!("{cap_base}_END")),
129 }
130 })
131 .collect();
132 Self {
133 per_loop,
134 start_container: format!("{cap_base}_START"),
135 end_container: format!("{cap_base}_END"),
136 }
137 }
138
139 pub fn containers(&self) -> Vec<(String, Vec<String>)> {
144 [
145 (self.start_container.clone(), self.starts()),
146 (self.end_container.clone(), self.ends()),
147 ]
148 .into_iter()
149 .filter(|(container, members)| members.as_slice() != [container.clone()])
150 .collect()
151 }
152
153 pub fn interior(&self, region_index: usize, segment: &str) -> (String, String) {
165 let (start, end) = &self.per_loop[region_index];
166 let start_base = start.strip_suffix("_START").unwrap_or(start);
167 let end_base = end.strip_suffix("_END").unwrap_or(end);
168 (
169 format!("{start_base}:{segment}_START"),
170 format!("{end_base}:{segment}_END"),
171 )
172 }
173
174 pub fn starts(&self) -> Vec<String> {
176 self.per_loop.iter().map(|(start, _)| start.clone()).collect()
177 }
178
179 pub fn ends(&self) -> Vec<String> {
181 self.per_loop.iter().map(|(_, end)| end.clone()).collect()
182 }
183}
184
185pub(super) fn parse_boolean_operation(operation: &str) -> Result<BooleanOperation, String> {
186 match operation {
187 "UNION" => Ok(BooleanOperation::Union),
188 "SUBTRACT" => Ok(BooleanOperation::Subtract),
189 "INTERSECT" => Ok(BooleanOperation::Intersect),
190 other => Err(format!("unsupported boolean operation '{other}'")),
191 }
192}
193
194struct BooleanParam {
196 operation: String,
197 targets: Vec<String>,
198 merge_coplanar_faces: bool,
199}
200
201fn read_boolean_param(ctx: &FeatureContext) -> BooleanParam {
202 let boolean = ctx.param("boolean");
203 let operation = boolean
204 .and_then(|value| value.get("operation"))
205 .and_then(|value| value.as_str())
206 .unwrap_or("NONE")
207 .to_uppercase();
208 let targets = boolean
209 .and_then(|value| value.get("targets"))
210 .and_then(|value| value.as_array())
211 .map(|array| {
212 array
213 .iter()
214 .filter_map(|entry| entry.as_str())
215 .map(|name| name.trim().to_string())
216 .filter(|name| !name.is_empty())
217 .collect()
218 })
219 .unwrap_or_default();
220 let merge_coplanar_faces = boolean
221 .and_then(|value| value.get("mergeCoplanarFaces"))
222 .and_then(|value| value.as_bool())
223 .unwrap_or(true);
224 BooleanParam {
225 operation,
226 targets,
227 merge_coplanar_faces,
228 }
229}
230
231fn collect_edge_face_names(
233 solid: &BrepSolid,
234) -> (std::collections::HashMap<u64, Vec<String>>, Vec<u64>) {
235 use std::collections::HashMap;
236 let mut edge_faces: HashMap<u64, Vec<String>> = HashMap::new();
237 let mut encounter_order: Vec<u64> = Vec::new();
238 for shell in &solid.shells {
239 for face in &shell.faces {
240 let face_name = face.name.clone().unwrap_or_default();
241 for loop_record in &face.loops {
242 for coedge in &loop_record.coedges {
243 let entry = edge_faces.entry(coedge.edge_id).or_insert_with(|| {
244 encounter_order.push(coedge.edge_id);
245 Vec::new()
246 });
247 if entry.len() < 2 && !entry.contains(&face_name) {
248 entry.push(face_name.clone());
249 }
250 }
251 }
252 }
253 }
254 (edge_faces, encounter_order)
255}
256
257pub fn stamp_derived_edge_names(solid: &mut BrepSolid) {
261 use std::collections::HashMap;
262 let (edge_faces, encounter_order) = collect_edge_face_names(solid);
263 let solid_name_fallback = "Solid".to_string();
264 let mut base_counts: HashMap<String, usize> = HashMap::new();
265 for edge_id in encounter_order {
266 let Some(edge) = solid.edges.iter_mut().find(|edge| edge.id == edge_id) else {
267 continue;
268 };
269 if edge.degenerate {
270 continue;
271 }
272 let mut faces: Vec<String> = edge_faces
273 .get(&edge_id)
274 .map(|list| list.iter().filter(|n| !n.is_empty()).cloned().collect())
275 .unwrap_or_default();
276 faces.sort();
277 let base = if faces.len() >= 2 {
278 format!("{}|{}", faces[0], faces[1])
279 } else {
280 format!(
281 "{}|BOUNDARY",
282 faces.first().unwrap_or(&solid_name_fallback)
283 )
284 };
285 let is_topology_name = edge.name.as_deref().is_none_or(|n| n.contains('|'));
287 if is_topology_name {
288 let index = base_counts.entry(base.clone()).or_insert(0);
289 edge.name = Some(format!("{base}[{index}]"));
290 *index += 1;
291 }
292 }
293}
294
295pub fn namespace_copy_names(solid: &mut BrepSolid, suffix: &str) {
299 for shell in &mut solid.shells {
300 for face in &mut shell.faces {
301 if let Some(name) = face.name.as_ref() {
302 let trimmed = name.trim();
303 if !trimmed.is_empty() {
304 face.name = Some(format!("{trimmed}::{suffix}"));
305 }
306 }
307 }
308 }
309 for edge in &mut solid.edges {
310 if let Some(name) = edge.name.as_ref() {
311 let trimmed = name.trim();
312 if !trimmed.is_empty() && !trimmed.contains('|') {
315 edge.name = Some(format!("{trimmed}::{suffix}"));
316 }
317 }
318 }
319 stamp_derived_edge_names(solid);
320}
321
322pub fn ensure_unique_face_names(solid: &mut BrepSolid) {
326 use std::collections::HashMap;
327 let mut counts: HashMap<String, usize> = HashMap::new();
328 for shell in &solid.shells {
329 for face in &shell.faces {
330 if let Some(name) = &face.name {
331 *counts.entry(name.clone()).or_insert(0) += 1;
332 }
333 }
334 }
335 let mut running: HashMap<String, usize> = HashMap::new();
336 for shell in &mut solid.shells {
337 for face in &mut shell.faces {
338 let Some(name) = face.name.clone() else { continue };
339 if counts.get(&name).copied().unwrap_or(0) > 1 {
340 let index = running.entry(name.clone()).or_insert(0);
341 face.name = Some(format!("{name}[{index}]"));
342 *index += 1;
343 }
344 }
345 }
346}
347
348pub fn register_added(base: BrepSolid, name: &str) -> AddedSolid {
353 register_added_grouped(base, name, &[])
354}
355
356pub fn register_added_grouped(
366 mut base: BrepSolid,
367 name: &str,
368 containers: &[(String, Vec<String>)],
369) -> AddedSolid {
370 ensure_unique_face_names(&mut base);
371 stamp_derived_edge_names(&mut base);
372 let face_names = collect_face_names(&base);
373 let edge_names = collect_edge_names(&base);
374 let face_groups = collect_face_groups(&face_names, containers);
375 let mut edge_groups = collect_edge_group_aliases(&base, containers);
376 collect_derived_edge_bases(&edge_names, &mut edge_groups);
377 let handle = crate::register_solid_value(base);
378 AddedSolid {
379 handle,
380 name: name.to_string(),
381 face_names,
382 edge_names,
383 face_groups,
384 edge_groups,
385 }
386}
387
388fn collect_derived_edge_bases(
407 edge_names: &[(u64, String)],
408 groups: &mut Vec<(String, Vec<String>)>,
409) {
410 use std::collections::HashSet;
411 let taken: HashSet<&str> = groups.iter().map(|(name, _)| name.as_str()).collect();
412 let exact: HashSet<&str> = edge_names.iter().map(|(_, name)| name.as_str()).collect();
413 let mut bases: Vec<(String, Vec<String>)> = Vec::new();
414 for (_, name) in edge_names {
415 let Some(base) = name.strip_suffix(']').and_then(|head| {
417 let at = head.rfind('[')?;
418 let (base, index) = (&head[..at], &head[at + 1..]);
419 (!index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
420 .then_some(base)
421 }) else {
422 continue;
423 };
424 if base.is_empty() || !base.contains('|') {
425 continue;
426 }
427 if taken.contains(base) || exact.contains(base) {
428 continue;
429 }
430 match bases.iter_mut().find(|(existing, _)| existing == base) {
431 Some((_, members)) => members.push(name.clone()),
432 None => bases.push((base.to_string(), vec![name.clone()])),
433 }
434 }
435 groups.append(&mut bases);
436}
437
438fn collect_face_groups(
441 face_names: &[(u64, String)],
442 containers: &[(String, Vec<String>)],
443) -> Vec<(String, Vec<String>)> {
444 let mut groups = Vec::new();
445 for (container, members) in containers {
446 let live: Vec<String> = members
447 .iter()
448 .filter(|member| face_names.iter().any(|(_, name)| name == *member))
449 .cloned()
450 .collect();
451 if !live.is_empty() {
452 groups.push((container.clone(), live));
453 }
454 }
455 groups
456}
457
458fn collect_edge_group_aliases(
473 solid: &BrepSolid,
474 containers: &[(String, Vec<String>)],
475) -> Vec<(String, Vec<String>)> {
476 use std::collections::HashMap;
477 if containers.is_empty() {
478 return Vec::new();
479 }
480 let mut container_of: HashMap<&str, &str> = HashMap::new();
482 for (container, members) in containers {
483 for member in members {
484 container_of.insert(member.as_str(), container.as_str());
485 }
486 }
487
488 let (edge_faces, encounter_order) = collect_edge_face_names(solid);
489
490 let solid_name_fallback = "Solid".to_string();
491 let mut base_counts: HashMap<String, usize> = HashMap::new();
492 let mut aliases: HashMap<String, Vec<String>> = HashMap::new();
493 let mut order: Vec<String> = Vec::new();
494 for edge_id in encounter_order {
495 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
496 continue;
497 };
498 if edge.degenerate {
499 continue;
500 }
501 if !edge.name.as_deref().is_none_or(|name| name.contains('|')) {
504 continue;
505 }
506 let mut faces: Vec<String> = edge_faces
507 .get(&edge_id)
508 .map(|list| list.iter().filter(|name| !name.is_empty()).cloned().collect())
509 .unwrap_or_default();
510 let substituted = faces.iter().any(|name| container_of.contains_key(name.as_str()));
513 for face in &mut faces {
514 if let Some(container) = container_of.get(face.as_str()) {
515 *face = (*container).to_string();
516 }
517 }
518 faces.sort();
519 let base = if faces.len() >= 2 {
520 format!("{}|{}", faces[0], faces[1])
521 } else {
522 format!("{}|BOUNDARY", faces.first().unwrap_or(&solid_name_fallback))
523 };
524 let index = base_counts.entry(base.clone()).or_insert(0);
525 let alias = format!("{base}[{index}]");
526 *index += 1;
527 let Some(live) = edge.name.clone() else { continue };
530 if !substituted || live == alias {
531 continue;
532 }
533 if aliases.entry(alias.clone()).or_default().is_empty() {
534 order.push(alias.clone());
535 }
536 aliases.get_mut(&alias).expect("just inserted").push(live);
537 }
538 order
539 .into_iter()
540 .map(|alias| {
541 let ids = aliases.remove(&alias).unwrap_or_default();
542 (alias, ids)
543 })
544 .filter(|(_, ids)| !ids.is_empty())
545 .collect()
546}
547
548pub fn finalize_solids(ctx: &FeatureContext, bodies: Vec<(String, BrepSolid)>) -> FeatureResult {
553 let boolean = read_boolean_param(ctx);
554 let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
555
556 let separate = |mut result: FeatureResult, bodies: Vec<(String, BrepSolid)>| {
557 for (name, solid) in bodies {
558 result.added.push(register_added(solid, &name));
559 }
560 result
561 };
562
563 if boolean.operation == "NONE" || boolean.targets.is_empty() {
564 return separate(result, bodies);
565 }
566 let operation = match parse_boolean_operation(&boolean.operation) {
567 Ok(operation) => operation,
568 Err(error) => return ctx.fail(error),
569 };
570 let resolved = resolve_solid_names(ctx.scene, &boolean.targets, &mut result.unresolved);
571 if resolved.is_empty() {
572 return separate(result, bodies);
573 }
574 let options = BooleanOptions {
575 merge_coplanar_faces: boolean.merge_coplanar_faces,
576 ..BooleanOptions::default()
577 };
578 let mut bodies = bodies.into_iter();
579 let Some((_, first)) = bodies.next() else {
580 return result; };
582 let mut current = first;
585 for (_, target_handle) in &resolved {
586 let operand = crate::register_solid_value(current);
587 let folded = crate::with_two_registered_solids(*target_handle, operand, |target, op_solid| {
588 crate::boolean_operation(target, op_solid, operation, &options)
589 });
590 crate::free_registered_solid(operand);
591 match folded {
592 Ok(solid) => current = solid,
593 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
594 }
595 }
596 for (_, body) in bodies {
597 match crate::boolean_operation(¤t, &body, operation, &options) {
598 Ok(solid) => current = solid,
599 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
600 }
601 }
602 result.added.push(register_added(current, &resolved[0].0));
603 result.removed = resolved.into_iter().map(|(name, _)| name).collect();
604 result
605}
606
607pub fn finalize_solid(ctx: &FeatureContext, base: BrepSolid, base_name: &str) -> FeatureResult {
608 finalize_solid_grouped(ctx, base, base_name, &[])
609}
610
611pub fn finalize_solid_grouped(
617 ctx: &FeatureContext,
618 base: BrepSolid,
619 base_name: &str,
620 containers: &[(String, Vec<String>)],
621) -> FeatureResult {
622 let boolean = read_boolean_param(ctx);
623 let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
624
625 if boolean.operation == "NONE" || boolean.targets.is_empty() {
626 result.added.push(register_added_grouped(base, base_name, containers));
627 return result;
628 }
629
630 let operation = match parse_boolean_operation(&boolean.operation) {
631 Ok(operation) => operation,
632 Err(error) => return ctx.fail(error),
633 };
634
635 let resolved = resolve_solid_names(ctx.scene, &boolean.targets, &mut result.unresolved);
636
637 if resolved.is_empty() {
640 result.added.push(register_added_grouped(base, base_name, containers));
641 return result;
642 }
643
644 let options = BooleanOptions {
645 merge_coplanar_faces: boolean.merge_coplanar_faces,
646 ..BooleanOptions::default()
647 };
648 let mut current = base;
652 for (_, target_handle) in &resolved {
653 let operand_handle = crate::register_solid_value(current);
654 let folded = crate::with_two_registered_solids(*target_handle, operand_handle, |target, operand| {
655 crate::boolean_operation(target, operand, operation, &options)
656 });
657 crate::free_registered_solid(operand_handle);
658 match folded {
659 Ok(solid) => current = solid,
660 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
661 }
662 }
663
664 let result_name = resolved[0].0.clone();
665 result
666 .added
667 .push(register_added_grouped(current, &result_name, containers));
668 result.removed = resolved.into_iter().map(|(name, _)| name).collect();
669 result
670}
671
672pub struct BlendTarget {
682 pub handle: u32,
683 pub name: String,
684 pub edge_points: Vec<Vec3>,
685 pub edge_names: Vec<String>,
688}
689
690pub struct BlendSelection {
692 pub target: Option<BlendTarget>,
694 pub multi_solid: bool,
695 pub unresolved: Vec<String>,
697}
698
699enum ResolvedRef {
701 Edge(u64),
702 Face(u64),
703}
704
705pub(crate) fn reference_name(value: &serde_json::Value) -> Option<String> {
709 let raw = match value {
710 serde_json::Value::String(text) => Some(text.as_str()),
711 serde_json::Value::Object(map) => map.get("name").and_then(|value| value.as_str()),
712 _ => None,
713 }?;
714 let trimmed = raw.trim();
715 (!trimmed.is_empty()).then(|| trimmed.to_string())
716}
717
718fn edge_name_or_id(solid: &BrepSolid, edge_id: u64) -> String {
724 solid
725 .edges
726 .iter()
727 .find(|edge| edge.id == edge_id)
728 .and_then(|edge| edge.name.clone())
729 .filter(|name| !name.is_empty())
730 .unwrap_or_else(|| format!("E{edge_id}"))
731}
732
733fn sample_edge_midpoint(solid: &BrepSolid, edge_id: u64) -> Result<Vec3, String> {
736 let edge = solid
737 .edges
738 .iter()
739 .find(|edge| edge.id == edge_id)
740 .ok_or_else(|| format!("dressup: edge {edge_id} not found on target solid"))?;
741 let mid = (edge.t0 + edge.t1) * 0.5;
742 edge.curve
743 .evaluate(mid)
744 .map_err(|error| format!("dressup: edge {edge_id} midpoint sample failed: {error}"))
745}
746
747fn face_boundary_edges(solid: &BrepSolid, face_id: u64) -> Result<Vec<u64>, String> {
750 let face = solid
751 .shells
752 .iter()
753 .flat_map(|shell| &shell.faces)
754 .find(|face| face.id == face_id)
755 .ok_or_else(|| format!("dressup: face {face_id} not found on target solid"))?;
756 let mut ids = Vec::new();
757 let mut seen = FxHashSet::default();
758 for loop_record in &face.loops {
759 for coedge in &loop_record.coedges {
760 let degenerate = solid
761 .edges
762 .iter()
763 .find(|edge| edge.id == coedge.edge_id)
764 .map(|edge| edge.degenerate)
765 .unwrap_or(false);
766 if degenerate {
767 continue;
768 }
769 if seen.insert(coedge.edge_id) {
770 ids.push(coedge.edge_id);
771 }
772 }
773 }
774 Ok(ids)
775}
776
777pub fn resolve_blend_selection(ctx: &FeatureContext) -> Result<BlendSelection, String> {
782 let names = reference_names(ctx.param("edges"));
783
784 let mut unresolved = Vec::new();
785 let mut resolved: Vec<(u32, ResolvedRef)> = Vec::new();
786 for name in names {
787 let edges = ctx.scene.resolve_edge_group(&name);
793 let faces = ctx.scene.resolve_face_group(&name);
794 if edges.is_empty() && faces.is_empty() {
795 unresolved.push(name);
796 continue;
797 }
798 for edge in edges {
799 resolved.push((edge.handle, ResolvedRef::Edge(edge.edge_id)));
800 }
801 for face in faces {
802 resolved.push((face.handle, ResolvedRef::Face(face.face_id)));
803 }
804 }
805
806 let mut handles: Vec<u32> = resolved.iter().map(|(handle, _)| *handle).collect();
808 handles.sort_unstable();
809 handles.dedup();
810 if handles.len() > 1 {
811 return Ok(BlendSelection {
812 target: None,
813 multi_solid: true,
814 unresolved,
815 });
816 }
817 let Some(&handle) = handles.first() else {
818 return Ok(BlendSelection {
820 target: None,
821 multi_solid: false,
822 unresolved,
823 });
824 };
825
826 let name = ctx
830 .scene
831 .solids
832 .iter()
833 .find(|(_, ®istered)| registered == handle)
834 .map(|(name, _)| name.clone())
835 .ok_or_else(|| format!("dressup: target handle {handle} has no scene name"))?;
836
837 let (edge_points, edge_names) = crate::with_registered_solid_str(handle, |solid| {
842 let mut seen = FxHashSet::default();
843 let mut points = Vec::new();
844 let mut names = Vec::new();
845 for (_, entity) in &resolved {
846 match entity {
847 ResolvedRef::Edge(edge_id) => {
848 if seen.insert(*edge_id) {
849 points.push(sample_edge_midpoint(solid, *edge_id)?);
850 names.push(edge_name_or_id(solid, *edge_id));
851 }
852 }
853 ResolvedRef::Face(face_id) => {
854 for edge_id in face_boundary_edges(solid, *face_id)? {
855 if seen.insert(edge_id) {
856 points.push(sample_edge_midpoint(solid, edge_id)?);
857 names.push(edge_name_or_id(solid, edge_id));
858 }
859 }
860 }
861 }
862 }
863 Ok((points, names))
864 })?;
865
866 Ok(BlendSelection {
867 target: Some(BlendTarget {
868 handle,
869 name,
870 edge_points,
871 edge_names,
872 }),
873 multi_solid: false,
874 unresolved,
875 })
876}
877
878pub fn blend_face_base(id: &str) -> String {
880 format!("{}:BLEND", if id.is_empty() { "F" } else { id })
881}
882
883pub fn blend_face_name(id: &str, edge_name: &str) -> String {
885 format!("{}:{}", blend_face_base(id), edge_name)
886}
887
888pub fn require_blend_direction(ctx: &FeatureContext, operation: &str) -> Result<(), String> {
891 let direction = ctx
892 .param("direction")
893 .and_then(|value| value.as_str())
894 .map(|text| text.trim().to_uppercase())
895 .filter(|text| !text.is_empty())
896 .unwrap_or_else(|| "AUTO".to_string());
897 if direction != "AUTO" && direction != "INSET" {
898 return Err(format!(
899 "{operation} direction '{direction}' belonged to the removed legacy mesh pipeline (only AUTO/INSET exist)"
900 ));
901 }
902 Ok(())
903}
904
905pub(super) fn number_or_default(ctx: &FeatureContext, key: &str, default: f64) -> f64 {
908 match ctx.param(key) {
909 None | Some(serde_json::Value::Null) => default,
910 Some(_) => match ctx.number(key) {
911 Ok(value) if value.is_finite() => value,
912 _ => default,
913 },
914 }
915}
916
917pub fn optional_number(ctx: &FeatureContext, key: &str) -> Result<Option<f64>, String> {
922 if ctx.param(key).is_some() {
923 ctx.number(key).map(Some)
924 } else {
925 Ok(None)
926 }
927}
928
929pub fn compose_trs_matrix(
940 translate: [f64; 3],
941 rotate_rad: [f64; 3],
942 scale: [f64; 3],
943 pivot: [f64; 3],
944) -> [f64; 16] {
945 let (a, b) = (rotate_rad[0].cos(), rotate_rad[0].sin());
946 let (c, d) = (rotate_rad[1].cos(), rotate_rad[1].sin());
947 let (e, f) = (rotate_rad[2].cos(), rotate_rad[2].sin());
948 let (ae, af, be, bf) = (a * e, a * f, b * e, b * f);
949 let r = [
951 [c * e, -c * f, d],
952 [af + be * d, ae - bf * d, -b * c],
953 [bf - ae * d, be + af * d, a * c],
954 ];
955 let mut rs = [[0.0f64; 3]; 3];
957 for i in 0..3 {
958 for j in 0..3 {
959 rs[i][j] = r[i][j] * scale[j];
960 }
961 }
962 let rs_pivot = [
963 rs[0][0] * pivot[0] + rs[0][1] * pivot[1] + rs[0][2] * pivot[2],
964 rs[1][0] * pivot[0] + rs[1][1] * pivot[1] + rs[1][2] * pivot[2],
965 rs[2][0] * pivot[0] + rs[2][1] * pivot[1] + rs[2][2] * pivot[2],
966 ];
967 let tx = translate[0] + pivot[0] - rs_pivot[0];
968 let ty = translate[1] + pivot[1] - rs_pivot[1];
969 let tz = translate[2] + pivot[2] - rs_pivot[2];
970 [
971 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,
975 ]
976}
977
978pub fn vec3_from_value(
985 env: &crate::feature_pipeline::Env,
986 value: Option<&serde_json::Value>,
987 key: &str,
988 default: [f64; 3],
989) -> Result<[f64; 3], String> {
990 let Some(value) = value.filter(|value| !value.is_null()) else {
991 return Ok(default);
992 };
993 let component = |slot: Option<&serde_json::Value>, fallback: f64| -> Result<f64, String> {
994 match slot {
995 None | Some(serde_json::Value::Null) => Ok(fallback),
996 Some(serde_json::Value::Number(number)) => number
997 .as_f64()
998 .ok_or_else(|| format!("param `{key}` has a non-finite component")),
999 Some(serde_json::Value::String(source)) => {
1000 let number = env
1001 .eval(source)
1002 .map_err(|error| format!("param `{key}`: {error}"))?;
1003 if !number.is_finite() {
1008 return Err(format!(
1009 "param `{key}`: `{source}` evaluated to {number}"
1010 ));
1011 }
1012 Ok(number)
1013 }
1014 Some(other) => Err(format!(
1015 "param `{key}` component must be a number or expression, found {other}"
1016 )),
1017 }
1018 };
1019 if let Some(array) = value.as_array() {
1020 return Ok([
1021 component(array.first(), default[0])?,
1022 component(array.get(1), default[1])?,
1023 component(array.get(2), default[2])?,
1024 ]);
1025 }
1026 if value.is_object() {
1027 return Ok([
1028 component(value.get("x"), default[0])?,
1029 component(value.get("y"), default[1])?,
1030 component(value.get("z"), default[2])?,
1031 ]);
1032 }
1033 Err(format!("param `{key}` must be a vec3 array or object"))
1034}
1035
1036pub(super) fn resolve_solid_names(
1042 scene: &crate::feature_pipeline::SceneMap,
1043 names: &[String],
1044 unresolved: &mut Vec<String>,
1045) -> Vec<(String, u32)> {
1046 let mut resolved = Vec::new();
1047 for name in names {
1048 match scene.resolve_solid(name) {
1049 Some(handle) => resolved.push((name.clone(), handle)),
1050 None => unresolved.push(name.clone()),
1051 }
1052 }
1053 resolved
1054}
1055
1056pub(super) fn sheet_body_name(ctx: &FeatureContext) -> Option<String> {
1059 first_reference_name(ctx.param("sheet")).or_else(|| {
1060 let mut bodies = ctx.scene.solids.iter().filter_map(|(name, &handle)| {
1061 crate::feature_pipeline::sheet_metal::get_tree(handle).map(|_| name.clone())
1062 });
1063 match (bodies.next(), bodies.next()) {
1064 (Some(only), None) => Some(only),
1065 _ => None,
1066 }
1067 })
1068}
1069
1070pub(super) fn single_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1073 let name = match value {
1074 Some(serde_json::Value::String(text)) => Some(text.clone()),
1075 Some(serde_json::Value::Array(array)) => array.iter().find_map(|entry| {
1076 entry
1077 .as_str()
1078 .or_else(|| entry.get("name").and_then(|name| name.as_str()))
1079 .map(str::to_string)
1080 }),
1081 Some(object @ serde_json::Value::Object(_)) => {
1082 object.get("name").and_then(|name| name.as_str()).map(str::to_string)
1083 }
1084 _ => None,
1085 }?;
1086 let trimmed = name.trim();
1087 (!trimmed.is_empty()).then(|| trimmed.to_string())
1088}
1089
1090pub fn first_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1094 fn one(value: &serde_json::Value) -> Option<String> {
1095 match value {
1096 serde_json::Value::Array(items) => items.iter().find_map(one),
1097 other => reference_name(other),
1098 }
1099 }
1100 value.and_then(one)
1101}
1102
1103pub fn normalize_profile_alias(name: String) -> String {
1111 match name.strip_suffix(":FACE") {
1112 Some(base) => base.to_string(),
1113 None => name,
1114 }
1115}
1116
1117pub(crate) fn reference_name_array(value: Option<&serde_json::Value>) -> Vec<String> {
1120 value
1121 .and_then(serde_json::Value::as_array)
1122 .map(|items| items.iter().filter_map(reference_name).collect())
1123 .unwrap_or_default()
1124}
1125
1126pub fn reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1129 match value {
1130 Some(serde_json::Value::Array(items)) => items.iter().filter_map(reference_name).collect(),
1131 Some(other) => reference_name(other).into_iter().collect(),
1132 None => Vec::new(),
1133 }
1134}
1135
1136pub(super) fn unique_reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1138 let mut seen = std::collections::HashSet::new();
1139 reference_names(value)
1140 .into_iter()
1141 .filter(|name| seen.insert(name.clone()))
1142 .collect()
1143}
1144
1145pub fn consume_profile_sketch(ctx: &FeatureContext) -> bool {
1148 !matches!(
1149 ctx.param("consumeProfileSketch"),
1150 Some(serde_json::Value::Bool(false))
1151 )
1152}
1153
1154pub fn consume_sketch(ctx: &FeatureContext, reference_name: &str, result: &mut FeatureResult) {
1158 if !consume_profile_sketch(ctx) {
1159 return;
1160 }
1161 consume_sketch_always(reference_name, result);
1162}
1163
1164pub fn consume_sketch_always(reference_name: &str, result: &mut FeatureResult) {
1169 let base = reference_name
1170 .strip_suffix(":PROFILE")
1171 .unwrap_or(reference_name)
1172 .to_string();
1173 if !result.removed.contains(&base) {
1174 result.removed.push(base);
1175 }
1176}
1177
1178pub fn profile_loop_uv_curves(
1184 profile: &crate::feature_pipeline::SketchProfile,
1185 profile_loop: &crate::feature_pipeline::ProfileLoop,
1186) -> Result<Vec<NurbsCurve>, String> {
1187 let mut curves = Vec::with_capacity(profile_loop.curves.len());
1188 for curve in &profile_loop.curves {
1189 let mut control_points = Vec::with_capacity(curve.control_points.len());
1190 for cp in &curve.control_points {
1191 let delta = cp.point()?.sub(profile.origin);
1192 control_points.push(crate::Vec4::from_point(
1193 Vec3::new(delta.dot(profile.x_axis), delta.dot(profile.y_axis), 0.0),
1194 cp.w,
1195 ));
1196 }
1197 curves.push(NurbsCurve::new(
1198 curve.degree,
1199 curve.knots.clone(),
1200 control_points,
1201 )?);
1202 }
1203 Ok(curves)
1204}
1205
1206pub fn resolve_path(ctx: &FeatureContext, name: &str) -> Result<Vec<NurbsCurve>, String> {
1214 if let Some(chain) = ctx.scene.resolve_path(name) {
1215 return Ok(chain.clone());
1216 }
1217 if let Some(edge) = ctx.scene.resolve_edge(name) {
1218 return Ok(vec![edge_curve(edge)?]);
1219 }
1220 if let Some((owner, geometry)) = name.rsplit_once(':') {
1221 if geometry.len() > 1 && geometry.starts_with('G') {
1222 if let Some(chain) = ctx.scene.resolve_path(owner) {
1223 return Ok(chain.clone());
1224 }
1225 }
1226 }
1227 Err(format!(
1228 "path '{name}' not found (no sketch path or resident edge)"
1229 ))
1230}
1231
1232#[derive(Debug, Clone)]
1242pub struct PathSegment {
1243 pub name: String,
1244 pub curve: NurbsCurve,
1245 pub axis: Option<Vec3>,
1249}
1250
1251pub fn resolve_sweep_path(
1282 ctx: &FeatureContext,
1283 names: &[String],
1284) -> Result<crate::SweepPath, String> {
1285 let segments = resolve_path_chain(ctx, names)?;
1286 let mut curves = Vec::with_capacity(segments.len());
1287 let mut segment_names = Vec::with_capacity(segments.len());
1288 let mut axes = Vec::with_capacity(segments.len());
1289 for segment in segments {
1290 curves.push(segment.curve);
1291 segment_names.push(segment.name);
1292 axes.push(segment.axis);
1293 }
1294 crate::SweepPath::new(curves, segment_names)?.with_screw_axes(axes)
1295}
1296
1297pub fn resolve_path_chain(
1298 ctx: &FeatureContext,
1299 names: &[String],
1300) -> Result<Vec<PathSegment>, String> {
1301 let mut segments: Vec<PathSegment> = Vec::new();
1302 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1303 for reference in names {
1304 let curves = resolve_path(ctx, reference)?;
1305 let published = ctx.scene.resolve_path_segment_names(reference);
1306 let published_axes = ctx.scene.resolve_path_segment_axes(reference);
1307 let single = curves.len() == 1;
1308 for (index, curve) in curves.into_iter().enumerate() {
1309 let name = published
1310 .and_then(|names| names.get(index).cloned().flatten())
1311 .filter(|name| !name.trim().is_empty())
1312 .unwrap_or_else(|| {
1313 if single {
1314 reference.clone()
1315 } else {
1316 format!("{reference}[{index}]")
1317 }
1318 });
1319 let axis = published_axes
1320 .and_then(|axes| axes.get(index).copied().flatten())
1321 .map(|axis| axis.direction);
1322 if seen.insert(name.clone()) {
1323 segments.push(PathSegment { name, curve, axis });
1324 }
1325 }
1326 }
1327 if segments.is_empty() {
1328 return Err("path selection resolved to no curves".into());
1329 }
1330 chain_path_segments(segments)
1331}
1332
1333fn path_endpoints(curve: &NurbsCurve) -> Result<(Vec3, Vec3), String> {
1335 let [t0, t1] = curve.domain()?;
1336 Ok((curve.evaluate(t0)?, curve.evaluate(t1)?))
1337}
1338
1339fn chain_path_segments(segments: Vec<PathSegment>) -> Result<Vec<PathSegment>, String> {
1342 if segments.len() < 2 {
1343 return Ok(segments);
1344 }
1345 let ends: Vec<(Vec3, Vec3)> = segments
1346 .iter()
1347 .map(|segment| path_endpoints(&segment.curve))
1348 .collect::<Result<_, _>>()?;
1349 let scale = ends
1353 .iter()
1354 .flat_map(|(a, b)| [a, b])
1355 .map(|point| point.sub(ends[0].0).length())
1356 .fold(1.0_f64, f64::max);
1357 let tolerance = 1e-5 * scale;
1358 let joins = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
1359
1360 let mut used = vec![false; segments.len()];
1361 used[0] = true;
1362 let mut order: Vec<(usize, bool)> = vec![(0, false)];
1365 let mut head = ends[0].0;
1366 let mut tail = ends[0].1;
1367 loop {
1368 if order.len() > 1 && joins(tail, head) {
1371 break;
1372 }
1373 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, tail, tolerance) {
1374 used[index] = true;
1375 order.push((index, reversed));
1376 tail = far;
1377 continue;
1378 }
1379 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, head, tolerance) {
1380 used[index] = true;
1381 order.insert(0, (index, !reversed));
1383 head = far;
1384 continue;
1385 }
1386 break;
1387 }
1388 let stranded: Vec<&str> = segments
1389 .iter()
1390 .zip(&used)
1391 .filter(|(_, used)| !**used)
1392 .map(|(segment, _)| segment.name.as_str())
1393 .collect();
1394 if !stranded.is_empty() {
1395 return Err(format!(
1396 "the selected path edges do not form ONE connected chain — {} joins neither \
1397 end of the chain the other selections form (a sweep path must be a single \
1398 head-to-tail run; select connected edges, or one branch at a time)",
1399 stranded.join(", ")
1400 ));
1401 }
1402 let mut chained = Vec::with_capacity(segments.len());
1403 let mut taken: Vec<Option<PathSegment>> = segments.into_iter().map(Some).collect();
1404 for (index, reversed) in order {
1405 let mut segment = taken[index].take().expect("each segment placed once");
1406 if reversed {
1407 segment.curve = segment.curve.reversed()?;
1408 }
1409 chained.push(segment);
1410 }
1411 Ok(chained)
1412}
1413
1414fn attach_path_segment(
1417 ends: &[(Vec3, Vec3)],
1418 used: &[bool],
1419 cursor: Vec3,
1420 tolerance: f64,
1421) -> Option<(usize, bool, Vec3)> {
1422 for (index, (start, end)) in ends.iter().enumerate() {
1423 if used[index] {
1424 continue;
1425 }
1426 if start.sub(cursor).length() <= tolerance {
1427 return Some((index, false, *end));
1428 }
1429 if end.sub(cursor).length() <= tolerance {
1430 return Some((index, true, *start));
1431 }
1432 }
1433 None
1434}
1435
1436pub fn translate_curves(curves: &[NurbsCurve], offset: Vec3) -> Result<Vec<NurbsCurve>, String> {
1440 curves
1441 .iter()
1442 .map(|curve| translate_curve(curve, offset))
1443 .collect()
1444}
1445
1446pub fn sketch_plane_frame(ctx: &FeatureContext, names: &[String]) -> Option<Frame> {
1459 for name in names {
1460 if let Some(frame) = ctx.scene.resolve_frame(name) {
1461 return Some(frame);
1462 }
1463 if let Some((owner, _)) = name.rsplit_once(':') {
1464 if let Some(frame) = ctx.scene.resolve_frame(owner) {
1465 return Some(frame);
1466 }
1467 }
1468 }
1469 None
1470}
1471
1472pub fn sketch_base_name(ctx: &FeatureContext, name: &str) -> String {
1482 if let Some((owner, tail)) = name.rsplit_once(':') {
1483 if tail.len() > 1 && tail.starts_with('G') && ctx.scene.resolve_frame(owner).is_some() {
1484 return owner.to_string();
1485 }
1486 }
1487 name.to_string()
1488}
1489
1490pub fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
1495 let [start, end] = curve.domain()?;
1496 let epsilon = (1e-9 * (end - start)).max(2e-9);
1497 let mut result = curve.clone();
1498 if t0 > start + epsilon && t0 < end - epsilon {
1499 result = result.split(t0)?.1;
1500 }
1501 let domain = result.domain()?;
1502 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
1503 result = result.split(t1)?.0;
1504 }
1505 Ok(result)
1506}
1507
1508pub fn edge_curve(edge: EdgeRef) -> Result<NurbsCurve, String> {
1521 crate::with_registered_solid_str(edge.handle, |solid| {
1522 let record = solid
1523 .edges
1524 .iter()
1525 .find(|candidate| candidate.id == edge.edge_id)
1526 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1527 trimmed_curve(&record.curve, record.t0, record.t1)
1528 })
1529}
1530
1531pub fn edge_axis(edge: EdgeRef) -> Result<Axis, String> {
1535 crate::with_registered_solid_str(edge.handle, |solid| {
1536 let record = solid
1537 .edges
1538 .iter()
1539 .find(|candidate| candidate.id == edge.edge_id)
1540 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1541 let start = record.curve.evaluate(record.t0)?;
1542 let end = record.curve.evaluate(record.t1)?;
1543 let direction = end.sub(start).normalized()?;
1544 Ok(Axis {
1545 point: start,
1546 direction,
1547 })
1548 })
1549}
1550
1551pub fn translate_curve(curve: &NurbsCurve, offset: Vec3) -> Result<NurbsCurve, String> {
1566 let mut control_points = Vec::with_capacity(curve.control_points.len());
1567 for cp in &curve.control_points {
1568 let cartesian = cp.point()?;
1569 control_points.push(crate::Vec4::from_point(cartesian.add(offset), cp.w));
1570 }
1571 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
1572}
1573
1574pub struct HoleGroup {
1577 pub index: usize,
1579 pub depth: usize,
1581 pub children: Vec<HoleGroup>,
1582}
1583
1584pub fn hole_nesting(
1590 profile: &SketchProfile,
1591 region: &[crate::feature_pipeline::ProfileLoop],
1592) -> Result<Vec<HoleGroup>, String> {
1593 let count = region.len().saturating_sub(1);
1594 if count == 0 {
1595 return Ok(Vec::new());
1596 }
1597 let mut polygons: Vec<Vec<[f64; 2]>> = Vec::with_capacity(count);
1599 for hole in ®ion[1..] {
1600 let mut polygon = Vec::new();
1601 for curve in &hole.curves {
1602 let [t0, t1] = curve.domain()?;
1603 for step in 0..8 {
1604 let t = t0 + (t1 - t0) * (step as f64 / 8.0);
1605 let point = curve.evaluate(t)?;
1606 let delta = point.sub(profile.origin);
1607 polygon.push([delta.dot(profile.x_axis), delta.dot(profile.y_axis)]);
1608 }
1609 }
1610 polygons.push(polygon);
1611 }
1612 let mut contains = vec![vec![false; count]; count];
1614 for i in 0..count {
1615 let Some(representative) = polygons[i].first().copied() else {
1616 continue;
1617 };
1618 for j in 0..count {
1619 if i != j && point_in_polygon_uv(representative, &polygons[j]) {
1620 contains[i][j] = true;
1621 }
1622 }
1623 }
1624 let depths: Vec<usize> = (0..count)
1625 .map(|i| contains[i].iter().filter(|inside| **inside).count())
1626 .collect();
1627 Ok((0..count)
1628 .filter(|&i| depths[i] == 0)
1629 .map(|i| hole_group(i, &contains, &depths))
1630 .collect())
1631}
1632
1633fn hole_group(i: usize, contains: &[Vec<bool>], depths: &[usize]) -> HoleGroup {
1636 let children = (0..depths.len())
1637 .filter(|&k| contains[k][i] && depths[k] == depths[i] + 1)
1638 .map(|k| hole_group(k, contains, depths))
1639 .collect();
1640 HoleGroup {
1641 index: i + 1,
1642 depth: depths[i],
1643 children,
1644 }
1645}
1646
1647fn point_in_polygon_uv(point: [f64; 2], polygon: &[[f64; 2]]) -> bool {
1649 let count = polygon.len();
1650 if count < 3 {
1651 return false;
1652 }
1653 let mut inside = false;
1654 let mut j = count - 1;
1655 for i in 0..count {
1656 let pi = polygon[i];
1657 let pj = polygon[j];
1658 let intersects = (pi[1] > point[1]) != (pj[1] > point[1])
1659 && point[0] < (pj[0] - pi[0]) * (point[1] - pi[1]) / (pj[1] - pi[1]) + pi[0];
1660 if intersects {
1661 inside = !inside;
1662 }
1663 j = i;
1664 }
1665 inside
1666}
1667
1668pub fn name_hole_cutter_faces(cutter: &mut BrepSolid, id: &str, key: &str, segment: Option<&str>) {
1675 let name = hole_face_name(id, key, segment);
1676 for face in cutter
1677 .shells
1678 .iter_mut()
1679 .flat_map(|shell| shell.faces.iter_mut())
1680 {
1681 if face.name.is_none() {
1682 face.name = Some(name.clone());
1683 }
1684 }
1685}
1686
1687pub fn hole_face_name(id: &str, key: &str, segment: Option<&str>) -> String {
1692 match segment {
1693 Some(segment) => format!("{id}:HOLE:{key}:{segment}"),
1694 None => format!("{id}:HOLE:{key}"),
1695 }
1696}
1697
1698pub fn subtract_solid(body: BrepSolid, cutter: BrepSolid) -> Result<BrepSolid, String> {
1701 let options = BooleanOptions {
1702 merge_coplanar_faces: true,
1703 ..BooleanOptions::default()
1704 };
1705 let body_handle = crate::register_solid_value(body);
1706 let cutter_handle = crate::register_solid_value(cutter);
1707 let cut = crate::with_two_registered_solids(body_handle, cutter_handle, |body, tool| {
1708 crate::boolean_operation(body, tool, BooleanOperation::Subtract, &options)
1709 });
1710 crate::free_registered_solid(body_handle);
1711 crate::free_registered_solid(cutter_handle);
1712 cut.map_err(String::from)
1714}
1715
1716pub fn hole_prism(
1722 hole_curves: &[NurbsCurve],
1723 direction: Vec3,
1724 distance: f64,
1725 depth: usize,
1726) -> Result<BrepSolid, String> {
1727 let dir = direction.normalized()?;
1728 let span = distance.abs();
1729 let margin = span.max(1.0) * (depth as f64 + 1.0);
1730 let start_t = distance.min(0.0) - margin;
1731 let length = span + 2.0 * margin;
1732 let offset = dir.scale(start_t);
1733 let mut curves = Vec::with_capacity(hole_curves.len());
1734 for curve in hole_curves {
1735 curves.push(translate_curve(curve, offset)?);
1736 }
1737 crate::extrude_profile_brep(&curves, dir, length)
1738}
1739
1740pub fn subtract_hole_prism(
1745 feature: &str,
1746 body: BrepSolid,
1747 hole_curves: &[NurbsCurve],
1748 direction: Vec3,
1749 distance: f64,
1750 id: &str,
1751 key: &str,
1752) -> Result<BrepSolid, String> {
1753 if hole_curves.len() < 2 {
1754 return Ok(body);
1755 }
1756 let mut cutter = hole_prism(hole_curves, direction, distance, 0)?;
1757 name_hole_cutter_faces(&mut cutter, id, key, None);
1758 subtract_solid(body, cutter)
1759 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))
1760}
1761
1762pub fn subtract_region_holes(
1775 mut body: BrepSolid,
1776 profile: &SketchProfile,
1777 region: &[crate::feature_pipeline::ProfileLoop],
1778 feature: &str,
1779 id: &str,
1780 segment: Option<&str>,
1781 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, String>,
1782) -> Result<BrepSolid, String> {
1783 for group in hole_nesting(profile, region)? {
1784 let key = hole_key(®ion[group.index], group.index);
1785 let Some(cutter) = hole_cutter(region, &group, id, segment, build)
1786 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))?
1787 else {
1788 continue;
1789 };
1790 body = subtract_solid(body, cutter)
1791 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))?;
1792 }
1793 Ok(body)
1794}
1795
1796pub fn hole_key(hole: &crate::feature_pipeline::ProfileLoop, index: usize) -> String {
1800 hole.key().unwrap_or_else(|| index.to_string())
1801}
1802
1803fn hole_cutter(
1806 region: &[crate::feature_pipeline::ProfileLoop],
1807 group: &HoleGroup,
1808 id: &str,
1809 segment: Option<&str>,
1810 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, String>,
1811) -> Result<Option<BrepSolid>, String> {
1812 if region[group.index].curves.len() < 2 {
1813 return Ok(None);
1814 }
1815 let mut cutter = build(group.index, group.depth)?;
1816 name_hole_cutter_faces(
1817 &mut cutter,
1818 id,
1819 &hole_key(®ion[group.index], group.index),
1820 segment,
1821 );
1822 for child in &group.children {
1823 let Some(child_cutter) = hole_cutter(region, child, id, segment, build)? else {
1824 continue;
1825 };
1826 cutter = subtract_solid(cutter, child_cutter).map_err(|error| {
1827 format!("island {} carve from hole {} failed: {error}", child.index, group.index)
1828 })?;
1829 }
1830 Ok(Some(cutter))
1831}
1832
1833pub enum PlaneLikeRef {
1845 Face(FaceRef),
1847 Frame(Frame),
1849}
1850
1851impl PlaneLikeRef {
1852 pub fn point_normal(&self) -> Result<(Vec3, Vec3), String> {
1857 match self {
1858 PlaneLikeRef::Frame(frame) => Ok((frame.origin, frame.z_axis)),
1859 PlaneLikeRef::Face(face) => face_point_normal(*face),
1860 }
1861 }
1862
1863 pub fn frame(&self) -> Result<Frame, String> {
1866 match self {
1867 PlaneLikeRef::Frame(frame) => Ok(*frame),
1868 PlaneLikeRef::Face(face) => face_frame(*face),
1869 }
1870 }
1871}
1872
1873pub fn resolve_plane_reference(ctx: &FeatureContext, name: &str) -> Option<PlaneLikeRef> {
1878 if let Some(face) = ctx.scene.resolve_face(name) {
1879 return Some(PlaneLikeRef::Face(face));
1880 }
1881 ctx.scene.resolve_frame(name).map(PlaneLikeRef::Frame)
1882}
1883
1884pub fn face_point_normal(face: FaceRef) -> Result<(Vec3, Vec3), String> {
1889 crate::with_registered_solid_str(face.handle, |solid| {
1890 for shell in &solid.shells {
1891 for record in &shell.faces {
1892 if record.id != face.face_id {
1893 continue;
1894 }
1895 let [u0, u1] = record.surface.domain_u()?;
1896 let [v0, v1] = record.surface.domain_v()?;
1897 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
1898 let point = record.surface.evaluate(um, vm)?;
1899 let mut normal = record.surface.normal(um, vm)?;
1900 if !record.same_sense {
1901 normal = normal.scale(-1.0);
1902 }
1903 return Ok((point, normal));
1904 }
1905 }
1906 Err(format!("face {} not found on solid", face.face_id))
1907 })
1908}
1909
1910pub fn face_boundary_points(
1917 solid: &BrepSolid,
1918 record: &crate::FaceRecord,
1919) -> Result<Vec<Vec3>, String> {
1920 let mut points = Vec::new();
1921 for loop_record in &record.loops {
1922 for coedge in &loop_record.coedges {
1923 let Some(edge) = solid.edges.iter().find(|e| e.id == coedge.edge_id) else {
1924 continue;
1925 };
1926 if edge.degenerate {
1927 continue;
1928 }
1929 for step in 0..=4 {
1930 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
1931 points.push(edge.curve.evaluate(t)?);
1932 }
1933 }
1934 }
1935 Ok(points)
1936}
1937
1938pub fn bounds_of(points: &[Vec3]) -> Option<(Vec3, Vec3)> {
1940 let mut iter = points.iter();
1941 let first = *iter.next()?;
1942 let (mut min, mut max) = (first, first);
1943 for point in iter {
1944 min = Vec3::new(min.x.min(point.x), min.y.min(point.y), min.z.min(point.z));
1945 max = Vec3::new(max.x.max(point.x), max.y.max(point.y), max.z.max(point.z));
1946 }
1947 Some((min, max))
1948}
1949
1950pub fn face_frame(face: FaceRef) -> Result<Frame, String> {
1957 crate::with_registered_solid_str(face.handle, |solid| {
1958 let record = solid
1959 .shells
1960 .iter()
1961 .flat_map(|shell| &shell.faces)
1962 .find(|candidate| candidate.id == face.face_id)
1963 .ok_or_else(|| format!("face {} not found on solid", face.face_id))?;
1964
1965 let [u0, u1] = record.surface.domain_u()?;
1966 let [v0, v1] = record.surface.domain_v()?;
1967 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
1968
1969 let center_normal = record.surface.normal(um, vm)?;
1971 for (u, v) in [(u0, v0), (u1, v0), (u1, v1), (u0, v1)] {
1972 let normal = record.surface.normal(u, v)?;
1973 if normal.dot(center_normal).abs() < 1.0 - 1e-6 {
1974 return Err(
1975 "sketch/plane on a non-planar face is not yet migrated to the Rust pipeline"
1976 .into(),
1977 );
1978 }
1979 }
1980 let normal = if record.same_sense {
1982 center_normal
1983 } else {
1984 center_normal.scale(-1.0)
1985 };
1986
1987 let boundary = face_boundary_points(solid, record)?;
1989 let plane_point = record.surface.evaluate(um, vm)?;
1990 let center = match bounds_of(&boundary) {
1991 Some((min, max)) => min.add(max).scale(0.5),
1992 None => plane_point,
1993 };
1994 let unit = normal.normalized()?;
1996 let signed = center.sub(plane_point).dot(unit);
1997 let origin = center.sub(unit.scale(signed));
1998 Frame::from_origin_normal(origin, normal)
1999 })
2000}
2001
2002pub fn union_region_solids(solids: Vec<BrepSolid>) -> Result<BrepSolid, String> {
2012 union_solids_keeping(solids, &[])
2013}
2014
2015pub fn union_solids_keeping(
2029 solids: Vec<BrepSolid>,
2030 keep_unmerged: &[String],
2031) -> Result<BrepSolid, String> {
2032 let mut iter = solids.into_iter();
2033 let mut current = iter
2034 .next()
2035 .ok_or("profile produced no region solids to union")?;
2036 let options = BooleanOptions {
2037 merge_coplanar_faces: true,
2038 keep_unmerged_name_substrs: keep_unmerged.to_vec(),
2039 ..BooleanOptions::default()
2040 };
2041 for next in iter {
2042 let a = crate::register_solid_value(current);
2043 let b = crate::register_solid_value(next);
2044 let unioned = crate::with_two_registered_solids(a, b, |left, right| {
2045 crate::boolean_operation(left, right, BooleanOperation::Union, &options)
2046 });
2047 crate::free_registered_solid(a);
2048 crate::free_registered_solid(b);
2049 current = unioned.map_err(|error| format!("region union failed: {error}"))?;
2050 }
2051 Ok(current)
2052}
2053
2054
2055pub(super) fn primitive_transform_schema() -> serde_json::Value {
2057 serde_json::json!({
2058 "type": "transform",
2059 "default_value": {
2060 "position": [
2061 0,
2062 0,
2063 0
2064 ],
2065 "rotationEuler": [
2066 0,
2067 0,
2068 0
2069 ],
2070 "scale": [
2071 1,
2072 1,
2073 1
2074 ]
2075 },
2076 "referenceSelectionFilter": [
2077 "FACE",
2078 "EDGE",
2079 "VERTEX",
2080 "PLANE",
2081 "DATUM"
2082 ],
2083 "referenceLabel": "Start Reference",
2084 "referencePlaceholder": "Select point, edge, or face…",
2085 "hint": "Select a start reference, then position, rotate, and scale the solid relative to it."
2086 })
2087}
2088
2089pub(super) fn optional_boolean_schema() -> serde_json::Value {
2091 serde_json::json!({
2092 "type": "boolean_operation",
2093 "default_value": {
2094 "targets": [],
2095 "operation": "NONE",
2096 "mergeCoplanarFaces": true
2097 },
2098 "hint": "Optional boolean operation with selected solids"
2099 })
2100}
2101
2102