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 name.strip_suffix(":FACE") {
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 = reference_name
1350 .strip_suffix(":PROFILE")
1351 .unwrap_or(reference_name)
1352 .to_string();
1353 if !result.removed.contains(&base) {
1354 result.removed.push(base);
1355 }
1356}
1357
1358pub fn profile_loop_uv_curves(
1364 profile: &crate::feature_pipeline::SketchProfile,
1365 profile_loop: &crate::feature_pipeline::ProfileLoop,
1366) -> Result<Vec<NurbsCurve>, String> {
1367 let mut curves = Vec::with_capacity(profile_loop.curves.len());
1368 for curve in &profile_loop.curves {
1369 let mut control_points = Vec::with_capacity(curve.control_points.len());
1370 for cp in &curve.control_points {
1371 let delta = cp.point()?.sub(profile.origin);
1372 control_points.push(crate::Vec4::from_point(
1373 Vec3::new(delta.dot(profile.x_axis), delta.dot(profile.y_axis), 0.0),
1374 cp.w,
1375 ));
1376 }
1377 curves.push(NurbsCurve::new(
1378 curve.degree,
1379 curve.knots.clone(),
1380 control_points,
1381 )?);
1382 }
1383 Ok(curves)
1384}
1385
1386pub fn resolve_path(ctx: &FeatureContext, name: &str) -> Result<Vec<NurbsCurve>, String> {
1394 if let Some(chain) = ctx.scene.resolve_path(name) {
1395 return Ok(chain.clone());
1396 }
1397 if let Some(edge) = ctx.scene.resolve_edge(name) {
1398 return Ok(vec![edge_curve(edge)?]);
1399 }
1400 if let Some((owner, geometry)) = name.rsplit_once(':') {
1401 if geometry.len() > 1 && geometry.starts_with('G') {
1402 if let Some(chain) = ctx.scene.resolve_path(owner) {
1403 return Ok(chain.clone());
1404 }
1405 }
1406 }
1407 Err(format!(
1408 "path '{name}' not found (no sketch path or resident edge)"
1409 ))
1410}
1411
1412#[derive(Debug, Clone)]
1422pub struct PathSegment {
1423 pub name: String,
1424 pub curve: NurbsCurve,
1425 pub axis: Option<Vec3>,
1429}
1430
1431pub fn resolve_sweep_path(
1462 ctx: &FeatureContext,
1463 names: &[String],
1464) -> Result<crate::SweepPath, String> {
1465 let segments = resolve_path_chain(ctx, names)?;
1466 let mut curves = Vec::with_capacity(segments.len());
1467 let mut segment_names = Vec::with_capacity(segments.len());
1468 let mut axes = Vec::with_capacity(segments.len());
1469 for segment in segments {
1470 curves.push(segment.curve);
1471 segment_names.push(segment.name);
1472 axes.push(segment.axis);
1473 }
1474 crate::SweepPath::new(curves, segment_names)?
1475 .with_screw_axes(axes)
1476 .map_err(String::from)
1477}
1478
1479pub fn resolve_path_chain(
1480 ctx: &FeatureContext,
1481 names: &[String],
1482) -> Result<Vec<PathSegment>, String> {
1483 let mut segments: Vec<PathSegment> = Vec::new();
1484 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1485 for reference in names {
1486 let curves = resolve_path(ctx, reference)?;
1487 let published = ctx.scene.resolve_path_segment_names(reference);
1488 let published_axes = ctx.scene.resolve_path_segment_axes(reference);
1489 let single = curves.len() == 1;
1490 for (index, curve) in curves.into_iter().enumerate() {
1491 let name = published
1492 .and_then(|names| names.get(index).cloned().flatten())
1493 .filter(|name| !name.trim().is_empty())
1494 .unwrap_or_else(|| {
1495 if single {
1496 reference.clone()
1497 } else {
1498 format!("{reference}[{index}]")
1499 }
1500 });
1501 let axis = published_axes
1502 .and_then(|axes| axes.get(index).copied().flatten())
1503 .map(|axis| axis.direction);
1504 if seen.insert(name.clone()) {
1505 segments.push(PathSegment { name, curve, axis });
1506 }
1507 }
1508 }
1509 if segments.is_empty() {
1510 return Err("path selection resolved to no curves".into());
1511 }
1512 chain_path_segments(segments)
1513}
1514
1515fn path_endpoints(curve: &NurbsCurve) -> Result<(Vec3, Vec3), String> {
1517 let [t0, t1] = curve.domain()?;
1518 Ok((curve.evaluate(t0)?, curve.evaluate(t1)?))
1519}
1520
1521fn chain_path_segments(segments: Vec<PathSegment>) -> Result<Vec<PathSegment>, String> {
1524 if segments.len() < 2 {
1525 return Ok(segments);
1526 }
1527 let ends: Vec<(Vec3, Vec3)> = segments
1528 .iter()
1529 .map(|segment| path_endpoints(&segment.curve))
1530 .collect::<Result<_, _>>()?;
1531 let scale = ends
1535 .iter()
1536 .flat_map(|(a, b)| [a, b])
1537 .map(|point| point.sub(ends[0].0).length())
1538 .fold(1.0_f64, f64::max);
1539 let tolerance = 1e-5 * scale;
1540 let joins = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
1541
1542 let mut used = vec![false; segments.len()];
1543 used[0] = true;
1544 let mut order: Vec<(usize, bool)> = vec![(0, false)];
1547 let mut head = ends[0].0;
1548 let mut tail = ends[0].1;
1549 loop {
1550 if order.len() > 1 && joins(tail, head) {
1553 break;
1554 }
1555 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, tail, tolerance) {
1556 used[index] = true;
1557 order.push((index, reversed));
1558 tail = far;
1559 continue;
1560 }
1561 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, head, tolerance) {
1562 used[index] = true;
1563 order.insert(0, (index, !reversed));
1565 head = far;
1566 continue;
1567 }
1568 break;
1569 }
1570 let stranded: Vec<&str> = segments
1571 .iter()
1572 .zip(&used)
1573 .filter(|(_, used)| !**used)
1574 .map(|(segment, _)| segment.name.as_str())
1575 .collect();
1576 if !stranded.is_empty() {
1577 return Err(format!(
1578 "the selected path edges do not form ONE connected chain — {} joins neither \
1579 end of the chain the other selections form (a sweep path must be a single \
1580 head-to-tail run; select connected edges, or one branch at a time)",
1581 stranded.join(", ")
1582 ));
1583 }
1584 let mut chained = Vec::with_capacity(segments.len());
1585 let mut taken: Vec<Option<PathSegment>> = segments.into_iter().map(Some).collect();
1586 for (index, reversed) in order {
1587 let mut segment = taken[index].take().expect("each segment placed once");
1588 if reversed {
1589 segment.curve = segment.curve.reversed()?;
1590 }
1591 chained.push(segment);
1592 }
1593 Ok(chained)
1594}
1595
1596fn attach_path_segment(
1599 ends: &[(Vec3, Vec3)],
1600 used: &[bool],
1601 cursor: Vec3,
1602 tolerance: f64,
1603) -> Option<(usize, bool, Vec3)> {
1604 for (index, (start, end)) in ends.iter().enumerate() {
1605 if used[index] {
1606 continue;
1607 }
1608 if start.sub(cursor).length() <= tolerance {
1609 return Some((index, false, *end));
1610 }
1611 if end.sub(cursor).length() <= tolerance {
1612 return Some((index, true, *start));
1613 }
1614 }
1615 None
1616}
1617
1618pub fn translate_curves(curves: &[NurbsCurve], offset: Vec3) -> Result<Vec<NurbsCurve>, String> {
1622 curves
1623 .iter()
1624 .map(|curve| translate_curve(curve, offset))
1625 .collect()
1626}
1627
1628pub fn sketch_plane_frame(ctx: &FeatureContext, names: &[String]) -> Option<Frame> {
1641 for name in names {
1642 if let Some(frame) = ctx.scene.resolve_frame(name) {
1643 return Some(frame);
1644 }
1645 if let Some((owner, _)) = name.rsplit_once(':') {
1646 if let Some(frame) = ctx.scene.resolve_frame(owner) {
1647 return Some(frame);
1648 }
1649 }
1650 }
1651 None
1652}
1653
1654pub fn sketch_base_name(ctx: &FeatureContext, name: &str) -> String {
1664 if let Some((owner, tail)) = name.rsplit_once(':') {
1665 if tail.len() > 1 && tail.starts_with('G') && ctx.scene.resolve_frame(owner).is_some() {
1666 return owner.to_string();
1667 }
1668 }
1669 name.to_string()
1670}
1671
1672pub fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
1677 let [start, end] = curve.domain()?;
1678 let epsilon = (1e-9 * (end - start)).max(2e-9);
1679 let mut result = curve.clone();
1680 if t0 > start + epsilon && t0 < end - epsilon {
1681 result = result.split(t0)?.1;
1682 }
1683 let domain = result.domain()?;
1684 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
1685 result = result.split(t1)?.0;
1686 }
1687 Ok(result)
1688}
1689
1690pub fn edge_curve(edge: EdgeRef) -> Result<NurbsCurve, String> {
1703 crate::with_registered_solid_str(edge.handle, |solid| {
1704 let record = solid
1705 .edges
1706 .iter()
1707 .find(|candidate| candidate.id == edge.edge_id)
1708 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1709 trimmed_curve(&record.curve, record.t0, record.t1)
1710 })
1711}
1712
1713pub fn edge_axis(edge: EdgeRef) -> Result<Axis, String> {
1717 crate::with_registered_solid_str(edge.handle, |solid| {
1718 let record = solid
1719 .edges
1720 .iter()
1721 .find(|candidate| candidate.id == edge.edge_id)
1722 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1723 let start = record.curve.evaluate(record.t0)?;
1724 let end = record.curve.evaluate(record.t1)?;
1725 let direction = end.sub(start).normalized()?;
1726 Ok(Axis {
1727 point: start,
1728 direction,
1729 })
1730 })
1731}
1732
1733pub fn translate_curve(curve: &NurbsCurve, offset: Vec3) -> Result<NurbsCurve, String> {
1748 let mut control_points = Vec::with_capacity(curve.control_points.len());
1749 for cp in &curve.control_points {
1750 let cartesian = cp.point()?;
1751 control_points.push(crate::Vec4::from_point(cartesian.add(offset), cp.w));
1752 }
1753 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
1754}
1755
1756pub struct HoleGroup {
1759 pub index: usize,
1761 pub depth: usize,
1763 pub children: Vec<HoleGroup>,
1764}
1765
1766pub fn hole_nesting(
1772 profile: &SketchProfile,
1773 region: &[crate::feature_pipeline::ProfileLoop],
1774) -> Result<Vec<HoleGroup>, String> {
1775 let count = region.len().saturating_sub(1);
1776 if count == 0 {
1777 return Ok(Vec::new());
1778 }
1779 let mut polygons: Vec<Vec<[f64; 2]>> = Vec::with_capacity(count);
1781 for hole in ®ion[1..] {
1782 let mut polygon = Vec::new();
1783 for curve in &hole.curves {
1784 let [t0, t1] = curve.domain()?;
1785 for step in 0..8 {
1786 let t = t0 + (t1 - t0) * (step as f64 / 8.0);
1787 let point = curve.evaluate(t)?;
1788 let delta = point.sub(profile.origin);
1789 polygon.push([delta.dot(profile.x_axis), delta.dot(profile.y_axis)]);
1790 }
1791 }
1792 polygons.push(polygon);
1793 }
1794 let mut contains = vec![vec![false; count]; count];
1796 for i in 0..count {
1797 let Some(representative) = polygons[i].first().copied() else {
1798 continue;
1799 };
1800 for j in 0..count {
1801 if i != j && point_in_polygon_uv(representative, &polygons[j]) {
1802 contains[i][j] = true;
1803 }
1804 }
1805 }
1806 let depths: Vec<usize> = (0..count)
1807 .map(|i| contains[i].iter().filter(|inside| **inside).count())
1808 .collect();
1809 Ok((0..count)
1810 .filter(|&i| depths[i] == 0)
1811 .map(|i| hole_group(i, &contains, &depths))
1812 .collect())
1813}
1814
1815fn hole_group(i: usize, contains: &[Vec<bool>], depths: &[usize]) -> HoleGroup {
1818 let children = (0..depths.len())
1819 .filter(|&k| contains[k][i] && depths[k] == depths[i] + 1)
1820 .map(|k| hole_group(k, contains, depths))
1821 .collect();
1822 HoleGroup {
1823 index: i + 1,
1824 depth: depths[i],
1825 children,
1826 }
1827}
1828
1829fn point_in_polygon_uv(point: [f64; 2], polygon: &[[f64; 2]]) -> bool {
1831 polygon.len() >= 3 && crate::polygon::parity_inside(point, polygon.len(), |i| polygon[i])
1832}
1833
1834pub fn name_hole_cutter_faces(cutter: &mut BrepSolid, id: &str, key: &str, segment: Option<&str>) {
1841 let name = hole_face_name(id, key, segment);
1842 for face in cutter
1843 .shells
1844 .iter_mut()
1845 .flat_map(|shell| shell.faces.iter_mut())
1846 {
1847 if face.name.is_none() {
1848 face.name = Some(name.clone());
1849 }
1850 }
1851}
1852
1853pub fn hole_face_name(id: &str, key: &str, segment: Option<&str>) -> String {
1858 match segment {
1859 Some(segment) => format!("{id}:HOLE:{key}:{segment}"),
1860 None => format!("{id}:HOLE:{key}"),
1861 }
1862}
1863
1864pub fn subtract_solid(body: BrepSolid, cutter: BrepSolid) -> Result<BrepSolid, KernelRefusal> {
1867 let options = BooleanOptions {
1868 merge_coplanar_faces: true,
1869 ..BooleanOptions::default()
1870 };
1871 let body_handle = crate::register_solid_value(body);
1872 let cutter_handle = crate::register_solid_value(cutter);
1873 let cut = crate::with_two_registered_solids(body_handle, cutter_handle, |body, tool| {
1874 crate::boolean_operation(body, tool, BooleanOperation::Subtract, &options)
1875 });
1876 crate::free_registered_solid(body_handle);
1877 crate::free_registered_solid(cutter_handle);
1878 cut
1879}
1880
1881pub fn intersect_solid(body: BrepSolid, tool: BrepSolid) -> Result<BrepSolid, KernelRefusal> {
1884 let options = BooleanOptions {
1885 merge_coplanar_faces: true,
1886 ..BooleanOptions::default()
1887 };
1888 let body_handle = crate::register_solid_value(body);
1889 let tool_handle = crate::register_solid_value(tool);
1890 let common = crate::with_two_registered_solids(body_handle, tool_handle, |body, tool| {
1891 crate::boolean_operation(body, tool, BooleanOperation::Intersect, &options)
1892 });
1893 crate::free_registered_solid(body_handle);
1894 crate::free_registered_solid(tool_handle);
1895 common
1896}
1897
1898pub fn hole_prism(
1904 hole_curves: &[NurbsCurve],
1905 direction: Vec3,
1906 distance: f64,
1907 depth: usize,
1908) -> Result<BrepSolid, KernelRefusal> {
1909 let dir = direction.normalized().or_input(KernelStage::Collect, "direction")?;
1910 let span = distance.abs();
1911 let margin = span.max(1.0) * (depth as f64 + 1.0);
1912 let start_t = distance.min(0.0) - margin;
1913 let length = span + 2.0 * margin;
1914 let offset = dir.scale(start_t);
1915 let mut curves = Vec::with_capacity(hole_curves.len());
1916 for curve in hole_curves {
1917 curves.push(translate_curve(curve, offset).or_refuse(KernelStage::Collect, "translate_curve")?);
1918 }
1919 crate::extrude_profile_brep(&curves, dir, length)
1920}
1921
1922pub fn subtract_hole_prism(
1927 feature: &str,
1928 body: BrepSolid,
1929 hole_curves: &[NurbsCurve],
1930 direction: Vec3,
1931 distance: f64,
1932 id: &str,
1933 key: &str,
1934) -> Result<BrepSolid, String> {
1935 if hole_curves.len() < 2 {
1936 return Ok(body);
1937 }
1938 let mut cutter = hole_prism(hole_curves, direction, distance, 0)?;
1939 name_hole_cutter_faces(&mut cutter, id, key, None);
1940 subtract_solid(body, cutter)
1941 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))
1942}
1943
1944pub fn subtract_region_holes(
1957 mut body: BrepSolid,
1958 profile: &SketchProfile,
1959 region: &[crate::feature_pipeline::ProfileLoop],
1960 feature: &str,
1961 id: &str,
1962 segment: Option<&str>,
1963 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, KernelRefusal>,
1964) -> Result<BrepSolid, FeatureRefusal> {
1965 for group in hole_nesting(profile, region)? {
1966 let key = hole_key(®ion[group.index], group.index);
1967 let Some(cutter) = hole_cutter(region, &group, id, segment, build).map_err(|error| {
1970 error.with_message(|error| format!("{feature}: hole {key} cut failed: {error}"))
1971 })?
1972 else {
1973 continue;
1974 };
1975 body = subtract_solid(body, cutter).map_err(|error| {
1976 error.with_message(|error| format!("{feature}: hole {key} cut failed: {error}"))
1977 })?;
1978 }
1979 Ok(body)
1980}
1981
1982pub fn hole_key(hole: &crate::feature_pipeline::ProfileLoop, index: usize) -> String {
1986 hole.key().unwrap_or_else(|| index.to_string())
1987}
1988
1989fn hole_cutter(
1992 region: &[crate::feature_pipeline::ProfileLoop],
1993 group: &HoleGroup,
1994 id: &str,
1995 segment: Option<&str>,
1996 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, KernelRefusal>,
1997) -> Result<Option<BrepSolid>, KernelRefusal> {
1998 if region[group.index].curves.len() < 2 {
1999 return Ok(None);
2000 }
2001 let mut cutter = build(group.index, group.depth)?;
2002 name_hole_cutter_faces(
2003 &mut cutter,
2004 id,
2005 &hole_key(®ion[group.index], group.index),
2006 segment,
2007 );
2008 for child in &group.children {
2009 let Some(child_cutter) = hole_cutter(region, child, id, segment, build)? else {
2010 continue;
2011 };
2012 cutter = subtract_solid(cutter, child_cutter).map_err(|error| {
2013 error.with_message(|error| {
2014 format!("island {} carve from hole {} failed: {error}", child.index, group.index)
2015 })
2016 })?;
2017 }
2018 Ok(Some(cutter))
2019}
2020
2021pub enum PlaneLikeRef {
2033 Face(FaceRef),
2035 Frame(Frame),
2037}
2038
2039impl PlaneLikeRef {
2040 pub fn point_normal(&self) -> Result<(Vec3, Vec3), String> {
2045 match self {
2046 PlaneLikeRef::Frame(frame) => Ok((frame.origin, frame.z_axis)),
2047 PlaneLikeRef::Face(face) => face_point_normal(*face),
2048 }
2049 }
2050
2051 pub fn frame(&self) -> Result<Frame, String> {
2054 match self {
2055 PlaneLikeRef::Frame(frame) => Ok(*frame),
2056 PlaneLikeRef::Face(face) => face_frame(*face),
2057 }
2058 }
2059}
2060
2061pub fn resolve_plane_reference(ctx: &FeatureContext, name: &str) -> Option<PlaneLikeRef> {
2066 if let Some(face) = ctx.scene.resolve_face(name) {
2067 return Some(PlaneLikeRef::Face(face));
2068 }
2069 ctx.scene.resolve_frame(name).map(PlaneLikeRef::Frame)
2070}
2071
2072pub fn face_point_normal(face: FaceRef) -> Result<(Vec3, Vec3), String> {
2077 crate::with_registered_solid_str(face.handle, |solid| {
2078 for shell in &solid.shells {
2079 for record in &shell.faces {
2080 if record.id != face.face_id {
2081 continue;
2082 }
2083 let [u0, u1] = record.surface.domain_u()?;
2084 let [v0, v1] = record.surface.domain_v()?;
2085 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
2086 let point = record.surface.evaluate(um, vm)?;
2087 let mut normal = record.surface.normal(um, vm)?;
2088 if !record.same_sense {
2089 normal = normal.scale(-1.0);
2090 }
2091 return Ok((point, normal));
2092 }
2093 }
2094 Err(format!("face {} not found on solid", face.face_id))
2095 })
2096}
2097
2098pub fn face_boundary_points(
2105 solid: &BrepSolid,
2106 record: &crate::FaceRecord,
2107) -> Result<Vec<Vec3>, String> {
2108 let mut points = Vec::new();
2109 for loop_record in &record.loops {
2110 for coedge in &loop_record.coedges {
2111 let Some(edge) = solid.edges.iter().find(|e| e.id == coedge.edge_id) else {
2112 continue;
2113 };
2114 if edge.degenerate {
2115 continue;
2116 }
2117 for step in 0..=4 {
2118 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
2119 points.push(edge.curve.evaluate(t)?);
2120 }
2121 }
2122 }
2123 Ok(points)
2124}
2125
2126pub fn bounds_of(points: &[Vec3]) -> Option<(Vec3, Vec3)> {
2128 let mut iter = points.iter();
2129 let first = *iter.next()?;
2130 let (mut min, mut max) = (first, first);
2131 for point in iter {
2132 min = Vec3::new(min.x.min(point.x), min.y.min(point.y), min.z.min(point.z));
2133 max = Vec3::new(max.x.max(point.x), max.y.max(point.y), max.z.max(point.z));
2134 }
2135 Some((min, max))
2136}
2137
2138pub fn face_frame(face: FaceRef) -> Result<Frame, String> {
2145 crate::with_registered_solid_str(face.handle, |solid| {
2146 let record = solid
2147 .shells
2148 .iter()
2149 .flat_map(|shell| &shell.faces)
2150 .find(|candidate| candidate.id == face.face_id)
2151 .ok_or_else(|| format!("face {} not found on solid", face.face_id))?;
2152
2153 let [u0, u1] = record.surface.domain_u()?;
2154 let [v0, v1] = record.surface.domain_v()?;
2155 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2156
2157 let center_normal = record.surface.normal(um, vm)?;
2159 for (u, v) in [(u0, v0), (u1, v0), (u1, v1), (u0, v1)] {
2160 let normal = record.surface.normal(u, v)?;
2161 if normal.dot(center_normal).abs() < 1.0 - 1e-6 {
2162 return Err(
2163 "sketch/plane on a non-planar face is not yet migrated to the Rust pipeline"
2164 .into(),
2165 );
2166 }
2167 }
2168 let normal = if record.same_sense {
2170 center_normal
2171 } else {
2172 center_normal.scale(-1.0)
2173 };
2174
2175 let boundary = face_boundary_points(solid, record)?;
2177 let plane_point = record.surface.evaluate(um, vm)?;
2178 let center = match bounds_of(&boundary) {
2179 Some((min, max)) => min.add(max).scale(0.5),
2180 None => plane_point,
2181 };
2182 let unit = normal.normalized()?;
2184 let signed = center.sub(plane_point).dot(unit);
2185 let origin = center.sub(unit.scale(signed));
2186 Frame::from_origin_normal(origin, normal)
2187 })
2188}
2189
2190pub fn union_region_solids(solids: Vec<BrepSolid>) -> Result<BrepSolid, String> {
2200 union_solids_keeping(solids, &[])
2201}
2202
2203pub fn union_solids_keeping(
2217 solids: Vec<BrepSolid>,
2218 keep_unmerged: &[String],
2219) -> Result<BrepSolid, String> {
2220 let mut iter = solids.into_iter();
2221 let mut current = iter
2222 .next()
2223 .ok_or("profile produced no region solids to union")?;
2224 let options = BooleanOptions {
2225 merge_coplanar_faces: true,
2226 keep_unmerged_name_substrs: keep_unmerged.to_vec(),
2227 ..BooleanOptions::default()
2228 };
2229 for next in iter {
2230 let a = crate::register_solid_value(current);
2231 let b = crate::register_solid_value(next);
2232 let unioned = crate::with_two_registered_solids(a, b, |left, right| {
2233 crate::boolean_operation(left, right, BooleanOperation::Union, &options)
2234 });
2235 crate::free_registered_solid(a);
2236 crate::free_registered_solid(b);
2237 current = unioned.map_err(|error| format!("region union failed: {error}"))?;
2238 }
2239 Ok(current)
2240}
2241
2242
2243pub(super) fn primitive_transform_schema() -> serde_json::Value {
2245 serde_json::json!({
2246 "type": "transform",
2247 "default_value": {
2248 "position": [
2249 0,
2250 0,
2251 0
2252 ],
2253 "rotationEuler": [
2254 0,
2255 0,
2256 0
2257 ],
2258 "scale": [
2259 1,
2260 1,
2261 1
2262 ]
2263 },
2264 "referenceSelectionFilter": [
2265 "FACE",
2266 "EDGE",
2267 "VERTEX",
2268 "PLANE",
2269 "DATUM"
2270 ],
2271 "referenceLabel": "Start Reference",
2272 "referencePlaceholder": "Select point, edge, or face…",
2273 "hint": "Select a start reference, then position, rotate, and scale the solid relative to it."
2274 })
2275}
2276
2277pub(super) fn optional_boolean_schema() -> serde_json::Value {
2279 serde_json::json!({
2280 "type": "boolean_operation",
2281 "default_value": {
2282 "targets": [],
2283 "operation": "NONE",
2284 "mergeCoplanarFaces": true
2285 },
2286 "hint": "Optional boolean operation with selected solids"
2287 })
2288}
2289
2290
2291