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