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 pub edge_ids: Vec<u64>,
691 pub picked_edge_ids: Vec<u64>,
694}
695
696pub struct BlendSelection {
698 pub target: Option<BlendTarget>,
700 pub multi_solid: bool,
701 pub unresolved: Vec<String>,
703}
704
705enum ResolvedRef {
707 Edge(u64),
708 Face(u64),
709}
710
711pub(crate) fn reference_name(value: &serde_json::Value) -> Option<String> {
715 let raw = match value {
716 serde_json::Value::String(text) => Some(text.as_str()),
717 serde_json::Value::Object(map) => map.get("name").and_then(|value| value.as_str()),
718 _ => None,
719 }?;
720 let trimmed = raw.trim();
721 (!trimmed.is_empty()).then(|| trimmed.to_string())
722}
723
724fn edge_name_or_id(solid: &BrepSolid, edge_id: u64) -> String {
730 solid
731 .edges
732 .iter()
733 .find(|edge| edge.id == edge_id)
734 .and_then(|edge| edge.name.clone())
735 .filter(|name| !name.is_empty())
736 .unwrap_or_else(|| format!("E{edge_id}"))
737}
738
739fn sample_edge_midpoint(solid: &BrepSolid, edge_id: u64) -> Result<Vec3, String> {
742 let edge = solid
743 .edges
744 .iter()
745 .find(|edge| edge.id == edge_id)
746 .ok_or_else(|| format!("dressup: edge {edge_id} not found on target solid"))?;
747 let mid = (edge.t0 + edge.t1) * 0.5;
748 edge.curve
749 .evaluate(mid)
750 .map_err(|error| format!("dressup: edge {edge_id} midpoint sample failed: {error}"))
751}
752
753fn face_boundary_edges(solid: &BrepSolid, face_id: u64) -> Result<Vec<u64>, String> {
761 let face = solid
762 .shells
763 .iter()
764 .flat_map(|shell| &shell.faces)
765 .find(|face| face.id == face_id)
766 .ok_or_else(|| format!("dressup: face {face_id} not found on target solid"))?;
767 let ruled_revolution = matches!(
768 face.surface.analytic(),
769 Some(crate::AnalyticSurface::RuledRevolution { .. })
770 );
771 let mut seams = FxHashSet::default();
772 if ruled_revolution {
773 let mut uses = FxHashSet::default();
774 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
775 if !uses.insert(coedge.edge_id) {
776 seams.insert(coedge.edge_id);
777 }
778 }
779 }
780 let mut ids = Vec::new();
781 let mut seen = FxHashSet::default();
782 for loop_record in &face.loops {
783 for coedge in &loop_record.coedges {
784 if seams.contains(&coedge.edge_id) {
785 continue;
786 }
787 let degenerate = solid
788 .edges
789 .iter()
790 .find(|edge| edge.id == coedge.edge_id)
791 .map(|edge| edge.degenerate)
792 .unwrap_or(false);
793 if degenerate {
794 continue;
795 }
796 if seen.insert(coedge.edge_id) {
797 ids.push(coedge.edge_id);
798 }
799 }
800 }
801 Ok(ids)
802}
803
804pub fn resolve_blend_selection(ctx: &FeatureContext) -> Result<BlendSelection, String> {
809 let names = reference_names(ctx.param("edges"));
810
811 let mut unresolved = Vec::new();
812 let mut resolved: Vec<(u32, ResolvedRef)> = Vec::new();
813 for name in names {
814 let edges = ctx.scene.resolve_edge_group(&name);
820 let faces = ctx.scene.resolve_face_group(&name);
821 if edges.is_empty() && faces.is_empty() {
822 unresolved.push(name);
823 continue;
824 }
825 for edge in edges {
826 resolved.push((edge.handle, ResolvedRef::Edge(edge.edge_id)));
827 }
828 for face in faces {
829 resolved.push((face.handle, ResolvedRef::Face(face.face_id)));
830 }
831 }
832
833 let mut handles: Vec<u32> = resolved.iter().map(|(handle, _)| *handle).collect();
835 handles.sort_unstable();
836 handles.dedup();
837 if handles.len() > 1 {
838 return Ok(BlendSelection {
839 target: None,
840 multi_solid: true,
841 unresolved,
842 });
843 }
844 let Some(&handle) = handles.first() else {
845 return Ok(BlendSelection {
847 target: None,
848 multi_solid: false,
849 unresolved,
850 });
851 };
852
853 let name = ctx
857 .scene
858 .solids
859 .iter()
860 .find(|(_, ®istered)| registered == handle)
861 .map(|(name, _)| name.clone())
862 .ok_or_else(|| format!("dressup: target handle {handle} has no scene name"))?;
863
864 let (edge_points, edge_names, edge_ids, picked_edge_ids) = crate::with_registered_solid_str(handle, |solid| {
869 let mut seen = FxHashSet::default();
870 let mut picked = Vec::new();
871 let mut points = Vec::new();
872 let mut names = Vec::new();
873 let mut ids = Vec::new();
874 for (_, entity) in &resolved {
875 match entity {
876 ResolvedRef::Edge(edge_id) => {
877 if !picked.contains(edge_id) {
878 picked.push(*edge_id);
879 }
880 if seen.insert(*edge_id) {
881 points.push(sample_edge_midpoint(solid, *edge_id)?);
882 names.push(edge_name_or_id(solid, *edge_id));
883 ids.push(*edge_id);
884 }
885 }
886 ResolvedRef::Face(face_id) => {
887 for edge_id in face_boundary_edges(solid, *face_id)? {
888 if seen.insert(edge_id) {
889 points.push(sample_edge_midpoint(solid, edge_id)?);
890 names.push(edge_name_or_id(solid, edge_id));
891 ids.push(edge_id);
892 }
893 }
894 }
895 }
896 }
897 Ok((points, names, ids, picked))
898 })?;
899
900 Ok(BlendSelection {
901 target: Some(BlendTarget {
902 handle,
903 name,
904 edge_points,
905 edge_names,
906 edge_ids,
907 picked_edge_ids,
908 }),
909 multi_solid: false,
910 unresolved,
911 })
912}
913
914pub fn open_tangent_chain_extension(solid: &BrepSolid, selected: &[u64]) -> Result<Vec<(u64, Vec3, String)>, String> {
937 walk_tangent_chains(solid, selected, selected, false)
938}
939
940pub fn tangent_chain_expansion(
956 solid: &BrepSolid,
957 seeds: &[u64],
958 selected: &[u64],
959) -> Result<Vec<(u64, Vec3, String)>, String> {
960 walk_tangent_chains(solid, seeds, selected, true)
961}
962
963pub(super) fn single_tangent_continuation(candidates: &[(u64, Vec3)], onward: Vec3) -> Option<u64> {
968 let mut tangent = candidates.iter().filter(|(_, direction)| direction.dot(onward) >= 1.0 - 1e-6);
969 let first = tangent.next()?;
970 tangent.next().is_none().then_some(first.0)
971}
972
973fn walk_tangent_chains(
974 solid: &BrepSolid,
975 seeds: &[u64],
976 selected: &[u64],
977 follow: bool,
978) -> Result<Vec<(u64, Vec3, String)>, String> {
979 let tangent_at = |edge: &crate::topology::EdgeRecord, vertex: u64| -> Result<Vec3, String> {
980 let at_start = edge.start_vertex_id == vertex;
982 let t = if at_start { edge.t0 } else { edge.t1 };
983 let tangent = edge.curve.unit_tangent(t, edge.t0, edge.t1)?;
984 Ok(if at_start { tangent } else { tangent.scale(-1.0) })
985 };
986 let carriers = |edge_id: u64| -> Vec<&crate::NurbsSurface> {
987 solid
988 .shells
989 .iter()
990 .flat_map(|shell| &shell.faces)
991 .filter(|face| face.loops.iter().flat_map(|lp| &lp.coedges).any(|coedge| coedge.edge_id == edge_id))
992 .map(|face| &face.surface)
993 .collect()
994 };
995 let changes_carrier = |from: u64, to: u64| -> bool {
996 let own = carriers(from);
997 !carriers(to)
998 .iter()
999 .all(|next| own.iter().any(|surface| crate::face_merge::same_surface(surface, next, 1e-9)))
1000 };
1001 let seam = |edge_id: u64| -> bool {
1004 solid.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
1005 face.loops.iter().flat_map(|lp| &lp.coedges).filter(|coedge| coedge.edge_id == edge_id).count() == 2
1006 })
1007 };
1008 let mere_split = |vertex: u64| -> bool {
1009 solid
1010 .edges
1011 .iter()
1012 .filter(|edge| !edge.degenerate && (edge.start_vertex_id == vertex || edge.end_vertex_id == vertex) && !seam(edge.id))
1013 .count()
1014 == 2
1015 };
1016 let mut taken: FxHashSet<u64> = selected.iter().chain(seeds).copied().collect();
1017 let mut added = Vec::new();
1018 for &seed in seeds {
1019 let Some(seed_edge) = solid.edges.iter().find(|edge| edge.id == seed) else { continue };
1020 if seed_edge.start_vertex_id == seed_edge.end_vertex_id || seed_edge.degenerate {
1021 continue;
1022 }
1023 for start in [seed_edge.start_vertex_id, seed_edge.end_vertex_id] {
1024 let mut current = seed_edge;
1025 let mut vertex = start;
1026 for _ in 0..if follow { solid.edges.len().max(128) } else { 128 } {
1027 let onward = tangent_at(current, vertex)?.scale(-1.0);
1030 let mut candidates = Vec::new();
1031 for candidate in &solid.edges {
1032 if candidate.id == current.id
1033 || candidate.degenerate
1034 || candidate.start_vertex_id == candidate.end_vertex_id
1035 || (candidate.start_vertex_id != vertex && candidate.end_vertex_id != vertex)
1036 {
1037 continue;
1038 }
1039 candidates.push((candidate.id, tangent_at(candidate, vertex)?));
1040 }
1041 let Some(next_id) = single_tangent_continuation(&candidates, onward) else { break };
1042 let Some(next) = solid.edges.iter().find(|edge| edge.id == next_id) else { break };
1043 if !follow && current.id == seed && !changes_carrier(seed, next.id) && !mere_split(vertex) {
1044 break;
1045 }
1046 if !taken.insert(next.id) {
1047 break;
1048 }
1049 added.push((next.id, sample_edge_midpoint(solid, next.id)?, edge_name_or_id(solid, next.id)));
1050 vertex = if next.start_vertex_id == vertex { next.end_vertex_id } else { next.start_vertex_id };
1051 current = next;
1052 }
1053 }
1054 }
1055 Ok(added)
1056}
1057
1058pub fn blend_face_base(id: &str) -> String {
1060 format!("{}:BLEND", if id.is_empty() { "F" } else { id })
1061}
1062
1063pub fn blend_face_name(id: &str, edge_name: &str) -> String {
1065 format!("{}:{}", blend_face_base(id), edge_name)
1066}
1067
1068pub fn require_blend_direction(ctx: &FeatureContext, operation: &str) -> Result<(), String> {
1071 let direction = ctx
1072 .param("direction")
1073 .and_then(|value| value.as_str())
1074 .map(|text| text.trim().to_uppercase())
1075 .filter(|text| !text.is_empty())
1076 .unwrap_or_else(|| "AUTO".to_string());
1077 if direction != "AUTO" && direction != "INSET" {
1078 return Err(format!(
1079 "{operation} direction '{direction}' belonged to the removed legacy mesh pipeline (only AUTO/INSET exist)"
1080 ));
1081 }
1082 Ok(())
1083}
1084
1085pub(super) fn number_or_default(ctx: &FeatureContext, key: &str, default: f64) -> f64 {
1088 match ctx.param(key) {
1089 None | Some(serde_json::Value::Null) => default,
1090 Some(_) => match ctx.number(key) {
1091 Ok(value) if value.is_finite() => value,
1092 _ => default,
1093 },
1094 }
1095}
1096
1097pub fn optional_number(ctx: &FeatureContext, key: &str) -> Result<Option<f64>, String> {
1102 if ctx.param(key).is_some() {
1103 ctx.number(key).map(Some)
1104 } else {
1105 Ok(None)
1106 }
1107}
1108
1109pub fn compose_trs_matrix(
1120 translate: [f64; 3],
1121 rotate_rad: [f64; 3],
1122 scale: [f64; 3],
1123 pivot: [f64; 3],
1124) -> [f64; 16] {
1125 let (a, b) = (rotate_rad[0].cos(), rotate_rad[0].sin());
1126 let (c, d) = (rotate_rad[1].cos(), rotate_rad[1].sin());
1127 let (e, f) = (rotate_rad[2].cos(), rotate_rad[2].sin());
1128 let (ae, af, be, bf) = (a * e, a * f, b * e, b * f);
1129 let r = [
1131 [c * e, -c * f, d],
1132 [af + be * d, ae - bf * d, -b * c],
1133 [bf - ae * d, be + af * d, a * c],
1134 ];
1135 let mut rs = [[0.0f64; 3]; 3];
1137 for i in 0..3 {
1138 for j in 0..3 {
1139 rs[i][j] = r[i][j] * scale[j];
1140 }
1141 }
1142 let rs_pivot = [
1143 rs[0][0] * pivot[0] + rs[0][1] * pivot[1] + rs[0][2] * pivot[2],
1144 rs[1][0] * pivot[0] + rs[1][1] * pivot[1] + rs[1][2] * pivot[2],
1145 rs[2][0] * pivot[0] + rs[2][1] * pivot[1] + rs[2][2] * pivot[2],
1146 ];
1147 let tx = translate[0] + pivot[0] - rs_pivot[0];
1148 let ty = translate[1] + pivot[1] - rs_pivot[1];
1149 let tz = translate[2] + pivot[2] - rs_pivot[2];
1150 [
1151 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,
1155 ]
1156}
1157
1158pub fn vec3_from_value(
1165 env: &crate::feature_pipeline::Env,
1166 value: Option<&serde_json::Value>,
1167 key: &str,
1168 default: [f64; 3],
1169) -> Result<[f64; 3], String> {
1170 let Some(value) = value.filter(|value| !value.is_null()) else {
1171 return Ok(default);
1172 };
1173 let component = |slot: Option<&serde_json::Value>, fallback: f64| -> Result<f64, String> {
1174 match slot {
1175 None | Some(serde_json::Value::Null) => Ok(fallback),
1176 Some(serde_json::Value::Number(number)) => number
1177 .as_f64()
1178 .ok_or_else(|| format!("param `{key}` has a non-finite component")),
1179 Some(serde_json::Value::String(source)) => {
1180 let number = env
1181 .eval(source)
1182 .map_err(|error| format!("param `{key}`: {error}"))?;
1183 if !number.is_finite() {
1188 return Err(format!(
1189 "param `{key}`: `{source}` evaluated to {number}"
1190 ));
1191 }
1192 Ok(number)
1193 }
1194 Some(other) => Err(format!(
1195 "param `{key}` component must be a number or expression, found {other}"
1196 )),
1197 }
1198 };
1199 if let Some(array) = value.as_array() {
1200 return Ok([
1201 component(array.first(), default[0])?,
1202 component(array.get(1), default[1])?,
1203 component(array.get(2), default[2])?,
1204 ]);
1205 }
1206 if value.is_object() {
1207 return Ok([
1208 component(value.get("x"), default[0])?,
1209 component(value.get("y"), default[1])?,
1210 component(value.get("z"), default[2])?,
1211 ]);
1212 }
1213 Err(format!("param `{key}` must be a vec3 array or object"))
1214}
1215
1216pub(super) fn resolve_solid_names(
1222 scene: &crate::feature_pipeline::SceneMap,
1223 names: &[String],
1224 unresolved: &mut Vec<String>,
1225) -> Vec<(String, u32)> {
1226 let mut resolved = Vec::new();
1227 for name in names {
1228 match scene.resolve_solid(name) {
1229 Some(handle) => resolved.push((name.clone(), handle)),
1230 None => unresolved.push(name.clone()),
1231 }
1232 }
1233 resolved
1234}
1235
1236pub(super) fn sheet_body_name(ctx: &FeatureContext) -> Option<String> {
1239 first_reference_name(ctx.param("sheet")).or_else(|| {
1240 let mut bodies = ctx.scene.solids.iter().filter_map(|(name, &handle)| {
1241 crate::feature_pipeline::sheet_metal::get_tree(handle).map(|_| name.clone())
1242 });
1243 match (bodies.next(), bodies.next()) {
1244 (Some(only), None) => Some(only),
1245 _ => None,
1246 }
1247 })
1248}
1249
1250pub(super) fn single_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1253 let name = match value {
1254 Some(serde_json::Value::String(text)) => Some(text.clone()),
1255 Some(serde_json::Value::Array(array)) => array.iter().find_map(|entry| {
1256 entry
1257 .as_str()
1258 .or_else(|| entry.get("name").and_then(|name| name.as_str()))
1259 .map(str::to_string)
1260 }),
1261 Some(object @ serde_json::Value::Object(_)) => {
1262 object.get("name").and_then(|name| name.as_str()).map(str::to_string)
1263 }
1264 _ => None,
1265 }?;
1266 let trimmed = name.trim();
1267 (!trimmed.is_empty()).then(|| trimmed.to_string())
1268}
1269
1270pub fn first_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1274 fn one(value: &serde_json::Value) -> Option<String> {
1275 match value {
1276 serde_json::Value::Array(items) => items.iter().find_map(one),
1277 other => reference_name(other),
1278 }
1279 }
1280 value.and_then(one)
1281}
1282
1283pub fn normalize_profile_alias(name: String) -> String {
1291 match crate::feature_pipeline::sketch_alias_base(&name) {
1292 Some(base) => base.to_string(),
1293 None => name,
1294 }
1295}
1296
1297pub(crate) fn reference_name_array(value: Option<&serde_json::Value>) -> Vec<String> {
1300 value
1301 .and_then(serde_json::Value::as_array)
1302 .map(|items| items.iter().filter_map(reference_name).collect())
1303 .unwrap_or_default()
1304}
1305
1306pub fn reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1309 match value {
1310 Some(serde_json::Value::Array(items)) => items.iter().filter_map(reference_name).collect(),
1311 Some(other) => reference_name(other).into_iter().collect(),
1312 None => Vec::new(),
1313 }
1314}
1315
1316pub(super) fn unique_reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1318 let mut seen = std::collections::HashSet::new();
1319 reference_names(value)
1320 .into_iter()
1321 .filter(|name| seen.insert(name.clone()))
1322 .collect()
1323}
1324
1325pub fn consume_profile_sketch(ctx: &FeatureContext) -> bool {
1328 !matches!(
1329 ctx.param("consumeProfileSketch"),
1330 Some(serde_json::Value::Bool(false))
1331 )
1332}
1333
1334pub fn consume_sketch(ctx: &FeatureContext, reference_name: &str, result: &mut FeatureResult) {
1338 if !consume_profile_sketch(ctx) {
1339 return;
1340 }
1341 consume_sketch_always(reference_name, result);
1342}
1343
1344pub fn consume_sketch_always(reference_name: &str, result: &mut FeatureResult) {
1349 let base = crate::feature_pipeline::canonical_sketch_name(reference_name).to_string();
1350 if !result.removed.contains(&base) {
1351 result.removed.push(base);
1352 }
1353}
1354
1355pub fn profile_loop_uv_curves(
1361 profile: &crate::feature_pipeline::SketchProfile,
1362 profile_loop: &crate::feature_pipeline::ProfileLoop,
1363) -> Result<Vec<NurbsCurve>, String> {
1364 let mut curves = Vec::with_capacity(profile_loop.curves.len());
1365 for curve in &profile_loop.curves {
1366 let mut control_points = Vec::with_capacity(curve.control_points.len());
1367 for cp in &curve.control_points {
1368 let delta = cp.point()?.sub(profile.origin);
1369 control_points.push(crate::Vec4::from_point(
1370 Vec3::new(delta.dot(profile.x_axis), delta.dot(profile.y_axis), 0.0),
1371 cp.w,
1372 ));
1373 }
1374 curves.push(NurbsCurve::new(
1375 curve.degree,
1376 curve.knots.clone(),
1377 control_points,
1378 )?);
1379 }
1380 Ok(curves)
1381}
1382
1383pub fn resolve_path(ctx: &FeatureContext, name: &str) -> Result<Vec<NurbsCurve>, String> {
1391 if let Some(chain) = ctx.scene.resolve_path(name) {
1392 return Ok(chain.clone());
1393 }
1394 if let Some(edge) = ctx.scene.resolve_edge(name) {
1395 return Ok(vec![edge_curve(edge)?]);
1396 }
1397 if let Some((owner, geometry)) = name.rsplit_once(':') {
1398 if geometry.len() > 1 && geometry.starts_with('G') {
1399 if let Some(chain) = ctx.scene.resolve_path(owner) {
1400 return Ok(chain.clone());
1401 }
1402 }
1403 }
1404 Err(format!(
1405 "path '{name}' not found (no sketch path or resident edge)"
1406 ))
1407}
1408
1409#[derive(Debug, Clone)]
1419pub struct PathSegment {
1420 pub name: String,
1421 pub curve: NurbsCurve,
1422 pub axis: Option<Vec3>,
1426}
1427
1428pub fn resolve_sweep_path(
1459 ctx: &FeatureContext,
1460 names: &[String],
1461) -> Result<crate::SweepPath, String> {
1462 let segments = resolve_path_chain(ctx, names)?;
1463 let mut curves = Vec::with_capacity(segments.len());
1464 let mut segment_names = Vec::with_capacity(segments.len());
1465 let mut axes = Vec::with_capacity(segments.len());
1466 for segment in segments {
1467 curves.push(segment.curve);
1468 segment_names.push(segment.name);
1469 axes.push(segment.axis);
1470 }
1471 crate::SweepPath::new(curves, segment_names)?
1472 .with_screw_axes(axes)
1473 .map_err(String::from)
1474}
1475
1476pub fn resolve_path_chain(
1477 ctx: &FeatureContext,
1478 names: &[String],
1479) -> Result<Vec<PathSegment>, String> {
1480 let mut segments: Vec<PathSegment> = Vec::new();
1481 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1482 for reference in names {
1483 let curves = resolve_path(ctx, reference)?;
1484 let published = ctx.scene.resolve_path_segment_names(reference);
1485 let published_axes = ctx.scene.resolve_path_segment_axes(reference);
1486 let single = curves.len() == 1;
1487 for (index, curve) in curves.into_iter().enumerate() {
1488 let name = published
1489 .and_then(|names| names.get(index).cloned().flatten())
1490 .filter(|name| !name.trim().is_empty())
1491 .unwrap_or_else(|| {
1492 if single {
1493 reference.clone()
1494 } else {
1495 format!("{reference}[{index}]")
1496 }
1497 });
1498 let axis = published_axes
1499 .and_then(|axes| axes.get(index).copied().flatten())
1500 .map(|axis| axis.direction);
1501 if seen.insert(name.clone()) {
1502 segments.push(PathSegment { name, curve, axis });
1503 }
1504 }
1505 }
1506 if segments.is_empty() {
1507 return Err("path selection resolved to no curves".into());
1508 }
1509 chain_path_segments(segments)
1510}
1511
1512fn path_endpoints(curve: &NurbsCurve) -> Result<(Vec3, Vec3), String> {
1514 let [t0, t1] = curve.domain()?;
1515 Ok((curve.evaluate(t0)?, curve.evaluate(t1)?))
1516}
1517
1518fn chain_path_segments(segments: Vec<PathSegment>) -> Result<Vec<PathSegment>, String> {
1521 if segments.len() < 2 {
1522 return Ok(segments);
1523 }
1524 let ends: Vec<(Vec3, Vec3)> = segments
1525 .iter()
1526 .map(|segment| path_endpoints(&segment.curve))
1527 .collect::<Result<_, _>>()?;
1528 let scale = ends
1532 .iter()
1533 .flat_map(|(a, b)| [a, b])
1534 .map(|point| point.sub(ends[0].0).length())
1535 .fold(1.0_f64, f64::max);
1536 let tolerance = 1e-5 * scale;
1537 let joins = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
1538
1539 let mut used = vec![false; segments.len()];
1540 used[0] = true;
1541 let mut order: Vec<(usize, bool)> = vec![(0, false)];
1544 let mut head = ends[0].0;
1545 let mut tail = ends[0].1;
1546 loop {
1547 if order.len() > 1 && joins(tail, head) {
1550 break;
1551 }
1552 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, tail, tolerance) {
1553 used[index] = true;
1554 order.push((index, reversed));
1555 tail = far;
1556 continue;
1557 }
1558 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, head, tolerance) {
1559 used[index] = true;
1560 order.insert(0, (index, !reversed));
1562 head = far;
1563 continue;
1564 }
1565 break;
1566 }
1567 let stranded: Vec<&str> = segments
1568 .iter()
1569 .zip(&used)
1570 .filter(|(_, used)| !**used)
1571 .map(|(segment, _)| segment.name.as_str())
1572 .collect();
1573 if !stranded.is_empty() {
1574 return Err(format!(
1575 "the selected path edges do not form ONE connected chain — {} joins neither \
1576 end of the chain the other selections form (a sweep path must be a single \
1577 head-to-tail run; select connected edges, or one branch at a time)",
1578 stranded.join(", ")
1579 ));
1580 }
1581 let mut chained = Vec::with_capacity(segments.len());
1582 let mut taken: Vec<Option<PathSegment>> = segments.into_iter().map(Some).collect();
1583 for (index, reversed) in order {
1584 let mut segment = taken[index].take().expect("each segment placed once");
1585 if reversed {
1586 segment.curve = segment.curve.reversed()?;
1587 }
1588 chained.push(segment);
1589 }
1590 Ok(chained)
1591}
1592
1593fn attach_path_segment(
1596 ends: &[(Vec3, Vec3)],
1597 used: &[bool],
1598 cursor: Vec3,
1599 tolerance: f64,
1600) -> Option<(usize, bool, Vec3)> {
1601 for (index, (start, end)) in ends.iter().enumerate() {
1602 if used[index] {
1603 continue;
1604 }
1605 if start.sub(cursor).length() <= tolerance {
1606 return Some((index, false, *end));
1607 }
1608 if end.sub(cursor).length() <= tolerance {
1609 return Some((index, true, *start));
1610 }
1611 }
1612 None
1613}
1614
1615pub fn translate_curves(curves: &[NurbsCurve], offset: Vec3) -> Result<Vec<NurbsCurve>, String> {
1619 curves
1620 .iter()
1621 .map(|curve| translate_curve(curve, offset))
1622 .collect()
1623}
1624
1625pub fn sketch_plane_frame(ctx: &FeatureContext, names: &[String]) -> Option<Frame> {
1638 for name in names {
1639 if let Some(frame) = ctx.scene.resolve_frame(name) {
1640 return Some(frame);
1641 }
1642 if let Some((owner, _)) = name.rsplit_once(':') {
1643 if let Some(frame) = ctx.scene.resolve_frame(owner) {
1644 return Some(frame);
1645 }
1646 }
1647 }
1648 None
1649}
1650
1651pub fn sketch_base_name(ctx: &FeatureContext, name: &str) -> String {
1661 if let Some((owner, tail)) = name.rsplit_once(':') {
1662 if tail.len() > 1 && tail.starts_with('G') && ctx.scene.resolve_frame(owner).is_some() {
1663 return owner.to_string();
1664 }
1665 }
1666 name.to_string()
1667}
1668
1669pub fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
1674 let [start, end] = curve.domain()?;
1675 let epsilon = (1e-9 * (end - start)).max(2e-9);
1676 let mut result = curve.clone();
1677 if t0 > start + epsilon && t0 < end - epsilon {
1678 result = result.split(t0)?.1;
1679 }
1680 let domain = result.domain()?;
1681 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
1682 result = result.split(t1)?.0;
1683 }
1684 Ok(result)
1685}
1686
1687pub fn edge_curve(edge: EdgeRef) -> Result<NurbsCurve, String> {
1700 crate::with_registered_solid_str(edge.handle, |solid| {
1701 let record = solid
1702 .edges
1703 .iter()
1704 .find(|candidate| candidate.id == edge.edge_id)
1705 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1706 trimmed_curve(&record.curve, record.t0, record.t1)
1707 })
1708}
1709
1710pub fn edge_axis(edge: EdgeRef) -> Result<Axis, String> {
1714 crate::with_registered_solid_str(edge.handle, |solid| {
1715 let record = solid
1716 .edges
1717 .iter()
1718 .find(|candidate| candidate.id == edge.edge_id)
1719 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1720 let start = record.curve.evaluate(record.t0)?;
1721 let end = record.curve.evaluate(record.t1)?;
1722 let direction = end.sub(start).normalized()?;
1723 Ok(Axis {
1724 point: start,
1725 direction,
1726 })
1727 })
1728}
1729
1730pub fn translate_curve(curve: &NurbsCurve, offset: Vec3) -> Result<NurbsCurve, String> {
1745 let mut control_points = Vec::with_capacity(curve.control_points.len());
1746 for cp in &curve.control_points {
1747 let cartesian = cp.point()?;
1748 control_points.push(crate::Vec4::from_point(cartesian.add(offset), cp.w));
1749 }
1750 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
1751}
1752
1753pub struct HoleGroup {
1756 pub index: usize,
1758 pub depth: usize,
1760 pub children: Vec<HoleGroup>,
1761}
1762
1763pub fn hole_nesting(
1769 profile: &SketchProfile,
1770 region: &[crate::feature_pipeline::ProfileLoop],
1771) -> Result<Vec<HoleGroup>, String> {
1772 let count = region.len().saturating_sub(1);
1773 if count == 0 {
1774 return Ok(Vec::new());
1775 }
1776 let mut polygons: Vec<Vec<[f64; 2]>> = Vec::with_capacity(count);
1778 for hole in ®ion[1..] {
1779 let mut polygon = Vec::new();
1780 for curve in &hole.curves {
1781 let [t0, t1] = curve.domain()?;
1782 for step in 0..8 {
1783 let t = t0 + (t1 - t0) * (step as f64 / 8.0);
1784 let point = curve.evaluate(t)?;
1785 let delta = point.sub(profile.origin);
1786 polygon.push([delta.dot(profile.x_axis), delta.dot(profile.y_axis)]);
1787 }
1788 }
1789 polygons.push(polygon);
1790 }
1791 let mut contains = vec![vec![false; count]; count];
1793 for i in 0..count {
1794 let Some(representative) = polygons[i].first().copied() else {
1795 continue;
1796 };
1797 for j in 0..count {
1798 if i != j && point_in_polygon_uv(representative, &polygons[j]) {
1799 contains[i][j] = true;
1800 }
1801 }
1802 }
1803 let depths: Vec<usize> = (0..count)
1804 .map(|i| contains[i].iter().filter(|inside| **inside).count())
1805 .collect();
1806 Ok((0..count)
1807 .filter(|&i| depths[i] == 0)
1808 .map(|i| hole_group(i, &contains, &depths))
1809 .collect())
1810}
1811
1812fn hole_group(i: usize, contains: &[Vec<bool>], depths: &[usize]) -> HoleGroup {
1815 let children = (0..depths.len())
1816 .filter(|&k| contains[k][i] && depths[k] == depths[i] + 1)
1817 .map(|k| hole_group(k, contains, depths))
1818 .collect();
1819 HoleGroup {
1820 index: i + 1,
1821 depth: depths[i],
1822 children,
1823 }
1824}
1825
1826fn point_in_polygon_uv(point: [f64; 2], polygon: &[[f64; 2]]) -> bool {
1828 polygon.len() >= 3 && crate::polygon::parity_inside(point, polygon.len(), |i| polygon[i])
1829}
1830
1831pub fn name_hole_cutter_faces(cutter: &mut BrepSolid, id: &str, key: &str, segment: Option<&str>) {
1838 let name = hole_face_name(id, key, segment);
1839 for face in cutter
1840 .shells
1841 .iter_mut()
1842 .flat_map(|shell| shell.faces.iter_mut())
1843 {
1844 if face.name.is_none() {
1845 face.name = Some(name.clone());
1846 }
1847 }
1848}
1849
1850pub fn hole_face_name(id: &str, key: &str, segment: Option<&str>) -> String {
1855 match segment {
1856 Some(segment) => format!("{id}:HOLE:{key}:{segment}"),
1857 None => format!("{id}:HOLE:{key}"),
1858 }
1859}
1860
1861pub fn subtract_solid(body: BrepSolid, cutter: BrepSolid) -> Result<BrepSolid, KernelRefusal> {
1864 let options = BooleanOptions {
1865 merge_coplanar_faces: true,
1866 ..BooleanOptions::default()
1867 };
1868 let body_handle = crate::register_solid_value(body);
1869 let cutter_handle = crate::register_solid_value(cutter);
1870 let cut = crate::with_two_registered_solids(body_handle, cutter_handle, |body, tool| {
1871 crate::boolean_operation(body, tool, BooleanOperation::Subtract, &options)
1872 });
1873 crate::free_registered_solid(body_handle);
1874 crate::free_registered_solid(cutter_handle);
1875 cut
1876}
1877
1878pub fn intersect_solid(body: BrepSolid, tool: BrepSolid) -> Result<BrepSolid, KernelRefusal> {
1881 let options = BooleanOptions {
1882 merge_coplanar_faces: true,
1883 ..BooleanOptions::default()
1884 };
1885 let body_handle = crate::register_solid_value(body);
1886 let tool_handle = crate::register_solid_value(tool);
1887 let common = crate::with_two_registered_solids(body_handle, tool_handle, |body, tool| {
1888 crate::boolean_operation(body, tool, BooleanOperation::Intersect, &options)
1889 });
1890 crate::free_registered_solid(body_handle);
1891 crate::free_registered_solid(tool_handle);
1892 common
1893}
1894
1895pub fn hole_prism(
1901 hole_curves: &[NurbsCurve],
1902 direction: Vec3,
1903 distance: f64,
1904 depth: usize,
1905) -> Result<BrepSolid, KernelRefusal> {
1906 let dir = direction.normalized().or_input(KernelStage::Collect, "direction")?;
1907 let span = distance.abs();
1908 let margin = span.max(1.0) * (depth as f64 + 1.0);
1909 let start_t = distance.min(0.0) - margin;
1910 let length = span + 2.0 * margin;
1911 let offset = dir.scale(start_t);
1912 let mut curves = Vec::with_capacity(hole_curves.len());
1913 for curve in hole_curves {
1914 curves.push(translate_curve(curve, offset).or_refuse(KernelStage::Collect, "translate_curve")?);
1915 }
1916 crate::extrude_profile_brep(&curves, dir, length)
1917}
1918
1919pub fn subtract_hole_prism(
1924 feature: &str,
1925 body: BrepSolid,
1926 hole_curves: &[NurbsCurve],
1927 direction: Vec3,
1928 distance: f64,
1929 id: &str,
1930 key: &str,
1931) -> Result<BrepSolid, String> {
1932 if hole_curves.len() < 2 {
1933 return Ok(body);
1934 }
1935 let mut cutter = hole_prism(hole_curves, direction, distance, 0)?;
1936 name_hole_cutter_faces(&mut cutter, id, key, None);
1937 subtract_solid(body, cutter)
1938 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))
1939}
1940
1941pub fn subtract_region_holes(
1954 mut body: BrepSolid,
1955 profile: &SketchProfile,
1956 region: &[crate::feature_pipeline::ProfileLoop],
1957 feature: &str,
1958 id: &str,
1959 segment: Option<&str>,
1960 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, KernelRefusal>,
1961) -> Result<BrepSolid, FeatureRefusal> {
1962 for group in hole_nesting(profile, region)? {
1963 let key = hole_key(®ion[group.index], group.index);
1964 let Some(cutter) = hole_cutter(region, &group, id, segment, build).map_err(|error| {
1967 error.with_message(|error| format!("{feature}: hole {key} cut failed: {error}"))
1968 })?
1969 else {
1970 continue;
1971 };
1972 body = subtract_solid(body, cutter).map_err(|error| {
1973 error.with_message(|error| format!("{feature}: hole {key} cut failed: {error}"))
1974 })?;
1975 }
1976 Ok(body)
1977}
1978
1979pub fn hole_key(hole: &crate::feature_pipeline::ProfileLoop, index: usize) -> String {
1983 hole.key().unwrap_or_else(|| index.to_string())
1984}
1985
1986fn hole_cutter(
1989 region: &[crate::feature_pipeline::ProfileLoop],
1990 group: &HoleGroup,
1991 id: &str,
1992 segment: Option<&str>,
1993 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, KernelRefusal>,
1994) -> Result<Option<BrepSolid>, KernelRefusal> {
1995 if region[group.index].curves.len() < 2 {
1996 return Ok(None);
1997 }
1998 let mut cutter = build(group.index, group.depth)?;
1999 name_hole_cutter_faces(
2000 &mut cutter,
2001 id,
2002 &hole_key(®ion[group.index], group.index),
2003 segment,
2004 );
2005 for child in &group.children {
2006 let Some(child_cutter) = hole_cutter(region, child, id, segment, build)? else {
2007 continue;
2008 };
2009 cutter = subtract_solid(cutter, child_cutter).map_err(|error| {
2010 error.with_message(|error| {
2011 format!("island {} carve from hole {} failed: {error}", child.index, group.index)
2012 })
2013 })?;
2014 }
2015 Ok(Some(cutter))
2016}
2017
2018pub enum PlaneLikeRef {
2030 Face(FaceRef),
2032 Frame(Frame),
2034}
2035
2036impl PlaneLikeRef {
2037 pub fn point_normal(&self) -> Result<(Vec3, Vec3), String> {
2042 match self {
2043 PlaneLikeRef::Frame(frame) => Ok((frame.origin, frame.z_axis)),
2044 PlaneLikeRef::Face(face) => face_point_normal(*face),
2045 }
2046 }
2047
2048 pub fn frame(&self) -> Result<Frame, String> {
2051 match self {
2052 PlaneLikeRef::Frame(frame) => Ok(*frame),
2053 PlaneLikeRef::Face(face) => face_frame(*face),
2054 }
2055 }
2056}
2057
2058pub fn resolve_plane_reference(ctx: &FeatureContext, name: &str) -> Option<PlaneLikeRef> {
2063 if let Some(face) = ctx.scene.resolve_face(name) {
2064 return Some(PlaneLikeRef::Face(face));
2065 }
2066 ctx.scene.resolve_frame(name).map(PlaneLikeRef::Frame)
2067}
2068
2069pub fn face_point_normal(face: FaceRef) -> Result<(Vec3, Vec3), String> {
2074 crate::with_registered_solid_str(face.handle, |solid| {
2075 for shell in &solid.shells {
2076 for record in &shell.faces {
2077 if record.id != face.face_id {
2078 continue;
2079 }
2080 let [u0, u1] = record.surface.domain_u()?;
2081 let [v0, v1] = record.surface.domain_v()?;
2082 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
2083 let point = record.surface.evaluate(um, vm)?;
2084 let mut normal = record.surface.normal(um, vm)?;
2085 if !record.same_sense {
2086 normal = normal.scale(-1.0);
2087 }
2088 return Ok((point, normal));
2089 }
2090 }
2091 Err(format!("face {} not found on solid", face.face_id))
2092 })
2093}
2094
2095pub fn face_boundary_points(
2102 solid: &BrepSolid,
2103 record: &crate::FaceRecord,
2104) -> Result<Vec<Vec3>, String> {
2105 let mut points = Vec::new();
2106 for loop_record in &record.loops {
2107 for coedge in &loop_record.coedges {
2108 let Some(edge) = solid.edges.iter().find(|e| e.id == coedge.edge_id) else {
2109 continue;
2110 };
2111 if edge.degenerate {
2112 continue;
2113 }
2114 for step in 0..=4 {
2115 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
2116 points.push(edge.curve.evaluate(t)?);
2117 }
2118 }
2119 }
2120 Ok(points)
2121}
2122
2123pub fn bounds_of(points: &[Vec3]) -> Option<(Vec3, Vec3)> {
2125 let mut iter = points.iter();
2126 let first = *iter.next()?;
2127 let (mut min, mut max) = (first, first);
2128 for point in iter {
2129 min = Vec3::new(min.x.min(point.x), min.y.min(point.y), min.z.min(point.z));
2130 max = Vec3::new(max.x.max(point.x), max.y.max(point.y), max.z.max(point.z));
2131 }
2132 Some((min, max))
2133}
2134
2135pub fn face_frame(face: FaceRef) -> Result<Frame, String> {
2142 crate::with_registered_solid_str(face.handle, |solid| {
2143 let record = solid
2144 .shells
2145 .iter()
2146 .flat_map(|shell| &shell.faces)
2147 .find(|candidate| candidate.id == face.face_id)
2148 .ok_or_else(|| format!("face {} not found on solid", face.face_id))?;
2149
2150 let [u0, u1] = record.surface.domain_u()?;
2151 let [v0, v1] = record.surface.domain_v()?;
2152 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2153
2154 let center_normal = record.surface.normal(um, vm)?;
2156 for (u, v) in [(u0, v0), (u1, v0), (u1, v1), (u0, v1)] {
2157 let normal = record.surface.normal(u, v)?;
2158 if normal.dot(center_normal).abs() < 1.0 - 1e-6 {
2159 return Err(
2160 "sketch/plane on a non-planar face is not yet migrated to the Rust pipeline"
2161 .into(),
2162 );
2163 }
2164 }
2165 let normal = if record.same_sense {
2167 center_normal
2168 } else {
2169 center_normal.scale(-1.0)
2170 };
2171
2172 let boundary = face_boundary_points(solid, record)?;
2174 let plane_point = record.surface.evaluate(um, vm)?;
2175 let center = match bounds_of(&boundary) {
2176 Some((min, max)) => min.add(max).scale(0.5),
2177 None => plane_point,
2178 };
2179 let unit = normal.normalized()?;
2181 let signed = center.sub(plane_point).dot(unit);
2182 let origin = center.sub(unit.scale(signed));
2183 Frame::from_origin_normal(origin, normal)
2184 })
2185}
2186
2187pub fn union_region_solids(solids: Vec<BrepSolid>) -> Result<BrepSolid, String> {
2197 union_solids_keeping(solids, &[])
2198}
2199
2200pub fn union_solids_keeping(
2214 solids: Vec<BrepSolid>,
2215 keep_unmerged: &[String],
2216) -> Result<BrepSolid, String> {
2217 let mut iter = solids.into_iter();
2218 let mut current = iter
2219 .next()
2220 .ok_or("profile produced no region solids to union")?;
2221 let options = BooleanOptions {
2222 merge_coplanar_faces: true,
2223 keep_unmerged_name_substrs: keep_unmerged.to_vec(),
2224 ..BooleanOptions::default()
2225 };
2226 for next in iter {
2227 let a = crate::register_solid_value(current);
2228 let b = crate::register_solid_value(next);
2229 let unioned = crate::with_two_registered_solids(a, b, |left, right| {
2230 crate::boolean_operation(left, right, BooleanOperation::Union, &options)
2231 });
2232 crate::free_registered_solid(a);
2233 crate::free_registered_solid(b);
2234 current = unioned.map_err(|error| format!("region union failed: {error}"))?;
2235 }
2236 Ok(current)
2237}
2238
2239
2240pub(super) fn primitive_transform_schema() -> serde_json::Value {
2242 serde_json::json!({
2243 "type": "transform",
2244 "default_value": {
2245 "position": [
2246 0,
2247 0,
2248 0
2249 ],
2250 "rotationEuler": [
2251 0,
2252 0,
2253 0
2254 ],
2255 "scale": [
2256 1,
2257 1,
2258 1
2259 ]
2260 },
2261 "referenceSelectionFilter": [
2262 "FACE",
2263 "EDGE",
2264 "VERTEX",
2265 "PLANE",
2266 "DATUM"
2267 ],
2268 "referenceLabel": "Start Reference",
2269 "referencePlaceholder": "Select point, edge, or face…",
2270 "hint": "Select a start reference, then position, rotate, and scale the solid relative to it."
2271 })
2272}
2273
2274pub(super) fn optional_boolean_schema() -> serde_json::Value {
2276 serde_json::json!({
2277 "type": "boolean_operation",
2278 "default_value": {
2279 "targets": [],
2280 "operation": "NONE",
2281 "mergeCoplanarFaces": true
2282 },
2283 "hint": "Optional boolean operation with selected solids"
2284 })
2285}
2286
2287
2288