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}
692
693pub struct BlendSelection {
695 pub target: Option<BlendTarget>,
697 pub multi_solid: bool,
698 pub unresolved: Vec<String>,
700}
701
702enum ResolvedRef {
704 Edge(u64),
705 Face(u64),
706}
707
708pub(crate) fn reference_name(value: &serde_json::Value) -> Option<String> {
712 let raw = match value {
713 serde_json::Value::String(text) => Some(text.as_str()),
714 serde_json::Value::Object(map) => map.get("name").and_then(|value| value.as_str()),
715 _ => None,
716 }?;
717 let trimmed = raw.trim();
718 (!trimmed.is_empty()).then(|| trimmed.to_string())
719}
720
721fn edge_name_or_id(solid: &BrepSolid, edge_id: u64) -> String {
727 solid
728 .edges
729 .iter()
730 .find(|edge| edge.id == edge_id)
731 .and_then(|edge| edge.name.clone())
732 .filter(|name| !name.is_empty())
733 .unwrap_or_else(|| format!("E{edge_id}"))
734}
735
736fn sample_edge_midpoint(solid: &BrepSolid, edge_id: u64) -> Result<Vec3, String> {
739 let edge = solid
740 .edges
741 .iter()
742 .find(|edge| edge.id == edge_id)
743 .ok_or_else(|| format!("dressup: edge {edge_id} not found on target solid"))?;
744 let mid = (edge.t0 + edge.t1) * 0.5;
745 edge.curve
746 .evaluate(mid)
747 .map_err(|error| format!("dressup: edge {edge_id} midpoint sample failed: {error}"))
748}
749
750fn face_boundary_edges(solid: &BrepSolid, face_id: u64) -> Result<Vec<u64>, String> {
753 let face = solid
754 .shells
755 .iter()
756 .flat_map(|shell| &shell.faces)
757 .find(|face| face.id == face_id)
758 .ok_or_else(|| format!("dressup: face {face_id} not found on target solid"))?;
759 let mut ids = Vec::new();
760 let mut seen = FxHashSet::default();
761 for loop_record in &face.loops {
762 for coedge in &loop_record.coedges {
763 let degenerate = solid
764 .edges
765 .iter()
766 .find(|edge| edge.id == coedge.edge_id)
767 .map(|edge| edge.degenerate)
768 .unwrap_or(false);
769 if degenerate {
770 continue;
771 }
772 if seen.insert(coedge.edge_id) {
773 ids.push(coedge.edge_id);
774 }
775 }
776 }
777 Ok(ids)
778}
779
780pub fn resolve_blend_selection(ctx: &FeatureContext) -> Result<BlendSelection, String> {
785 let names = reference_names(ctx.param("edges"));
786
787 let mut unresolved = Vec::new();
788 let mut resolved: Vec<(u32, ResolvedRef)> = Vec::new();
789 for name in names {
790 let edges = ctx.scene.resolve_edge_group(&name);
796 let faces = ctx.scene.resolve_face_group(&name);
797 if edges.is_empty() && faces.is_empty() {
798 unresolved.push(name);
799 continue;
800 }
801 for edge in edges {
802 resolved.push((edge.handle, ResolvedRef::Edge(edge.edge_id)));
803 }
804 for face in faces {
805 resolved.push((face.handle, ResolvedRef::Face(face.face_id)));
806 }
807 }
808
809 let mut handles: Vec<u32> = resolved.iter().map(|(handle, _)| *handle).collect();
811 handles.sort_unstable();
812 handles.dedup();
813 if handles.len() > 1 {
814 return Ok(BlendSelection {
815 target: None,
816 multi_solid: true,
817 unresolved,
818 });
819 }
820 let Some(&handle) = handles.first() else {
821 return Ok(BlendSelection {
823 target: None,
824 multi_solid: false,
825 unresolved,
826 });
827 };
828
829 let name = ctx
833 .scene
834 .solids
835 .iter()
836 .find(|(_, ®istered)| registered == handle)
837 .map(|(name, _)| name.clone())
838 .ok_or_else(|| format!("dressup: target handle {handle} has no scene name"))?;
839
840 let (edge_points, edge_names, edge_ids) = crate::with_registered_solid_str(handle, |solid| {
845 let mut seen = FxHashSet::default();
846 let mut points = Vec::new();
847 let mut names = Vec::new();
848 let mut ids = Vec::new();
849 for (_, entity) in &resolved {
850 match entity {
851 ResolvedRef::Edge(edge_id) => {
852 if seen.insert(*edge_id) {
853 points.push(sample_edge_midpoint(solid, *edge_id)?);
854 names.push(edge_name_or_id(solid, *edge_id));
855 ids.push(*edge_id);
856 }
857 }
858 ResolvedRef::Face(face_id) => {
859 for edge_id in face_boundary_edges(solid, *face_id)? {
860 if seen.insert(edge_id) {
861 points.push(sample_edge_midpoint(solid, edge_id)?);
862 names.push(edge_name_or_id(solid, edge_id));
863 ids.push(edge_id);
864 }
865 }
866 }
867 }
868 }
869 Ok((points, names, ids))
870 })?;
871
872 Ok(BlendSelection {
873 target: Some(BlendTarget {
874 handle,
875 name,
876 edge_points,
877 edge_names,
878 edge_ids,
879 }),
880 multi_solid: false,
881 unresolved,
882 })
883}
884
885pub fn open_tangent_chain_extension(solid: &BrepSolid, selected: &[u64]) -> Result<Vec<(u64, Vec3, String)>, String> {
903 let tangent_at = |edge: &crate::topology::EdgeRecord, vertex: u64| -> Result<Vec3, String> {
904 let at_start = edge.start_vertex_id == vertex;
906 let t = if at_start { edge.t0 } else { edge.t1 };
907 let tangent = edge.curve.unit_tangent(t, edge.t0, edge.t1)?;
908 Ok(if at_start { tangent } else { tangent.scale(-1.0) })
909 };
910 let carriers = |edge_id: u64| -> Vec<&crate::NurbsSurface> {
911 solid
912 .shells
913 .iter()
914 .flat_map(|shell| &shell.faces)
915 .filter(|face| face.loops.iter().flat_map(|lp| &lp.coedges).any(|coedge| coedge.edge_id == edge_id))
916 .map(|face| &face.surface)
917 .collect()
918 };
919 let changes_carrier = |from: u64, to: u64| -> bool {
920 let own = carriers(from);
921 !carriers(to)
922 .iter()
923 .all(|next| own.iter().any(|surface| crate::face_merge::same_surface(surface, next, 1e-9)))
924 };
925 let mut taken: FxHashSet<u64> = selected.iter().copied().collect();
926 let mut added = Vec::new();
927 for &seed in selected {
928 let Some(seed_edge) = solid.edges.iter().find(|edge| edge.id == seed) else { continue };
929 if seed_edge.start_vertex_id == seed_edge.end_vertex_id || seed_edge.degenerate {
930 continue;
931 }
932 for start in [seed_edge.start_vertex_id, seed_edge.end_vertex_id] {
933 let mut current = seed_edge;
934 let mut vertex = start;
935 for _ in 0..128 {
936 let onward = tangent_at(current, vertex)?.scale(-1.0);
939 let mut next = None;
940 let mut count = 0;
941 for candidate in &solid.edges {
942 if candidate.id == current.id
943 || candidate.degenerate
944 || candidate.start_vertex_id == candidate.end_vertex_id
945 || (candidate.start_vertex_id != vertex && candidate.end_vertex_id != vertex)
946 {
947 continue;
948 }
949 if tangent_at(candidate, vertex)?.dot(onward) >= 1.0 - 1e-6 {
950 count += 1;
951 next = Some(candidate);
952 }
953 }
954 let Some(next) = next.filter(|_| count == 1) else { break };
955 if current.id == seed && !changes_carrier(seed, next.id) {
956 break;
957 }
958 if !taken.insert(next.id) {
959 break;
960 }
961 added.push((next.id, sample_edge_midpoint(solid, next.id)?, edge_name_or_id(solid, next.id)));
962 vertex = if next.start_vertex_id == vertex { next.end_vertex_id } else { next.start_vertex_id };
963 current = next;
964 }
965 }
966 }
967 Ok(added)
968}
969
970pub fn blend_face_base(id: &str) -> String {
972 format!("{}:BLEND", if id.is_empty() { "F" } else { id })
973}
974
975pub fn blend_face_name(id: &str, edge_name: &str) -> String {
977 format!("{}:{}", blend_face_base(id), edge_name)
978}
979
980pub fn require_blend_direction(ctx: &FeatureContext, operation: &str) -> Result<(), String> {
983 let direction = ctx
984 .param("direction")
985 .and_then(|value| value.as_str())
986 .map(|text| text.trim().to_uppercase())
987 .filter(|text| !text.is_empty())
988 .unwrap_or_else(|| "AUTO".to_string());
989 if direction != "AUTO" && direction != "INSET" {
990 return Err(format!(
991 "{operation} direction '{direction}' belonged to the removed legacy mesh pipeline (only AUTO/INSET exist)"
992 ));
993 }
994 Ok(())
995}
996
997pub(super) fn number_or_default(ctx: &FeatureContext, key: &str, default: f64) -> f64 {
1000 match ctx.param(key) {
1001 None | Some(serde_json::Value::Null) => default,
1002 Some(_) => match ctx.number(key) {
1003 Ok(value) if value.is_finite() => value,
1004 _ => default,
1005 },
1006 }
1007}
1008
1009pub fn optional_number(ctx: &FeatureContext, key: &str) -> Result<Option<f64>, String> {
1014 if ctx.param(key).is_some() {
1015 ctx.number(key).map(Some)
1016 } else {
1017 Ok(None)
1018 }
1019}
1020
1021pub fn compose_trs_matrix(
1032 translate: [f64; 3],
1033 rotate_rad: [f64; 3],
1034 scale: [f64; 3],
1035 pivot: [f64; 3],
1036) -> [f64; 16] {
1037 let (a, b) = (rotate_rad[0].cos(), rotate_rad[0].sin());
1038 let (c, d) = (rotate_rad[1].cos(), rotate_rad[1].sin());
1039 let (e, f) = (rotate_rad[2].cos(), rotate_rad[2].sin());
1040 let (ae, af, be, bf) = (a * e, a * f, b * e, b * f);
1041 let r = [
1043 [c * e, -c * f, d],
1044 [af + be * d, ae - bf * d, -b * c],
1045 [bf - ae * d, be + af * d, a * c],
1046 ];
1047 let mut rs = [[0.0f64; 3]; 3];
1049 for i in 0..3 {
1050 for j in 0..3 {
1051 rs[i][j] = r[i][j] * scale[j];
1052 }
1053 }
1054 let rs_pivot = [
1055 rs[0][0] * pivot[0] + rs[0][1] * pivot[1] + rs[0][2] * pivot[2],
1056 rs[1][0] * pivot[0] + rs[1][1] * pivot[1] + rs[1][2] * pivot[2],
1057 rs[2][0] * pivot[0] + rs[2][1] * pivot[1] + rs[2][2] * pivot[2],
1058 ];
1059 let tx = translate[0] + pivot[0] - rs_pivot[0];
1060 let ty = translate[1] + pivot[1] - rs_pivot[1];
1061 let tz = translate[2] + pivot[2] - rs_pivot[2];
1062 [
1063 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,
1067 ]
1068}
1069
1070pub fn vec3_from_value(
1077 env: &crate::feature_pipeline::Env,
1078 value: Option<&serde_json::Value>,
1079 key: &str,
1080 default: [f64; 3],
1081) -> Result<[f64; 3], String> {
1082 let Some(value) = value.filter(|value| !value.is_null()) else {
1083 return Ok(default);
1084 };
1085 let component = |slot: Option<&serde_json::Value>, fallback: f64| -> Result<f64, String> {
1086 match slot {
1087 None | Some(serde_json::Value::Null) => Ok(fallback),
1088 Some(serde_json::Value::Number(number)) => number
1089 .as_f64()
1090 .ok_or_else(|| format!("param `{key}` has a non-finite component")),
1091 Some(serde_json::Value::String(source)) => {
1092 let number = env
1093 .eval(source)
1094 .map_err(|error| format!("param `{key}`: {error}"))?;
1095 if !number.is_finite() {
1100 return Err(format!(
1101 "param `{key}`: `{source}` evaluated to {number}"
1102 ));
1103 }
1104 Ok(number)
1105 }
1106 Some(other) => Err(format!(
1107 "param `{key}` component must be a number or expression, found {other}"
1108 )),
1109 }
1110 };
1111 if let Some(array) = value.as_array() {
1112 return Ok([
1113 component(array.first(), default[0])?,
1114 component(array.get(1), default[1])?,
1115 component(array.get(2), default[2])?,
1116 ]);
1117 }
1118 if value.is_object() {
1119 return Ok([
1120 component(value.get("x"), default[0])?,
1121 component(value.get("y"), default[1])?,
1122 component(value.get("z"), default[2])?,
1123 ]);
1124 }
1125 Err(format!("param `{key}` must be a vec3 array or object"))
1126}
1127
1128pub(super) fn resolve_solid_names(
1134 scene: &crate::feature_pipeline::SceneMap,
1135 names: &[String],
1136 unresolved: &mut Vec<String>,
1137) -> Vec<(String, u32)> {
1138 let mut resolved = Vec::new();
1139 for name in names {
1140 match scene.resolve_solid(name) {
1141 Some(handle) => resolved.push((name.clone(), handle)),
1142 None => unresolved.push(name.clone()),
1143 }
1144 }
1145 resolved
1146}
1147
1148pub(super) fn sheet_body_name(ctx: &FeatureContext) -> Option<String> {
1151 first_reference_name(ctx.param("sheet")).or_else(|| {
1152 let mut bodies = ctx.scene.solids.iter().filter_map(|(name, &handle)| {
1153 crate::feature_pipeline::sheet_metal::get_tree(handle).map(|_| name.clone())
1154 });
1155 match (bodies.next(), bodies.next()) {
1156 (Some(only), None) => Some(only),
1157 _ => None,
1158 }
1159 })
1160}
1161
1162pub(super) fn single_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1165 let name = match value {
1166 Some(serde_json::Value::String(text)) => Some(text.clone()),
1167 Some(serde_json::Value::Array(array)) => array.iter().find_map(|entry| {
1168 entry
1169 .as_str()
1170 .or_else(|| entry.get("name").and_then(|name| name.as_str()))
1171 .map(str::to_string)
1172 }),
1173 Some(object @ serde_json::Value::Object(_)) => {
1174 object.get("name").and_then(|name| name.as_str()).map(str::to_string)
1175 }
1176 _ => None,
1177 }?;
1178 let trimmed = name.trim();
1179 (!trimmed.is_empty()).then(|| trimmed.to_string())
1180}
1181
1182pub fn first_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1186 fn one(value: &serde_json::Value) -> Option<String> {
1187 match value {
1188 serde_json::Value::Array(items) => items.iter().find_map(one),
1189 other => reference_name(other),
1190 }
1191 }
1192 value.and_then(one)
1193}
1194
1195pub fn normalize_profile_alias(name: String) -> String {
1203 match name.strip_suffix(":FACE") {
1204 Some(base) => base.to_string(),
1205 None => name,
1206 }
1207}
1208
1209pub(crate) fn reference_name_array(value: Option<&serde_json::Value>) -> Vec<String> {
1212 value
1213 .and_then(serde_json::Value::as_array)
1214 .map(|items| items.iter().filter_map(reference_name).collect())
1215 .unwrap_or_default()
1216}
1217
1218pub fn reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1221 match value {
1222 Some(serde_json::Value::Array(items)) => items.iter().filter_map(reference_name).collect(),
1223 Some(other) => reference_name(other).into_iter().collect(),
1224 None => Vec::new(),
1225 }
1226}
1227
1228pub(super) fn unique_reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1230 let mut seen = std::collections::HashSet::new();
1231 reference_names(value)
1232 .into_iter()
1233 .filter(|name| seen.insert(name.clone()))
1234 .collect()
1235}
1236
1237pub fn consume_profile_sketch(ctx: &FeatureContext) -> bool {
1240 !matches!(
1241 ctx.param("consumeProfileSketch"),
1242 Some(serde_json::Value::Bool(false))
1243 )
1244}
1245
1246pub fn consume_sketch(ctx: &FeatureContext, reference_name: &str, result: &mut FeatureResult) {
1250 if !consume_profile_sketch(ctx) {
1251 return;
1252 }
1253 consume_sketch_always(reference_name, result);
1254}
1255
1256pub fn consume_sketch_always(reference_name: &str, result: &mut FeatureResult) {
1261 let base = reference_name
1262 .strip_suffix(":PROFILE")
1263 .unwrap_or(reference_name)
1264 .to_string();
1265 if !result.removed.contains(&base) {
1266 result.removed.push(base);
1267 }
1268}
1269
1270pub fn profile_loop_uv_curves(
1276 profile: &crate::feature_pipeline::SketchProfile,
1277 profile_loop: &crate::feature_pipeline::ProfileLoop,
1278) -> Result<Vec<NurbsCurve>, String> {
1279 let mut curves = Vec::with_capacity(profile_loop.curves.len());
1280 for curve in &profile_loop.curves {
1281 let mut control_points = Vec::with_capacity(curve.control_points.len());
1282 for cp in &curve.control_points {
1283 let delta = cp.point()?.sub(profile.origin);
1284 control_points.push(crate::Vec4::from_point(
1285 Vec3::new(delta.dot(profile.x_axis), delta.dot(profile.y_axis), 0.0),
1286 cp.w,
1287 ));
1288 }
1289 curves.push(NurbsCurve::new(
1290 curve.degree,
1291 curve.knots.clone(),
1292 control_points,
1293 )?);
1294 }
1295 Ok(curves)
1296}
1297
1298pub fn resolve_path(ctx: &FeatureContext, name: &str) -> Result<Vec<NurbsCurve>, String> {
1306 if let Some(chain) = ctx.scene.resolve_path(name) {
1307 return Ok(chain.clone());
1308 }
1309 if let Some(edge) = ctx.scene.resolve_edge(name) {
1310 return Ok(vec![edge_curve(edge)?]);
1311 }
1312 if let Some((owner, geometry)) = name.rsplit_once(':') {
1313 if geometry.len() > 1 && geometry.starts_with('G') {
1314 if let Some(chain) = ctx.scene.resolve_path(owner) {
1315 return Ok(chain.clone());
1316 }
1317 }
1318 }
1319 Err(format!(
1320 "path '{name}' not found (no sketch path or resident edge)"
1321 ))
1322}
1323
1324#[derive(Debug, Clone)]
1334pub struct PathSegment {
1335 pub name: String,
1336 pub curve: NurbsCurve,
1337 pub axis: Option<Vec3>,
1341}
1342
1343pub fn resolve_sweep_path(
1374 ctx: &FeatureContext,
1375 names: &[String],
1376) -> Result<crate::SweepPath, String> {
1377 let segments = resolve_path_chain(ctx, names)?;
1378 let mut curves = Vec::with_capacity(segments.len());
1379 let mut segment_names = Vec::with_capacity(segments.len());
1380 let mut axes = Vec::with_capacity(segments.len());
1381 for segment in segments {
1382 curves.push(segment.curve);
1383 segment_names.push(segment.name);
1384 axes.push(segment.axis);
1385 }
1386 crate::SweepPath::new(curves, segment_names)?
1387 .with_screw_axes(axes)
1388 .map_err(String::from)
1389}
1390
1391pub fn resolve_path_chain(
1392 ctx: &FeatureContext,
1393 names: &[String],
1394) -> Result<Vec<PathSegment>, String> {
1395 let mut segments: Vec<PathSegment> = Vec::new();
1396 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1397 for reference in names {
1398 let curves = resolve_path(ctx, reference)?;
1399 let published = ctx.scene.resolve_path_segment_names(reference);
1400 let published_axes = ctx.scene.resolve_path_segment_axes(reference);
1401 let single = curves.len() == 1;
1402 for (index, curve) in curves.into_iter().enumerate() {
1403 let name = published
1404 .and_then(|names| names.get(index).cloned().flatten())
1405 .filter(|name| !name.trim().is_empty())
1406 .unwrap_or_else(|| {
1407 if single {
1408 reference.clone()
1409 } else {
1410 format!("{reference}[{index}]")
1411 }
1412 });
1413 let axis = published_axes
1414 .and_then(|axes| axes.get(index).copied().flatten())
1415 .map(|axis| axis.direction);
1416 if seen.insert(name.clone()) {
1417 segments.push(PathSegment { name, curve, axis });
1418 }
1419 }
1420 }
1421 if segments.is_empty() {
1422 return Err("path selection resolved to no curves".into());
1423 }
1424 chain_path_segments(segments)
1425}
1426
1427fn path_endpoints(curve: &NurbsCurve) -> Result<(Vec3, Vec3), String> {
1429 let [t0, t1] = curve.domain()?;
1430 Ok((curve.evaluate(t0)?, curve.evaluate(t1)?))
1431}
1432
1433fn chain_path_segments(segments: Vec<PathSegment>) -> Result<Vec<PathSegment>, String> {
1436 if segments.len() < 2 {
1437 return Ok(segments);
1438 }
1439 let ends: Vec<(Vec3, Vec3)> = segments
1440 .iter()
1441 .map(|segment| path_endpoints(&segment.curve))
1442 .collect::<Result<_, _>>()?;
1443 let scale = ends
1447 .iter()
1448 .flat_map(|(a, b)| [a, b])
1449 .map(|point| point.sub(ends[0].0).length())
1450 .fold(1.0_f64, f64::max);
1451 let tolerance = 1e-5 * scale;
1452 let joins = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
1453
1454 let mut used = vec![false; segments.len()];
1455 used[0] = true;
1456 let mut order: Vec<(usize, bool)> = vec![(0, false)];
1459 let mut head = ends[0].0;
1460 let mut tail = ends[0].1;
1461 loop {
1462 if order.len() > 1 && joins(tail, head) {
1465 break;
1466 }
1467 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, tail, tolerance) {
1468 used[index] = true;
1469 order.push((index, reversed));
1470 tail = far;
1471 continue;
1472 }
1473 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, head, tolerance) {
1474 used[index] = true;
1475 order.insert(0, (index, !reversed));
1477 head = far;
1478 continue;
1479 }
1480 break;
1481 }
1482 let stranded: Vec<&str> = segments
1483 .iter()
1484 .zip(&used)
1485 .filter(|(_, used)| !**used)
1486 .map(|(segment, _)| segment.name.as_str())
1487 .collect();
1488 if !stranded.is_empty() {
1489 return Err(format!(
1490 "the selected path edges do not form ONE connected chain — {} joins neither \
1491 end of the chain the other selections form (a sweep path must be a single \
1492 head-to-tail run; select connected edges, or one branch at a time)",
1493 stranded.join(", ")
1494 ));
1495 }
1496 let mut chained = Vec::with_capacity(segments.len());
1497 let mut taken: Vec<Option<PathSegment>> = segments.into_iter().map(Some).collect();
1498 for (index, reversed) in order {
1499 let mut segment = taken[index].take().expect("each segment placed once");
1500 if reversed {
1501 segment.curve = segment.curve.reversed()?;
1502 }
1503 chained.push(segment);
1504 }
1505 Ok(chained)
1506}
1507
1508fn attach_path_segment(
1511 ends: &[(Vec3, Vec3)],
1512 used: &[bool],
1513 cursor: Vec3,
1514 tolerance: f64,
1515) -> Option<(usize, bool, Vec3)> {
1516 for (index, (start, end)) in ends.iter().enumerate() {
1517 if used[index] {
1518 continue;
1519 }
1520 if start.sub(cursor).length() <= tolerance {
1521 return Some((index, false, *end));
1522 }
1523 if end.sub(cursor).length() <= tolerance {
1524 return Some((index, true, *start));
1525 }
1526 }
1527 None
1528}
1529
1530pub fn translate_curves(curves: &[NurbsCurve], offset: Vec3) -> Result<Vec<NurbsCurve>, String> {
1534 curves
1535 .iter()
1536 .map(|curve| translate_curve(curve, offset))
1537 .collect()
1538}
1539
1540pub fn sketch_plane_frame(ctx: &FeatureContext, names: &[String]) -> Option<Frame> {
1553 for name in names {
1554 if let Some(frame) = ctx.scene.resolve_frame(name) {
1555 return Some(frame);
1556 }
1557 if let Some((owner, _)) = name.rsplit_once(':') {
1558 if let Some(frame) = ctx.scene.resolve_frame(owner) {
1559 return Some(frame);
1560 }
1561 }
1562 }
1563 None
1564}
1565
1566pub fn sketch_base_name(ctx: &FeatureContext, name: &str) -> String {
1576 if let Some((owner, tail)) = name.rsplit_once(':') {
1577 if tail.len() > 1 && tail.starts_with('G') && ctx.scene.resolve_frame(owner).is_some() {
1578 return owner.to_string();
1579 }
1580 }
1581 name.to_string()
1582}
1583
1584pub fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
1589 let [start, end] = curve.domain()?;
1590 let epsilon = (1e-9 * (end - start)).max(2e-9);
1591 let mut result = curve.clone();
1592 if t0 > start + epsilon && t0 < end - epsilon {
1593 result = result.split(t0)?.1;
1594 }
1595 let domain = result.domain()?;
1596 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
1597 result = result.split(t1)?.0;
1598 }
1599 Ok(result)
1600}
1601
1602pub fn edge_curve(edge: EdgeRef) -> Result<NurbsCurve, String> {
1615 crate::with_registered_solid_str(edge.handle, |solid| {
1616 let record = solid
1617 .edges
1618 .iter()
1619 .find(|candidate| candidate.id == edge.edge_id)
1620 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1621 trimmed_curve(&record.curve, record.t0, record.t1)
1622 })
1623}
1624
1625pub fn edge_axis(edge: EdgeRef) -> Result<Axis, String> {
1629 crate::with_registered_solid_str(edge.handle, |solid| {
1630 let record = solid
1631 .edges
1632 .iter()
1633 .find(|candidate| candidate.id == edge.edge_id)
1634 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1635 let start = record.curve.evaluate(record.t0)?;
1636 let end = record.curve.evaluate(record.t1)?;
1637 let direction = end.sub(start).normalized()?;
1638 Ok(Axis {
1639 point: start,
1640 direction,
1641 })
1642 })
1643}
1644
1645pub fn translate_curve(curve: &NurbsCurve, offset: Vec3) -> Result<NurbsCurve, String> {
1660 let mut control_points = Vec::with_capacity(curve.control_points.len());
1661 for cp in &curve.control_points {
1662 let cartesian = cp.point()?;
1663 control_points.push(crate::Vec4::from_point(cartesian.add(offset), cp.w));
1664 }
1665 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
1666}
1667
1668pub struct HoleGroup {
1671 pub index: usize,
1673 pub depth: usize,
1675 pub children: Vec<HoleGroup>,
1676}
1677
1678pub fn hole_nesting(
1684 profile: &SketchProfile,
1685 region: &[crate::feature_pipeline::ProfileLoop],
1686) -> Result<Vec<HoleGroup>, String> {
1687 let count = region.len().saturating_sub(1);
1688 if count == 0 {
1689 return Ok(Vec::new());
1690 }
1691 let mut polygons: Vec<Vec<[f64; 2]>> = Vec::with_capacity(count);
1693 for hole in ®ion[1..] {
1694 let mut polygon = Vec::new();
1695 for curve in &hole.curves {
1696 let [t0, t1] = curve.domain()?;
1697 for step in 0..8 {
1698 let t = t0 + (t1 - t0) * (step as f64 / 8.0);
1699 let point = curve.evaluate(t)?;
1700 let delta = point.sub(profile.origin);
1701 polygon.push([delta.dot(profile.x_axis), delta.dot(profile.y_axis)]);
1702 }
1703 }
1704 polygons.push(polygon);
1705 }
1706 let mut contains = vec![vec![false; count]; count];
1708 for i in 0..count {
1709 let Some(representative) = polygons[i].first().copied() else {
1710 continue;
1711 };
1712 for j in 0..count {
1713 if i != j && point_in_polygon_uv(representative, &polygons[j]) {
1714 contains[i][j] = true;
1715 }
1716 }
1717 }
1718 let depths: Vec<usize> = (0..count)
1719 .map(|i| contains[i].iter().filter(|inside| **inside).count())
1720 .collect();
1721 Ok((0..count)
1722 .filter(|&i| depths[i] == 0)
1723 .map(|i| hole_group(i, &contains, &depths))
1724 .collect())
1725}
1726
1727fn hole_group(i: usize, contains: &[Vec<bool>], depths: &[usize]) -> HoleGroup {
1730 let children = (0..depths.len())
1731 .filter(|&k| contains[k][i] && depths[k] == depths[i] + 1)
1732 .map(|k| hole_group(k, contains, depths))
1733 .collect();
1734 HoleGroup {
1735 index: i + 1,
1736 depth: depths[i],
1737 children,
1738 }
1739}
1740
1741fn point_in_polygon_uv(point: [f64; 2], polygon: &[[f64; 2]]) -> bool {
1743 let count = polygon.len();
1744 if count < 3 {
1745 return false;
1746 }
1747 let mut inside = false;
1748 let mut j = count - 1;
1749 for i in 0..count {
1750 let pi = polygon[i];
1751 let pj = polygon[j];
1752 let intersects = (pi[1] > point[1]) != (pj[1] > point[1])
1753 && point[0] < (pj[0] - pi[0]) * (point[1] - pi[1]) / (pj[1] - pi[1]) + pi[0];
1754 if intersects {
1755 inside = !inside;
1756 }
1757 j = i;
1758 }
1759 inside
1760}
1761
1762pub fn name_hole_cutter_faces(cutter: &mut BrepSolid, id: &str, key: &str, segment: Option<&str>) {
1769 let name = hole_face_name(id, key, segment);
1770 for face in cutter
1771 .shells
1772 .iter_mut()
1773 .flat_map(|shell| shell.faces.iter_mut())
1774 {
1775 if face.name.is_none() {
1776 face.name = Some(name.clone());
1777 }
1778 }
1779}
1780
1781pub fn hole_face_name(id: &str, key: &str, segment: Option<&str>) -> String {
1786 match segment {
1787 Some(segment) => format!("{id}:HOLE:{key}:{segment}"),
1788 None => format!("{id}:HOLE:{key}"),
1789 }
1790}
1791
1792pub fn subtract_solid(body: BrepSolid, cutter: BrepSolid) -> Result<BrepSolid, KernelRefusal> {
1795 let options = BooleanOptions {
1796 merge_coplanar_faces: true,
1797 ..BooleanOptions::default()
1798 };
1799 let body_handle = crate::register_solid_value(body);
1800 let cutter_handle = crate::register_solid_value(cutter);
1801 let cut = crate::with_two_registered_solids(body_handle, cutter_handle, |body, tool| {
1802 crate::boolean_operation(body, tool, BooleanOperation::Subtract, &options)
1803 });
1804 crate::free_registered_solid(body_handle);
1805 crate::free_registered_solid(cutter_handle);
1806 cut
1807}
1808
1809pub fn hole_prism(
1815 hole_curves: &[NurbsCurve],
1816 direction: Vec3,
1817 distance: f64,
1818 depth: usize,
1819) -> Result<BrepSolid, KernelRefusal> {
1820 let dir = direction.normalized().or_input(KernelStage::Collect, "direction")?;
1821 let span = distance.abs();
1822 let margin = span.max(1.0) * (depth as f64 + 1.0);
1823 let start_t = distance.min(0.0) - margin;
1824 let length = span + 2.0 * margin;
1825 let offset = dir.scale(start_t);
1826 let mut curves = Vec::with_capacity(hole_curves.len());
1827 for curve in hole_curves {
1828 curves.push(translate_curve(curve, offset).or_refuse(KernelStage::Collect, "translate_curve")?);
1829 }
1830 crate::extrude_profile_brep(&curves, dir, length)
1831}
1832
1833pub fn subtract_hole_prism(
1838 feature: &str,
1839 body: BrepSolid,
1840 hole_curves: &[NurbsCurve],
1841 direction: Vec3,
1842 distance: f64,
1843 id: &str,
1844 key: &str,
1845) -> Result<BrepSolid, String> {
1846 if hole_curves.len() < 2 {
1847 return Ok(body);
1848 }
1849 let mut cutter = hole_prism(hole_curves, direction, distance, 0)?;
1850 name_hole_cutter_faces(&mut cutter, id, key, None);
1851 subtract_solid(body, cutter)
1852 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))
1853}
1854
1855pub fn subtract_region_holes(
1868 mut body: BrepSolid,
1869 profile: &SketchProfile,
1870 region: &[crate::feature_pipeline::ProfileLoop],
1871 feature: &str,
1872 id: &str,
1873 segment: Option<&str>,
1874 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, KernelRefusal>,
1875) -> Result<BrepSolid, FeatureRefusal> {
1876 for group in hole_nesting(profile, region)? {
1877 let key = hole_key(®ion[group.index], group.index);
1878 let Some(cutter) = hole_cutter(region, &group, id, segment, build).map_err(|error| {
1881 error.with_message(|error| format!("{feature}: hole {key} cut failed: {error}"))
1882 })?
1883 else {
1884 continue;
1885 };
1886 body = subtract_solid(body, cutter).map_err(|error| {
1887 error.with_message(|error| format!("{feature}: hole {key} cut failed: {error}"))
1888 })?;
1889 }
1890 Ok(body)
1891}
1892
1893pub fn hole_key(hole: &crate::feature_pipeline::ProfileLoop, index: usize) -> String {
1897 hole.key().unwrap_or_else(|| index.to_string())
1898}
1899
1900fn hole_cutter(
1903 region: &[crate::feature_pipeline::ProfileLoop],
1904 group: &HoleGroup,
1905 id: &str,
1906 segment: Option<&str>,
1907 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, KernelRefusal>,
1908) -> Result<Option<BrepSolid>, KernelRefusal> {
1909 if region[group.index].curves.len() < 2 {
1910 return Ok(None);
1911 }
1912 let mut cutter = build(group.index, group.depth)?;
1913 name_hole_cutter_faces(
1914 &mut cutter,
1915 id,
1916 &hole_key(®ion[group.index], group.index),
1917 segment,
1918 );
1919 for child in &group.children {
1920 let Some(child_cutter) = hole_cutter(region, child, id, segment, build)? else {
1921 continue;
1922 };
1923 cutter = subtract_solid(cutter, child_cutter).map_err(|error| {
1924 error.with_message(|error| {
1925 format!("island {} carve from hole {} failed: {error}", child.index, group.index)
1926 })
1927 })?;
1928 }
1929 Ok(Some(cutter))
1930}
1931
1932pub enum PlaneLikeRef {
1944 Face(FaceRef),
1946 Frame(Frame),
1948}
1949
1950impl PlaneLikeRef {
1951 pub fn point_normal(&self) -> Result<(Vec3, Vec3), String> {
1956 match self {
1957 PlaneLikeRef::Frame(frame) => Ok((frame.origin, frame.z_axis)),
1958 PlaneLikeRef::Face(face) => face_point_normal(*face),
1959 }
1960 }
1961
1962 pub fn frame(&self) -> Result<Frame, String> {
1965 match self {
1966 PlaneLikeRef::Frame(frame) => Ok(*frame),
1967 PlaneLikeRef::Face(face) => face_frame(*face),
1968 }
1969 }
1970}
1971
1972pub fn resolve_plane_reference(ctx: &FeatureContext, name: &str) -> Option<PlaneLikeRef> {
1977 if let Some(face) = ctx.scene.resolve_face(name) {
1978 return Some(PlaneLikeRef::Face(face));
1979 }
1980 ctx.scene.resolve_frame(name).map(PlaneLikeRef::Frame)
1981}
1982
1983pub fn face_point_normal(face: FaceRef) -> Result<(Vec3, Vec3), String> {
1988 crate::with_registered_solid_str(face.handle, |solid| {
1989 for shell in &solid.shells {
1990 for record in &shell.faces {
1991 if record.id != face.face_id {
1992 continue;
1993 }
1994 let [u0, u1] = record.surface.domain_u()?;
1995 let [v0, v1] = record.surface.domain_v()?;
1996 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
1997 let point = record.surface.evaluate(um, vm)?;
1998 let mut normal = record.surface.normal(um, vm)?;
1999 if !record.same_sense {
2000 normal = normal.scale(-1.0);
2001 }
2002 return Ok((point, normal));
2003 }
2004 }
2005 Err(format!("face {} not found on solid", face.face_id))
2006 })
2007}
2008
2009pub fn face_boundary_points(
2016 solid: &BrepSolid,
2017 record: &crate::FaceRecord,
2018) -> Result<Vec<Vec3>, String> {
2019 let mut points = Vec::new();
2020 for loop_record in &record.loops {
2021 for coedge in &loop_record.coedges {
2022 let Some(edge) = solid.edges.iter().find(|e| e.id == coedge.edge_id) else {
2023 continue;
2024 };
2025 if edge.degenerate {
2026 continue;
2027 }
2028 for step in 0..=4 {
2029 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
2030 points.push(edge.curve.evaluate(t)?);
2031 }
2032 }
2033 }
2034 Ok(points)
2035}
2036
2037pub fn bounds_of(points: &[Vec3]) -> Option<(Vec3, Vec3)> {
2039 let mut iter = points.iter();
2040 let first = *iter.next()?;
2041 let (mut min, mut max) = (first, first);
2042 for point in iter {
2043 min = Vec3::new(min.x.min(point.x), min.y.min(point.y), min.z.min(point.z));
2044 max = Vec3::new(max.x.max(point.x), max.y.max(point.y), max.z.max(point.z));
2045 }
2046 Some((min, max))
2047}
2048
2049pub fn face_frame(face: FaceRef) -> Result<Frame, String> {
2056 crate::with_registered_solid_str(face.handle, |solid| {
2057 let record = solid
2058 .shells
2059 .iter()
2060 .flat_map(|shell| &shell.faces)
2061 .find(|candidate| candidate.id == face.face_id)
2062 .ok_or_else(|| format!("face {} not found on solid", face.face_id))?;
2063
2064 let [u0, u1] = record.surface.domain_u()?;
2065 let [v0, v1] = record.surface.domain_v()?;
2066 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2067
2068 let center_normal = record.surface.normal(um, vm)?;
2070 for (u, v) in [(u0, v0), (u1, v0), (u1, v1), (u0, v1)] {
2071 let normal = record.surface.normal(u, v)?;
2072 if normal.dot(center_normal).abs() < 1.0 - 1e-6 {
2073 return Err(
2074 "sketch/plane on a non-planar face is not yet migrated to the Rust pipeline"
2075 .into(),
2076 );
2077 }
2078 }
2079 let normal = if record.same_sense {
2081 center_normal
2082 } else {
2083 center_normal.scale(-1.0)
2084 };
2085
2086 let boundary = face_boundary_points(solid, record)?;
2088 let plane_point = record.surface.evaluate(um, vm)?;
2089 let center = match bounds_of(&boundary) {
2090 Some((min, max)) => min.add(max).scale(0.5),
2091 None => plane_point,
2092 };
2093 let unit = normal.normalized()?;
2095 let signed = center.sub(plane_point).dot(unit);
2096 let origin = center.sub(unit.scale(signed));
2097 Frame::from_origin_normal(origin, normal)
2098 })
2099}
2100
2101pub fn union_region_solids(solids: Vec<BrepSolid>) -> Result<BrepSolid, String> {
2111 union_solids_keeping(solids, &[])
2112}
2113
2114pub fn union_solids_keeping(
2128 solids: Vec<BrepSolid>,
2129 keep_unmerged: &[String],
2130) -> Result<BrepSolid, String> {
2131 let mut iter = solids.into_iter();
2132 let mut current = iter
2133 .next()
2134 .ok_or("profile produced no region solids to union")?;
2135 let options = BooleanOptions {
2136 merge_coplanar_faces: true,
2137 keep_unmerged_name_substrs: keep_unmerged.to_vec(),
2138 ..BooleanOptions::default()
2139 };
2140 for next in iter {
2141 let a = crate::register_solid_value(current);
2142 let b = crate::register_solid_value(next);
2143 let unioned = crate::with_two_registered_solids(a, b, |left, right| {
2144 crate::boolean_operation(left, right, BooleanOperation::Union, &options)
2145 });
2146 crate::free_registered_solid(a);
2147 crate::free_registered_solid(b);
2148 current = unioned.map_err(|error| format!("region union failed: {error}"))?;
2149 }
2150 Ok(current)
2151}
2152
2153
2154pub(super) fn primitive_transform_schema() -> serde_json::Value {
2156 serde_json::json!({
2157 "type": "transform",
2158 "default_value": {
2159 "position": [
2160 0,
2161 0,
2162 0
2163 ],
2164 "rotationEuler": [
2165 0,
2166 0,
2167 0
2168 ],
2169 "scale": [
2170 1,
2171 1,
2172 1
2173 ]
2174 },
2175 "referenceSelectionFilter": [
2176 "FACE",
2177 "EDGE",
2178 "VERTEX",
2179 "PLANE",
2180 "DATUM"
2181 ],
2182 "referenceLabel": "Start Reference",
2183 "referencePlaceholder": "Select point, edge, or face…",
2184 "hint": "Select a start reference, then position, rotate, and scale the solid relative to it."
2185 })
2186}
2187
2188pub(super) fn optional_boolean_schema() -> serde_json::Value {
2190 serde_json::json!({
2191 "type": "boolean_operation",
2192 "default_value": {
2193 "targets": [],
2194 "operation": "NONE",
2195 "mergeCoplanarFaces": true
2196 },
2197 "hint": "Optional boolean operation with selected solids"
2198 })
2199}
2200
2201