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
232#[derive(Debug, Clone, PartialEq, Eq)]
237pub(crate) enum ImplicitTarget {
238 NoSolid,
240 Sole(String),
242 Several(Vec<String>),
245}
246
247pub(crate) fn implicit_target_candidates(scene: &crate::feature_pipeline::SceneMap) -> Vec<String> {
256 let mut names: Vec<String> = scene
257 .solids
258 .keys()
259 .filter(|name| !scene.is_component_owned(name))
260 .cloned()
261 .collect();
262 names.sort();
263 names
264}
265
266pub(crate) fn implicit_boolean_target(scene: &crate::feature_pipeline::SceneMap) -> ImplicitTarget {
268 let mut names = implicit_target_candidates(scene);
269 match names.len() {
270 0 => ImplicitTarget::NoSolid,
271 1 => ImplicitTarget::Sole(names.remove(0)),
272 _ => ImplicitTarget::Several(names),
273 }
274}
275
276pub(crate) fn wants_implicit_target(feature_type: &str, input_params: &serde_json::Value) -> bool {
285 let is_hole = matches!(feature_type, "H" | "HOLE");
286 if matches!(feature_type, "B" | "BOOLEAN") {
287 return false;
288 }
289 let boolean = input_params.get("boolean");
290 if boolean.is_none() && !is_hole {
291 return false;
292 }
293 let operation = boolean
294 .and_then(|value| value.get("operation"))
295 .and_then(|value| value.as_str())
296 .unwrap_or(if is_hole { "SUBTRACT" } else { "NONE" });
297 if operation.eq_ignore_ascii_case("NONE") {
298 return false;
299 }
300 let named = boolean
301 .and_then(|value| value.get("targets"))
302 .and_then(|value| value.as_array())
303 .is_some_and(|array| {
304 array.iter().any(|entry| {
305 entry
306 .as_str()
307 .or_else(|| if is_hole { entry.get("name").and_then(|name| name.as_str()) } else { None })
308 .is_some_and(|name| !name.trim().is_empty())
309 })
310 });
311 !named
312}
313
314pub(crate) fn several_targets_fulfilment(operation: &str, solids: &[String]) -> crate::feature_pipeline::Fulfilment {
318 let listed: Vec<String> = solids.iter().map(|name| format!("`{name}`")).collect();
319 crate::feature_pipeline::Fulfilment {
320 requested: Vec::new(),
321 applied: Vec::new(),
322 rejected: vec![crate::feature_pipeline::Rejected {
323 name: "boolean".to_string(),
324 kind: crate::feature_pipeline::RejectedKind::NothingSelected,
325 reason: format!(
326 "{} names no target and the part holds {} solids ({}): pick the target solid; the body stands alone until you do",
327 operation.to_lowercase(),
328 solids.len(),
329 listed.join(", ")
330 ),
331 }],
332 extended: Vec::new(),
333 followed: Vec::new(),
334 }
335}
336
337fn apply_implicit_target(ctx: &FeatureContext, boolean: &mut BooleanParam, result: &mut FeatureResult) -> bool {
341 match implicit_boolean_target(ctx.scene) {
342 ImplicitTarget::NoSolid => false,
343 ImplicitTarget::Sole(name) => {
344 result.boolean_auto_target = Some(name.clone());
345 boolean.targets = vec![name];
346 true
347 }
348 ImplicitTarget::Several(names) => {
349 result.fulfilment = Some(several_targets_fulfilment(&boolean.operation, &names));
350 false
351 }
352 }
353}
354
355fn collect_edge_face_names(
357 solid: &BrepSolid,
358) -> (std::collections::HashMap<u64, Vec<String>>, Vec<u64>) {
359 use std::collections::HashMap;
360 let mut edge_faces: HashMap<u64, Vec<String>> = HashMap::new();
361 let mut encounter_order: Vec<u64> = Vec::new();
362 for shell in &solid.shells {
363 for face in &shell.faces {
364 let face_name = face.name.clone().unwrap_or_default();
365 for loop_record in &face.loops {
366 for coedge in &loop_record.coedges {
367 let entry = edge_faces.entry(coedge.edge_id).or_insert_with(|| {
368 encounter_order.push(coedge.edge_id);
369 Vec::new()
370 });
371 if entry.len() < 2 && !entry.contains(&face_name) {
372 entry.push(face_name.clone());
373 }
374 }
375 }
376 }
377 }
378 (edge_faces, encounter_order)
379}
380
381pub fn stamp_derived_edge_names(solid: &mut BrepSolid) {
385 use std::collections::HashMap;
386 let (edge_faces, encounter_order) = collect_edge_face_names(solid);
387 let solid_name_fallback = "Solid".to_string();
388 let mut base_counts: HashMap<String, usize> = HashMap::new();
389 for edge_id in encounter_order {
390 let Some(edge) = solid.edges.iter_mut().find(|edge| edge.id == edge_id) else {
391 continue;
392 };
393 if edge.degenerate {
394 continue;
395 }
396 let mut faces: Vec<String> = edge_faces
397 .get(&edge_id)
398 .map(|list| list.iter().filter(|n| !n.is_empty()).cloned().collect())
399 .unwrap_or_default();
400 faces.sort();
401 let base = if faces.len() >= 2 {
402 format!("{}|{}", faces[0], faces[1])
403 } else {
404 format!(
405 "{}|BOUNDARY",
406 faces.first().unwrap_or(&solid_name_fallback)
407 )
408 };
409 let is_topology_name = edge.name.as_deref().is_none_or(|n| n.contains('|'));
411 if is_topology_name {
412 let index = base_counts.entry(base.clone()).or_insert(0);
413 edge.name = Some(format!("{base}[{index}]"));
414 *index += 1;
415 }
416 }
417}
418
419pub fn namespace_copy_names(solid: &mut BrepSolid, suffix: &str) {
423 for shell in &mut solid.shells {
424 for face in &mut shell.faces {
425 if let Some(name) = face.name.as_ref() {
426 let trimmed = name.trim();
427 if !trimmed.is_empty() {
428 face.name = Some(format!("{trimmed}::{suffix}"));
429 }
430 }
431 }
432 }
433 for edge in &mut solid.edges {
434 if let Some(name) = edge.name.as_ref() {
435 let trimmed = name.trim();
436 if !trimmed.is_empty() && !trimmed.contains('|') {
439 edge.name = Some(format!("{trimmed}::{suffix}"));
440 }
441 }
442 }
443 stamp_derived_edge_names(solid);
444}
445
446pub fn ensure_unique_face_names(solid: &mut BrepSolid) {
450 use std::collections::HashMap;
451 let mut counts: HashMap<String, usize> = HashMap::new();
452 for shell in &solid.shells {
453 for face in &shell.faces {
454 if let Some(name) = &face.name {
455 *counts.entry(name.clone()).or_insert(0) += 1;
456 }
457 }
458 }
459 let mut running: HashMap<String, usize> = HashMap::new();
460 for shell in &mut solid.shells {
461 for face in &mut shell.faces {
462 let Some(name) = face.name.clone() else { continue };
463 if counts.get(&name).copied().unwrap_or(0) > 1 {
464 let index = running.entry(name.clone()).or_insert(0);
465 face.name = Some(format!("{name}[{index}]"));
466 *index += 1;
467 }
468 }
469 }
470}
471
472pub fn register_added(base: BrepSolid, name: &str) -> AddedSolid {
477 register_added_grouped(base, name, &[])
478}
479
480pub fn register_added_grouped(
490 mut base: BrepSolid,
491 name: &str,
492 containers: &[(String, Vec<String>)],
493) -> AddedSolid {
494 ensure_unique_face_names(&mut base);
495 stamp_derived_edge_names(&mut base);
496 let face_names = collect_face_names(&base);
497 let edge_names = collect_edge_names(&base);
498 let face_groups = collect_face_groups(&face_names, containers);
499 let mut edge_groups = collect_edge_group_aliases(&base, containers);
500 collect_derived_edge_bases(&edge_names, &mut edge_groups);
501 let handle = crate::register_solid_value(base);
502 AddedSolid {
503 handle,
504 name: name.to_string(),
505 face_names,
506 edge_names,
507 face_groups,
508 edge_groups,
509 }
510}
511
512fn collect_derived_edge_bases(
531 edge_names: &[(u64, String)],
532 groups: &mut Vec<(String, Vec<String>)>,
533) {
534 use std::collections::HashSet;
535 let taken: HashSet<&str> = groups.iter().map(|(name, _)| name.as_str()).collect();
536 let exact: HashSet<&str> = edge_names.iter().map(|(_, name)| name.as_str()).collect();
537 let mut bases: Vec<(String, Vec<String>)> = Vec::new();
538 for (_, name) in edge_names {
539 let Some(base) = name.strip_suffix(']').and_then(|head| {
541 let at = head.rfind('[')?;
542 let (base, index) = (&head[..at], &head[at + 1..]);
543 (!index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
544 .then_some(base)
545 }) else {
546 continue;
547 };
548 if base.is_empty() || !base.contains('|') {
549 continue;
550 }
551 if taken.contains(base) || exact.contains(base) {
552 continue;
553 }
554 match bases.iter_mut().find(|(existing, _)| existing == base) {
555 Some((_, members)) => members.push(name.clone()),
556 None => bases.push((base.to_string(), vec![name.clone()])),
557 }
558 }
559 groups.append(&mut bases);
560}
561
562fn collect_face_groups(
565 face_names: &[(u64, String)],
566 containers: &[(String, Vec<String>)],
567) -> Vec<(String, Vec<String>)> {
568 let mut groups = Vec::new();
569 for (container, members) in containers {
570 let live: Vec<String> = members
571 .iter()
572 .filter(|member| face_names.iter().any(|(_, name)| name == *member))
573 .cloned()
574 .collect();
575 if !live.is_empty() {
576 groups.push((container.clone(), live));
577 }
578 }
579 groups
580}
581
582fn collect_edge_group_aliases(
597 solid: &BrepSolid,
598 containers: &[(String, Vec<String>)],
599) -> Vec<(String, Vec<String>)> {
600 use std::collections::HashMap;
601 if containers.is_empty() {
602 return Vec::new();
603 }
604 let mut container_of: HashMap<&str, &str> = HashMap::new();
606 for (container, members) in containers {
607 for member in members {
608 container_of.insert(member.as_str(), container.as_str());
609 }
610 }
611
612 let (edge_faces, encounter_order) = collect_edge_face_names(solid);
613
614 let solid_name_fallback = "Solid".to_string();
615 let mut base_counts: HashMap<String, usize> = HashMap::new();
616 let mut aliases: HashMap<String, Vec<String>> = HashMap::new();
617 let mut order: Vec<String> = Vec::new();
618 for edge_id in encounter_order {
619 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
620 continue;
621 };
622 if edge.degenerate {
623 continue;
624 }
625 if !edge.name.as_deref().is_none_or(|name| name.contains('|')) {
628 continue;
629 }
630 let mut faces: Vec<String> = edge_faces
631 .get(&edge_id)
632 .map(|list| list.iter().filter(|name| !name.is_empty()).cloned().collect())
633 .unwrap_or_default();
634 let substituted = faces.iter().any(|name| container_of.contains_key(name.as_str()));
637 for face in &mut faces {
638 if let Some(container) = container_of.get(face.as_str()) {
639 *face = (*container).to_string();
640 }
641 }
642 faces.sort();
643 let base = if faces.len() >= 2 {
644 format!("{}|{}", faces[0], faces[1])
645 } else {
646 format!("{}|BOUNDARY", faces.first().unwrap_or(&solid_name_fallback))
647 };
648 let index = base_counts.entry(base.clone()).or_insert(0);
649 let alias = format!("{base}[{index}]");
650 *index += 1;
651 let Some(live) = edge.name.clone() else { continue };
654 if !substituted || live == alias {
655 continue;
656 }
657 if aliases.entry(alias.clone()).or_default().is_empty() {
658 order.push(alias.clone());
659 }
660 aliases.get_mut(&alias).expect("just inserted").push(live);
661 }
662 order
663 .into_iter()
664 .map(|alias| {
665 let ids = aliases.remove(&alias).unwrap_or_default();
666 (alias, ids)
667 })
668 .filter(|(_, ids)| !ids.is_empty())
669 .collect()
670}
671
672pub fn finalize_solids(ctx: &FeatureContext, bodies: Vec<(String, BrepSolid)>) -> FeatureResult {
678 let mut boolean = read_boolean_param(ctx);
679 let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
680
681 let separate = |mut result: FeatureResult, bodies: Vec<(String, BrepSolid)>| {
682 for (name, solid) in bodies {
683 result.added.push(register_added(solid, &name));
684 }
685 result
686 };
687
688 if boolean.operation == "NONE" {
689 return separate(result, bodies);
690 }
691 if boolean.targets.is_empty() && !apply_implicit_target(ctx, &mut boolean, &mut result) {
693 return separate(result, bodies);
694 }
695 let operation = match parse_boolean_operation(&boolean.operation) {
696 Ok(operation) => operation,
697 Err(error) => return ctx.fail(error),
698 };
699 let resolved = resolve_solid_names(ctx.scene, &boolean.targets, &mut result.unresolved);
700 if resolved.is_empty() {
701 return separate(result, bodies);
702 }
703 let options = BooleanOptions {
704 merge_coplanar_faces: boolean.merge_coplanar_faces,
705 ..BooleanOptions::default()
706 };
707 let mut bodies = bodies.into_iter();
708 let Some((_, first)) = bodies.next() else {
709 return result; };
711 let mut current = first;
714 for (_, target_handle) in &resolved {
715 let operand = crate::register_solid_value(current);
716 let folded = crate::with_two_registered_solids(*target_handle, operand, |target, op_solid| {
717 crate::boolean_operation(target, op_solid, operation, &options)
718 });
719 crate::free_registered_solid(operand);
720 match folded {
721 Ok(solid) => current = solid,
722 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
723 }
724 }
725 for (_, body) in bodies {
726 match crate::boolean_operation(¤t, &body, operation, &options) {
727 Ok(solid) => current = solid,
728 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
729 }
730 }
731 result.added.push(register_added(current, &resolved[0].0));
732 result.removed = resolved.into_iter().map(|(name, _)| name).collect();
733 result
734}
735
736pub fn finalize_solid(ctx: &FeatureContext, base: BrepSolid, base_name: &str) -> FeatureResult {
737 finalize_solid_grouped(ctx, base, base_name, &[])
738}
739
740pub fn finalize_solid_grouped(
746 ctx: &FeatureContext,
747 base: BrepSolid,
748 base_name: &str,
749 containers: &[(String, Vec<String>)],
750) -> FeatureResult {
751 let mut boolean = read_boolean_param(ctx);
752 let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
753
754 let stands_alone = boolean.operation == "NONE"
755 || (boolean.targets.is_empty() && !apply_implicit_target(ctx, &mut boolean, &mut result));
756 if stands_alone {
757 result.added.push(register_added_grouped(base, base_name, containers));
758 return result;
759 }
760
761 let operation = match parse_boolean_operation(&boolean.operation) {
762 Ok(operation) => operation,
763 Err(error) => return ctx.fail(error),
764 };
765
766 let resolved = resolve_solid_names(ctx.scene, &boolean.targets, &mut result.unresolved);
767
768 if resolved.is_empty() {
771 result.added.push(register_added_grouped(base, base_name, containers));
772 return result;
773 }
774
775 let options = BooleanOptions {
776 merge_coplanar_faces: boolean.merge_coplanar_faces,
777 ..BooleanOptions::default()
778 };
779 let mut current = base;
783 for (_, target_handle) in &resolved {
784 let operand_handle = crate::register_solid_value(current);
785 let folded = crate::with_two_registered_solids(*target_handle, operand_handle, |target, operand| {
786 crate::boolean_operation(target, operand, operation, &options)
787 });
788 crate::free_registered_solid(operand_handle);
789 match folded {
790 Ok(solid) => current = solid,
791 Err(error) => return ctx.fail(format!("boolean {} failed: {error}", boolean.operation)),
792 }
793 }
794
795 let result_name = resolved[0].0.clone();
796 result
797 .added
798 .push(register_added_grouped(current, &result_name, containers));
799 result.removed = resolved.into_iter().map(|(name, _)| name).collect();
800 result
801}
802
803pub struct BlendTarget {
813 pub handle: u32,
814 pub name: String,
815 pub edge_points: Vec<Vec3>,
816 pub edge_names: Vec<String>,
819 pub edge_ids: Vec<u64>,
821 pub picked_edge_ids: Vec<u64>,
824}
825
826pub struct BlendSelection {
828 pub target: Option<BlendTarget>,
830 pub multi_solid: bool,
831 pub unresolved: Vec<String>,
833}
834
835enum ResolvedRef {
837 Edge(u64),
838 Face(u64),
839}
840
841pub(crate) fn reference_name(value: &serde_json::Value) -> Option<String> {
845 let raw = match value {
846 serde_json::Value::String(text) => Some(text.as_str()),
847 serde_json::Value::Object(map) => map.get("name").and_then(|value| value.as_str()),
848 _ => None,
849 }?;
850 let trimmed = raw.trim();
851 (!trimmed.is_empty()).then(|| trimmed.to_string())
852}
853
854fn edge_name_or_id(solid: &BrepSolid, edge_id: u64) -> String {
860 solid
861 .edges
862 .iter()
863 .find(|edge| edge.id == edge_id)
864 .and_then(|edge| edge.name.clone())
865 .filter(|name| !name.is_empty())
866 .unwrap_or_else(|| format!("E{edge_id}"))
867}
868
869fn sample_edge_midpoint(solid: &BrepSolid, edge_id: u64) -> Result<Vec3, String> {
872 let edge = solid
873 .edges
874 .iter()
875 .find(|edge| edge.id == edge_id)
876 .ok_or_else(|| format!("dressup: edge {edge_id} not found on target solid"))?;
877 let mid = (edge.t0 + edge.t1) * 0.5;
878 edge.curve
879 .evaluate(mid)
880 .map_err(|error| format!("dressup: edge {edge_id} midpoint sample failed: {error}"))
881}
882
883fn face_boundary_edges(solid: &BrepSolid, face_id: u64) -> Result<Vec<u64>, String> {
891 let face = solid
892 .shells
893 .iter()
894 .flat_map(|shell| &shell.faces)
895 .find(|face| face.id == face_id)
896 .ok_or_else(|| format!("dressup: face {face_id} not found on target solid"))?;
897 let ruled_revolution = matches!(
898 face.surface.analytic(),
899 Some(crate::AnalyticSurface::RuledRevolution { .. })
900 );
901 let mut seams = FxHashSet::default();
902 if ruled_revolution {
903 let mut uses = FxHashSet::default();
904 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
905 if !uses.insert(coedge.edge_id) {
906 seams.insert(coedge.edge_id);
907 }
908 }
909 }
910 let mut ids = Vec::new();
911 let mut seen = FxHashSet::default();
912 for loop_record in &face.loops {
913 for coedge in &loop_record.coedges {
914 if seams.contains(&coedge.edge_id) {
915 continue;
916 }
917 let degenerate = solid
918 .edges
919 .iter()
920 .find(|edge| edge.id == coedge.edge_id)
921 .map(|edge| edge.degenerate)
922 .unwrap_or(false);
923 if degenerate {
924 continue;
925 }
926 if seen.insert(coedge.edge_id) {
927 ids.push(coedge.edge_id);
928 }
929 }
930 }
931 Ok(ids)
932}
933
934pub fn resolve_blend_selection(ctx: &FeatureContext) -> Result<BlendSelection, String> {
939 let names = reference_names(ctx.param("edges"));
940
941 let mut unresolved = Vec::new();
942 let mut resolved: Vec<(u32, ResolvedRef)> = Vec::new();
943 for name in names {
944 let edges = ctx.scene.resolve_edge_group(&name);
950 let faces = ctx.scene.resolve_face_group(&name);
951 if edges.is_empty() && faces.is_empty() {
952 unresolved.push(name);
953 continue;
954 }
955 for edge in edges {
956 resolved.push((edge.handle, ResolvedRef::Edge(edge.edge_id)));
957 }
958 for face in faces {
959 resolved.push((face.handle, ResolvedRef::Face(face.face_id)));
960 }
961 }
962
963 let mut handles: Vec<u32> = resolved.iter().map(|(handle, _)| *handle).collect();
965 handles.sort_unstable();
966 handles.dedup();
967 if handles.len() > 1 {
968 return Ok(BlendSelection {
969 target: None,
970 multi_solid: true,
971 unresolved,
972 });
973 }
974 let Some(&handle) = handles.first() else {
975 return Ok(BlendSelection {
977 target: None,
978 multi_solid: false,
979 unresolved,
980 });
981 };
982
983 let name = ctx
987 .scene
988 .solids
989 .iter()
990 .find(|(_, ®istered)| registered == handle)
991 .map(|(name, _)| name.clone())
992 .ok_or_else(|| format!("dressup: target handle {handle} has no scene name"))?;
993
994 let (edge_points, edge_names, edge_ids, picked_edge_ids) = crate::with_registered_solid_str(handle, |solid| {
999 let mut seen = FxHashSet::default();
1000 let mut picked = Vec::new();
1001 let mut points = Vec::new();
1002 let mut names = Vec::new();
1003 let mut ids = Vec::new();
1004 for (_, entity) in &resolved {
1005 match entity {
1006 ResolvedRef::Edge(edge_id) => {
1007 if !picked.contains(edge_id) {
1008 picked.push(*edge_id);
1009 }
1010 if seen.insert(*edge_id) {
1011 points.push(sample_edge_midpoint(solid, *edge_id)?);
1012 names.push(edge_name_or_id(solid, *edge_id));
1013 ids.push(*edge_id);
1014 }
1015 }
1016 ResolvedRef::Face(face_id) => {
1017 for edge_id in face_boundary_edges(solid, *face_id)? {
1018 if seen.insert(edge_id) {
1019 points.push(sample_edge_midpoint(solid, edge_id)?);
1020 names.push(edge_name_or_id(solid, edge_id));
1021 ids.push(edge_id);
1022 }
1023 }
1024 }
1025 }
1026 }
1027 Ok((points, names, ids, picked))
1028 })?;
1029
1030 Ok(BlendSelection {
1031 target: Some(BlendTarget {
1032 handle,
1033 name,
1034 edge_points,
1035 edge_names,
1036 edge_ids,
1037 picked_edge_ids,
1038 }),
1039 multi_solid: false,
1040 unresolved,
1041 })
1042}
1043
1044pub fn open_tangent_chain_extension(solid: &BrepSolid, selected: &[u64]) -> Result<Vec<(u64, Vec3, String)>, String> {
1067 walk_tangent_chains(solid, selected, selected, false)
1068}
1069
1070pub fn tangent_chain_expansion(
1086 solid: &BrepSolid,
1087 seeds: &[u64],
1088 selected: &[u64],
1089) -> Result<Vec<(u64, Vec3, String)>, String> {
1090 walk_tangent_chains(solid, seeds, selected, true)
1091}
1092
1093pub(super) fn single_tangent_continuation(candidates: &[(u64, Vec3)], onward: Vec3) -> Option<u64> {
1098 let mut tangent = candidates.iter().filter(|(_, direction)| direction.dot(onward) >= 1.0 - 1e-6);
1099 let first = tangent.next()?;
1100 tangent.next().is_none().then_some(first.0)
1101}
1102
1103fn walk_tangent_chains(
1104 solid: &BrepSolid,
1105 seeds: &[u64],
1106 selected: &[u64],
1107 follow: bool,
1108) -> Result<Vec<(u64, Vec3, String)>, String> {
1109 let tangent_at = |edge: &crate::topology::EdgeRecord, vertex: u64| -> Result<Vec3, String> {
1110 let at_start = edge.start_vertex_id == vertex;
1112 let t = if at_start { edge.t0 } else { edge.t1 };
1113 let tangent = edge.curve.unit_tangent(t, edge.t0, edge.t1)?;
1114 Ok(if at_start { tangent } else { tangent.scale(-1.0) })
1115 };
1116 let carriers = |edge_id: u64| -> Vec<&crate::NurbsSurface> {
1117 solid
1118 .shells
1119 .iter()
1120 .flat_map(|shell| &shell.faces)
1121 .filter(|face| face.loops.iter().flat_map(|lp| &lp.coedges).any(|coedge| coedge.edge_id == edge_id))
1122 .map(|face| &face.surface)
1123 .collect()
1124 };
1125 let changes_carrier = |from: u64, to: u64| -> bool {
1126 let own = carriers(from);
1127 !carriers(to)
1128 .iter()
1129 .all(|next| own.iter().any(|surface| crate::face_merge::same_surface(surface, next, 1e-9)))
1130 };
1131 let seam = |edge_id: u64| -> bool {
1134 solid.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
1135 face.loops.iter().flat_map(|lp| &lp.coedges).filter(|coedge| coedge.edge_id == edge_id).count() == 2
1136 })
1137 };
1138 let mere_split = |vertex: u64| -> bool {
1139 solid
1140 .edges
1141 .iter()
1142 .filter(|edge| !edge.degenerate && (edge.start_vertex_id == vertex || edge.end_vertex_id == vertex) && !seam(edge.id))
1143 .count()
1144 == 2
1145 };
1146 let mut taken: FxHashSet<u64> = selected.iter().chain(seeds).copied().collect();
1147 let mut added = Vec::new();
1148 for &seed in seeds {
1149 let Some(seed_edge) = solid.edges.iter().find(|edge| edge.id == seed) else { continue };
1150 if seed_edge.start_vertex_id == seed_edge.end_vertex_id || seed_edge.degenerate {
1151 continue;
1152 }
1153 for start in [seed_edge.start_vertex_id, seed_edge.end_vertex_id] {
1154 let mut current = seed_edge;
1155 let mut vertex = start;
1156 for _ in 0..if follow { solid.edges.len().max(128) } else { 128 } {
1157 let onward = tangent_at(current, vertex)?.scale(-1.0);
1160 let mut candidates = Vec::new();
1161 for candidate in &solid.edges {
1162 if candidate.id == current.id
1163 || candidate.degenerate
1164 || candidate.start_vertex_id == candidate.end_vertex_id
1165 || (candidate.start_vertex_id != vertex && candidate.end_vertex_id != vertex)
1166 {
1167 continue;
1168 }
1169 candidates.push((candidate.id, tangent_at(candidate, vertex)?));
1170 }
1171 let Some(next_id) = single_tangent_continuation(&candidates, onward) else { break };
1172 let Some(next) = solid.edges.iter().find(|edge| edge.id == next_id) else { break };
1173 if !follow && current.id == seed && !changes_carrier(seed, next.id) && !mere_split(vertex) {
1174 break;
1175 }
1176 if !taken.insert(next.id) {
1177 break;
1178 }
1179 added.push((next.id, sample_edge_midpoint(solid, next.id)?, edge_name_or_id(solid, next.id)));
1180 vertex = if next.start_vertex_id == vertex { next.end_vertex_id } else { next.start_vertex_id };
1181 current = next;
1182 }
1183 }
1184 }
1185 Ok(added)
1186}
1187
1188pub fn blend_face_base(id: &str) -> String {
1190 format!("{}:BLEND", if id.is_empty() { "F" } else { id })
1191}
1192
1193pub fn blend_face_name(id: &str, edge_name: &str) -> String {
1195 format!("{}:{}", blend_face_base(id), edge_name)
1196}
1197
1198pub fn require_blend_direction(ctx: &FeatureContext, operation: &str) -> Result<(), String> {
1201 let direction = ctx
1202 .param("direction")
1203 .and_then(|value| value.as_str())
1204 .map(|text| text.trim().to_uppercase())
1205 .filter(|text| !text.is_empty())
1206 .unwrap_or_else(|| "AUTO".to_string());
1207 if direction != "AUTO" && direction != "INSET" {
1208 return Err(format!(
1209 "{operation} direction '{direction}' belonged to the removed legacy mesh pipeline (only AUTO/INSET exist)"
1210 ));
1211 }
1212 Ok(())
1213}
1214
1215pub(super) fn number_or_default(ctx: &FeatureContext, key: &str, default: f64) -> f64 {
1218 match ctx.param(key) {
1219 None | Some(serde_json::Value::Null) => default,
1220 Some(_) => match ctx.number(key) {
1221 Ok(value) if value.is_finite() => value,
1222 _ => default,
1223 },
1224 }
1225}
1226
1227pub fn optional_number(ctx: &FeatureContext, key: &str) -> Result<Option<f64>, String> {
1232 if ctx.param(key).is_some() {
1233 ctx.number(key).map(Some)
1234 } else {
1235 Ok(None)
1236 }
1237}
1238
1239pub fn compose_trs_matrix(
1250 translate: [f64; 3],
1251 rotate_rad: [f64; 3],
1252 scale: [f64; 3],
1253 pivot: [f64; 3],
1254) -> [f64; 16] {
1255 let (a, b) = (rotate_rad[0].cos(), rotate_rad[0].sin());
1256 let (c, d) = (rotate_rad[1].cos(), rotate_rad[1].sin());
1257 let (e, f) = (rotate_rad[2].cos(), rotate_rad[2].sin());
1258 let (ae, af, be, bf) = (a * e, a * f, b * e, b * f);
1259 let r = [
1261 [c * e, -c * f, d],
1262 [af + be * d, ae - bf * d, -b * c],
1263 [bf - ae * d, be + af * d, a * c],
1264 ];
1265 let mut rs = [[0.0f64; 3]; 3];
1267 for i in 0..3 {
1268 for j in 0..3 {
1269 rs[i][j] = r[i][j] * scale[j];
1270 }
1271 }
1272 let rs_pivot = [
1273 rs[0][0] * pivot[0] + rs[0][1] * pivot[1] + rs[0][2] * pivot[2],
1274 rs[1][0] * pivot[0] + rs[1][1] * pivot[1] + rs[1][2] * pivot[2],
1275 rs[2][0] * pivot[0] + rs[2][1] * pivot[1] + rs[2][2] * pivot[2],
1276 ];
1277 let tx = translate[0] + pivot[0] - rs_pivot[0];
1278 let ty = translate[1] + pivot[1] - rs_pivot[1];
1279 let tz = translate[2] + pivot[2] - rs_pivot[2];
1280 [
1281 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,
1285 ]
1286}
1287
1288pub fn vec3_from_value(
1295 env: &crate::feature_pipeline::Env,
1296 value: Option<&serde_json::Value>,
1297 key: &str,
1298 default: [f64; 3],
1299) -> Result<[f64; 3], String> {
1300 let Some(value) = value.filter(|value| !value.is_null()) else {
1301 return Ok(default);
1302 };
1303 let component = |slot: Option<&serde_json::Value>, fallback: f64| -> Result<f64, String> {
1304 match slot {
1305 None | Some(serde_json::Value::Null) => Ok(fallback),
1306 Some(serde_json::Value::Number(number)) => number
1307 .as_f64()
1308 .ok_or_else(|| format!("param `{key}` has a non-finite component")),
1309 Some(serde_json::Value::String(source)) => {
1310 let number = env
1311 .eval(source)
1312 .map_err(|error| format!("param `{key}`: {error}"))?;
1313 if !number.is_finite() {
1318 return Err(format!(
1319 "param `{key}`: `{source}` evaluated to {number}"
1320 ));
1321 }
1322 Ok(number)
1323 }
1324 Some(other) => Err(format!(
1325 "param `{key}` component must be a number or expression, found {other}"
1326 )),
1327 }
1328 };
1329 if let Some(array) = value.as_array() {
1330 return Ok([
1331 component(array.first(), default[0])?,
1332 component(array.get(1), default[1])?,
1333 component(array.get(2), default[2])?,
1334 ]);
1335 }
1336 if value.is_object() {
1337 return Ok([
1338 component(value.get("x"), default[0])?,
1339 component(value.get("y"), default[1])?,
1340 component(value.get("z"), default[2])?,
1341 ]);
1342 }
1343 Err(format!("param `{key}` must be a vec3 array or object"))
1344}
1345
1346pub(super) fn resolve_solid_names(
1352 scene: &crate::feature_pipeline::SceneMap,
1353 names: &[String],
1354 unresolved: &mut Vec<String>,
1355) -> Vec<(String, u32)> {
1356 let mut resolved = Vec::new();
1357 for name in names {
1358 match scene.resolve_solid(name) {
1359 Some(handle) => resolved.push((name.clone(), handle)),
1360 None => unresolved.push(name.clone()),
1361 }
1362 }
1363 resolved
1364}
1365
1366pub(super) fn sheet_body_name(ctx: &FeatureContext) -> Option<String> {
1369 first_reference_name(ctx.param("sheet")).or_else(|| {
1370 let mut bodies = ctx.scene.solids.iter().filter_map(|(name, &handle)| {
1371 crate::feature_pipeline::sheet_metal::get_tree(handle).map(|_| name.clone())
1372 });
1373 match (bodies.next(), bodies.next()) {
1374 (Some(only), None) => Some(only),
1375 _ => None,
1376 }
1377 })
1378}
1379
1380pub(super) fn single_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1383 let name = match value {
1384 Some(serde_json::Value::String(text)) => Some(text.clone()),
1385 Some(serde_json::Value::Array(array)) => array.iter().find_map(|entry| {
1386 entry
1387 .as_str()
1388 .or_else(|| entry.get("name").and_then(|name| name.as_str()))
1389 .map(str::to_string)
1390 }),
1391 Some(object @ serde_json::Value::Object(_)) => {
1392 object.get("name").and_then(|name| name.as_str()).map(str::to_string)
1393 }
1394 _ => None,
1395 }?;
1396 let trimmed = name.trim();
1397 (!trimmed.is_empty()).then(|| trimmed.to_string())
1398}
1399
1400pub fn first_reference_name(value: Option<&serde_json::Value>) -> Option<String> {
1404 fn one(value: &serde_json::Value) -> Option<String> {
1405 match value {
1406 serde_json::Value::Array(items) => items.iter().find_map(one),
1407 other => reference_name(other),
1408 }
1409 }
1410 value.and_then(one)
1411}
1412
1413pub fn normalize_profile_alias(name: String) -> String {
1421 match crate::feature_pipeline::sketch_alias_base(&name) {
1422 Some(base) => base.to_string(),
1423 None => name,
1424 }
1425}
1426
1427pub(crate) fn reference_name_array(value: Option<&serde_json::Value>) -> Vec<String> {
1430 value
1431 .and_then(serde_json::Value::as_array)
1432 .map(|items| items.iter().filter_map(reference_name).collect())
1433 .unwrap_or_default()
1434}
1435
1436pub fn reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1439 match value {
1440 Some(serde_json::Value::Array(items)) => items.iter().filter_map(reference_name).collect(),
1441 Some(other) => reference_name(other).into_iter().collect(),
1442 None => Vec::new(),
1443 }
1444}
1445
1446pub(super) fn unique_reference_names(value: Option<&serde_json::Value>) -> Vec<String> {
1448 let mut seen = std::collections::HashSet::new();
1449 reference_names(value)
1450 .into_iter()
1451 .filter(|name| seen.insert(name.clone()))
1452 .collect()
1453}
1454
1455pub fn consume_profile_sketch(ctx: &FeatureContext) -> bool {
1458 !matches!(
1459 ctx.param("consumeProfileSketch"),
1460 Some(serde_json::Value::Bool(false))
1461 )
1462}
1463
1464pub fn consume_sketch(ctx: &FeatureContext, reference_name: &str, result: &mut FeatureResult) {
1468 if !consume_profile_sketch(ctx) {
1469 return;
1470 }
1471 consume_sketch_always(reference_name, result);
1472}
1473
1474pub fn consume_sketch_always(reference_name: &str, result: &mut FeatureResult) {
1479 let base = crate::feature_pipeline::canonical_sketch_name(reference_name).to_string();
1480 if !result.removed.contains(&base) {
1481 result.removed.push(base);
1482 }
1483}
1484
1485pub fn profile_loop_uv_curves(
1491 profile: &crate::feature_pipeline::SketchProfile,
1492 profile_loop: &crate::feature_pipeline::ProfileLoop,
1493) -> Result<Vec<NurbsCurve>, String> {
1494 let mut curves = Vec::with_capacity(profile_loop.curves.len());
1495 for curve in &profile_loop.curves {
1496 let mut control_points = Vec::with_capacity(curve.control_points.len());
1497 for cp in &curve.control_points {
1498 let delta = cp.point()?.sub(profile.origin);
1499 control_points.push(crate::Vec4::from_point(
1500 Vec3::new(delta.dot(profile.x_axis), delta.dot(profile.y_axis), 0.0),
1501 cp.w,
1502 ));
1503 }
1504 curves.push(NurbsCurve::new(
1505 curve.degree,
1506 curve.knots.clone(),
1507 control_points,
1508 )?);
1509 }
1510 Ok(curves)
1511}
1512
1513pub fn resolve_path(ctx: &FeatureContext, name: &str) -> Result<Vec<NurbsCurve>, String> {
1521 if let Some(chain) = ctx.scene.resolve_path(name) {
1522 return Ok(chain.clone());
1523 }
1524 if let Some(edge) = ctx.scene.resolve_edge(name) {
1525 return Ok(vec![edge_curve(edge)?]);
1526 }
1527 if let Some((owner, geometry)) = name.rsplit_once(':') {
1528 if geometry.len() > 1 && geometry.starts_with('G') {
1529 if let Some(chain) = ctx.scene.resolve_path(owner) {
1530 return Ok(chain.clone());
1531 }
1532 }
1533 }
1534 Err(format!(
1535 "path '{name}' not found (no sketch path or resident edge)"
1536 ))
1537}
1538
1539#[derive(Debug, Clone)]
1549pub struct PathSegment {
1550 pub name: String,
1551 pub curve: NurbsCurve,
1552 pub axis: Option<Vec3>,
1556}
1557
1558pub fn resolve_sweep_path(
1589 ctx: &FeatureContext,
1590 names: &[String],
1591) -> Result<crate::SweepPath, String> {
1592 let segments = resolve_path_chain(ctx, names)?;
1593 let mut curves = Vec::with_capacity(segments.len());
1594 let mut segment_names = Vec::with_capacity(segments.len());
1595 let mut axes = Vec::with_capacity(segments.len());
1596 for segment in segments {
1597 curves.push(segment.curve);
1598 segment_names.push(segment.name);
1599 axes.push(segment.axis);
1600 }
1601 crate::SweepPath::new(curves, segment_names)?
1602 .with_screw_axes(axes)
1603 .map_err(String::from)
1604}
1605
1606pub fn resolve_path_chain(
1607 ctx: &FeatureContext,
1608 names: &[String],
1609) -> Result<Vec<PathSegment>, String> {
1610 let mut segments: Vec<PathSegment> = Vec::new();
1611 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1612 for reference in names {
1613 let curves = resolve_path(ctx, reference)?;
1614 let published = ctx.scene.resolve_path_segment_names(reference);
1615 let published_axes = ctx.scene.resolve_path_segment_axes(reference);
1616 let single = curves.len() == 1;
1617 for (index, curve) in curves.into_iter().enumerate() {
1618 let name = published
1619 .and_then(|names| names.get(index).cloned().flatten())
1620 .filter(|name| !name.trim().is_empty())
1621 .unwrap_or_else(|| {
1622 if single {
1623 reference.clone()
1624 } else {
1625 format!("{reference}[{index}]")
1626 }
1627 });
1628 let axis = published_axes
1629 .and_then(|axes| axes.get(index).copied().flatten())
1630 .map(|axis| axis.direction);
1631 if seen.insert(name.clone()) {
1632 segments.push(PathSegment { name, curve, axis });
1633 }
1634 }
1635 }
1636 if segments.is_empty() {
1637 return Err("path selection resolved to no curves".into());
1638 }
1639 chain_path_segments(segments)
1640}
1641
1642fn path_endpoints(curve: &NurbsCurve) -> Result<(Vec3, Vec3), String> {
1644 let [t0, t1] = curve.domain()?;
1645 Ok((curve.evaluate(t0)?, curve.evaluate(t1)?))
1646}
1647
1648fn chain_path_segments(segments: Vec<PathSegment>) -> Result<Vec<PathSegment>, String> {
1651 if segments.len() < 2 {
1652 return Ok(segments);
1653 }
1654 let ends: Vec<(Vec3, Vec3)> = segments
1655 .iter()
1656 .map(|segment| path_endpoints(&segment.curve))
1657 .collect::<Result<_, _>>()?;
1658 let scale = ends
1662 .iter()
1663 .flat_map(|(a, b)| [a, b])
1664 .map(|point| point.sub(ends[0].0).length())
1665 .fold(1.0_f64, f64::max);
1666 let tolerance = 1e-5 * scale;
1667 let joins = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
1668
1669 let mut used = vec![false; segments.len()];
1670 used[0] = true;
1671 let mut order: Vec<(usize, bool)> = vec![(0, false)];
1674 let mut head = ends[0].0;
1675 let mut tail = ends[0].1;
1676 loop {
1677 if order.len() > 1 && joins(tail, head) {
1680 break;
1681 }
1682 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, tail, tolerance) {
1683 used[index] = true;
1684 order.push((index, reversed));
1685 tail = far;
1686 continue;
1687 }
1688 if let Some((index, reversed, far)) = attach_path_segment(&ends, &used, head, tolerance) {
1689 used[index] = true;
1690 order.insert(0, (index, !reversed));
1692 head = far;
1693 continue;
1694 }
1695 break;
1696 }
1697 let stranded: Vec<&str> = segments
1698 .iter()
1699 .zip(&used)
1700 .filter(|(_, used)| !**used)
1701 .map(|(segment, _)| segment.name.as_str())
1702 .collect();
1703 if !stranded.is_empty() {
1704 return Err(format!(
1705 "the selected path edges do not form ONE connected chain — {} joins neither \
1706 end of the chain the other selections form (a sweep path must be a single \
1707 head-to-tail run; select connected edges, or one branch at a time)",
1708 stranded.join(", ")
1709 ));
1710 }
1711 let mut chained = Vec::with_capacity(segments.len());
1712 let mut taken: Vec<Option<PathSegment>> = segments.into_iter().map(Some).collect();
1713 for (index, reversed) in order {
1714 let mut segment = taken[index].take().expect("each segment placed once");
1715 if reversed {
1716 segment.curve = segment.curve.reversed()?;
1717 }
1718 chained.push(segment);
1719 }
1720 Ok(chained)
1721}
1722
1723fn attach_path_segment(
1726 ends: &[(Vec3, Vec3)],
1727 used: &[bool],
1728 cursor: Vec3,
1729 tolerance: f64,
1730) -> Option<(usize, bool, Vec3)> {
1731 for (index, (start, end)) in ends.iter().enumerate() {
1732 if used[index] {
1733 continue;
1734 }
1735 if start.sub(cursor).length() <= tolerance {
1736 return Some((index, false, *end));
1737 }
1738 if end.sub(cursor).length() <= tolerance {
1739 return Some((index, true, *start));
1740 }
1741 }
1742 None
1743}
1744
1745pub fn translate_curves(curves: &[NurbsCurve], offset: Vec3) -> Result<Vec<NurbsCurve>, String> {
1749 curves
1750 .iter()
1751 .map(|curve| translate_curve(curve, offset))
1752 .collect()
1753}
1754
1755pub fn sketch_plane_frame(ctx: &FeatureContext, names: &[String]) -> Option<Frame> {
1768 for name in names {
1769 if let Some(frame) = ctx.scene.resolve_frame(name) {
1770 return Some(frame);
1771 }
1772 if let Some((owner, _)) = name.rsplit_once(':') {
1773 if let Some(frame) = ctx.scene.resolve_frame(owner) {
1774 return Some(frame);
1775 }
1776 }
1777 }
1778 None
1779}
1780
1781pub fn sketch_base_name(ctx: &FeatureContext, name: &str) -> String {
1791 if let Some((owner, tail)) = name.rsplit_once(':') {
1792 if tail.len() > 1 && tail.starts_with('G') && ctx.scene.resolve_frame(owner).is_some() {
1793 return owner.to_string();
1794 }
1795 }
1796 name.to_string()
1797}
1798
1799pub fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
1804 let [start, end] = curve.domain()?;
1805 let epsilon = (1e-9 * (end - start)).max(2e-9);
1806 let mut result = curve.clone();
1807 if t0 > start + epsilon && t0 < end - epsilon {
1808 result = result.split(t0)?.1;
1809 }
1810 let domain = result.domain()?;
1811 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
1812 result = result.split(t1)?.0;
1813 }
1814 Ok(result)
1815}
1816
1817pub fn edge_curve(edge: EdgeRef) -> Result<NurbsCurve, String> {
1830 crate::with_registered_solid_str(edge.handle, |solid| {
1831 let record = solid
1832 .edges
1833 .iter()
1834 .find(|candidate| candidate.id == edge.edge_id)
1835 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1836 trimmed_curve(&record.curve, record.t0, record.t1)
1837 })
1838}
1839
1840pub fn edge_axis(edge: EdgeRef) -> Result<Axis, String> {
1844 crate::with_registered_solid_str(edge.handle, |solid| {
1845 let record = solid
1846 .edges
1847 .iter()
1848 .find(|candidate| candidate.id == edge.edge_id)
1849 .ok_or_else(|| format!("edge {} not found on solid", edge.edge_id))?;
1850 let start = record.curve.evaluate(record.t0)?;
1851 let end = record.curve.evaluate(record.t1)?;
1852 let direction = end.sub(start).normalized()?;
1853 Ok(Axis {
1854 point: start,
1855 direction,
1856 })
1857 })
1858}
1859
1860pub fn translate_curve(curve: &NurbsCurve, offset: Vec3) -> Result<NurbsCurve, String> {
1875 let mut control_points = Vec::with_capacity(curve.control_points.len());
1876 for cp in &curve.control_points {
1877 let cartesian = cp.point()?;
1878 control_points.push(crate::Vec4::from_point(cartesian.add(offset), cp.w));
1879 }
1880 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
1881}
1882
1883pub struct HoleGroup {
1886 pub index: usize,
1888 pub depth: usize,
1890 pub children: Vec<HoleGroup>,
1891}
1892
1893pub fn hole_nesting(
1899 profile: &SketchProfile,
1900 region: &[crate::feature_pipeline::ProfileLoop],
1901) -> Result<Vec<HoleGroup>, String> {
1902 let count = region.len().saturating_sub(1);
1903 if count == 0 {
1904 return Ok(Vec::new());
1905 }
1906 let mut polygons: Vec<Vec<[f64; 2]>> = Vec::with_capacity(count);
1908 for hole in ®ion[1..] {
1909 let mut polygon = Vec::new();
1910 for curve in &hole.curves {
1911 let [t0, t1] = curve.domain()?;
1912 for step in 0..8 {
1913 let t = t0 + (t1 - t0) * (step as f64 / 8.0);
1914 let point = curve.evaluate(t)?;
1915 let delta = point.sub(profile.origin);
1916 polygon.push([delta.dot(profile.x_axis), delta.dot(profile.y_axis)]);
1917 }
1918 }
1919 polygons.push(polygon);
1920 }
1921 let mut contains = vec![vec![false; count]; count];
1923 for i in 0..count {
1924 let Some(representative) = polygons[i].first().copied() else {
1925 continue;
1926 };
1927 for j in 0..count {
1928 if i != j && point_in_polygon_uv(representative, &polygons[j]) {
1929 contains[i][j] = true;
1930 }
1931 }
1932 }
1933 let depths: Vec<usize> = (0..count)
1934 .map(|i| contains[i].iter().filter(|inside| **inside).count())
1935 .collect();
1936 Ok((0..count)
1937 .filter(|&i| depths[i] == 0)
1938 .map(|i| hole_group(i, &contains, &depths))
1939 .collect())
1940}
1941
1942fn hole_group(i: usize, contains: &[Vec<bool>], depths: &[usize]) -> HoleGroup {
1945 let children = (0..depths.len())
1946 .filter(|&k| contains[k][i] && depths[k] == depths[i] + 1)
1947 .map(|k| hole_group(k, contains, depths))
1948 .collect();
1949 HoleGroup {
1950 index: i + 1,
1951 depth: depths[i],
1952 children,
1953 }
1954}
1955
1956fn point_in_polygon_uv(point: [f64; 2], polygon: &[[f64; 2]]) -> bool {
1958 polygon.len() >= 3 && crate::polygon::parity_inside(point, polygon.len(), |i| polygon[i])
1959}
1960
1961pub fn name_hole_cutter_faces(cutter: &mut BrepSolid, id: &str, key: &str, segment: Option<&str>) {
1968 let name = hole_face_name(id, key, segment);
1969 for face in cutter
1970 .shells
1971 .iter_mut()
1972 .flat_map(|shell| shell.faces.iter_mut())
1973 {
1974 if face.name.is_none() {
1975 face.name = Some(name.clone());
1976 }
1977 }
1978}
1979
1980pub fn hole_face_name(id: &str, key: &str, segment: Option<&str>) -> String {
1985 match segment {
1986 Some(segment) => format!("{id}:HOLE:{key}:{segment}"),
1987 None => format!("{id}:HOLE:{key}"),
1988 }
1989}
1990
1991pub fn subtract_solid(body: BrepSolid, cutter: BrepSolid) -> Result<BrepSolid, KernelRefusal> {
1994 let options = BooleanOptions {
1995 merge_coplanar_faces: true,
1996 ..BooleanOptions::default()
1997 };
1998 let body_handle = crate::register_solid_value(body);
1999 let cutter_handle = crate::register_solid_value(cutter);
2000 let cut = crate::with_two_registered_solids(body_handle, cutter_handle, |body, tool| {
2001 crate::boolean_operation(body, tool, BooleanOperation::Subtract, &options)
2002 });
2003 crate::free_registered_solid(body_handle);
2004 crate::free_registered_solid(cutter_handle);
2005 cut
2006}
2007
2008pub fn intersect_solid(body: BrepSolid, tool: BrepSolid) -> Result<BrepSolid, KernelRefusal> {
2011 let options = BooleanOptions {
2012 merge_coplanar_faces: true,
2013 ..BooleanOptions::default()
2014 };
2015 let body_handle = crate::register_solid_value(body);
2016 let tool_handle = crate::register_solid_value(tool);
2017 let common = crate::with_two_registered_solids(body_handle, tool_handle, |body, tool| {
2018 crate::boolean_operation(body, tool, BooleanOperation::Intersect, &options)
2019 });
2020 crate::free_registered_solid(body_handle);
2021 crate::free_registered_solid(tool_handle);
2022 common
2023}
2024
2025pub fn hole_prism(
2031 hole_curves: &[NurbsCurve],
2032 direction: Vec3,
2033 distance: f64,
2034 depth: usize,
2035) -> Result<BrepSolid, KernelRefusal> {
2036 let dir = direction.normalized().or_input(KernelStage::Collect, "direction")?;
2037 let span = distance.abs();
2038 let margin = span.max(1.0) * (depth as f64 + 1.0);
2039 let start_t = distance.min(0.0) - margin;
2040 let length = span + 2.0 * margin;
2041 let offset = dir.scale(start_t);
2042 let mut curves = Vec::with_capacity(hole_curves.len());
2043 for curve in hole_curves {
2044 curves.push(translate_curve(curve, offset).or_refuse(KernelStage::Collect, "translate_curve")?);
2045 }
2046 crate::extrude_profile_brep(&curves, dir, length)
2047}
2048
2049pub fn subtract_hole_prism(
2054 feature: &str,
2055 body: BrepSolid,
2056 hole_curves: &[NurbsCurve],
2057 direction: Vec3,
2058 distance: f64,
2059 id: &str,
2060 key: &str,
2061) -> Result<BrepSolid, String> {
2062 if hole_curves.len() < 2 {
2063 return Ok(body);
2064 }
2065 let mut cutter = hole_prism(hole_curves, direction, distance, 0)?;
2066 name_hole_cutter_faces(&mut cutter, id, key, None);
2067 subtract_solid(body, cutter)
2068 .map_err(|error| format!("{feature}: hole {key} cut failed: {error}"))
2069}
2070
2071pub fn subtract_region_holes(
2084 mut body: BrepSolid,
2085 profile: &SketchProfile,
2086 region: &[crate::feature_pipeline::ProfileLoop],
2087 feature: &str,
2088 id: &str,
2089 segment: Option<&str>,
2090 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, KernelRefusal>,
2091) -> Result<BrepSolid, FeatureRefusal> {
2092 for group in hole_nesting(profile, region)? {
2093 let key = hole_key(®ion[group.index], group.index);
2094 let Some(cutter) = hole_cutter(region, &group, id, segment, build).map_err(|error| {
2097 error.with_message(|error| format!("{feature}: hole {key} cut failed: {error}"))
2098 })?
2099 else {
2100 continue;
2101 };
2102 body = subtract_solid(body, cutter).map_err(|error| {
2103 error.with_message(|error| format!("{feature}: hole {key} cut failed: {error}"))
2104 })?;
2105 }
2106 Ok(body)
2107}
2108
2109pub fn hole_key(hole: &crate::feature_pipeline::ProfileLoop, index: usize) -> String {
2113 hole.key().unwrap_or_else(|| index.to_string())
2114}
2115
2116fn hole_cutter(
2119 region: &[crate::feature_pipeline::ProfileLoop],
2120 group: &HoleGroup,
2121 id: &str,
2122 segment: Option<&str>,
2123 build: &mut dyn FnMut(usize, usize) -> Result<BrepSolid, KernelRefusal>,
2124) -> Result<Option<BrepSolid>, KernelRefusal> {
2125 if region[group.index].curves.len() < 2 {
2126 return Ok(None);
2127 }
2128 let mut cutter = build(group.index, group.depth)?;
2129 name_hole_cutter_faces(
2130 &mut cutter,
2131 id,
2132 &hole_key(®ion[group.index], group.index),
2133 segment,
2134 );
2135 for child in &group.children {
2136 let Some(child_cutter) = hole_cutter(region, child, id, segment, build)? else {
2137 continue;
2138 };
2139 cutter = subtract_solid(cutter, child_cutter).map_err(|error| {
2140 error.with_message(|error| {
2141 format!("island {} carve from hole {} failed: {error}", child.index, group.index)
2142 })
2143 })?;
2144 }
2145 Ok(Some(cutter))
2146}
2147
2148pub enum PlaneLikeRef {
2160 Face(FaceRef),
2162 Frame(Frame),
2164}
2165
2166impl PlaneLikeRef {
2167 pub fn point_normal(&self) -> Result<(Vec3, Vec3), String> {
2172 match self {
2173 PlaneLikeRef::Frame(frame) => Ok((frame.origin, frame.z_axis)),
2174 PlaneLikeRef::Face(face) => face_point_normal(*face),
2175 }
2176 }
2177
2178 pub fn frame(&self) -> Result<Frame, String> {
2181 match self {
2182 PlaneLikeRef::Frame(frame) => Ok(*frame),
2183 PlaneLikeRef::Face(face) => face_frame(*face),
2184 }
2185 }
2186}
2187
2188pub fn resolve_plane_reference(ctx: &FeatureContext, name: &str) -> Option<PlaneLikeRef> {
2193 if let Some(face) = ctx.scene.resolve_face(name) {
2194 return Some(PlaneLikeRef::Face(face));
2195 }
2196 ctx.scene.resolve_frame(name).map(PlaneLikeRef::Frame)
2197}
2198
2199pub fn face_point_normal(face: FaceRef) -> Result<(Vec3, Vec3), String> {
2204 crate::with_registered_solid_str(face.handle, |solid| {
2205 for shell in &solid.shells {
2206 for record in &shell.faces {
2207 if record.id != face.face_id {
2208 continue;
2209 }
2210 let [u0, u1] = record.surface.domain_u()?;
2211 let [v0, v1] = record.surface.domain_v()?;
2212 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
2213 let point = record.surface.evaluate(um, vm)?;
2214 let mut normal = record.surface.normal(um, vm)?;
2215 if !record.same_sense {
2216 normal = normal.scale(-1.0);
2217 }
2218 return Ok((point, normal));
2219 }
2220 }
2221 Err(format!("face {} not found on solid", face.face_id))
2222 })
2223}
2224
2225pub fn face_boundary_points(
2232 solid: &BrepSolid,
2233 record: &crate::FaceRecord,
2234) -> Result<Vec<Vec3>, String> {
2235 let mut points = Vec::new();
2236 for loop_record in &record.loops {
2237 for coedge in &loop_record.coedges {
2238 let Some(edge) = solid.edges.iter().find(|e| e.id == coedge.edge_id) else {
2239 continue;
2240 };
2241 if edge.degenerate {
2242 continue;
2243 }
2244 for step in 0..=4 {
2245 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
2246 points.push(edge.curve.evaluate(t)?);
2247 }
2248 }
2249 }
2250 Ok(points)
2251}
2252
2253pub fn bounds_of(points: &[Vec3]) -> Option<(Vec3, Vec3)> {
2255 let mut iter = points.iter();
2256 let first = *iter.next()?;
2257 let (mut min, mut max) = (first, first);
2258 for point in iter {
2259 min = Vec3::new(min.x.min(point.x), min.y.min(point.y), min.z.min(point.z));
2260 max = Vec3::new(max.x.max(point.x), max.y.max(point.y), max.z.max(point.z));
2261 }
2262 Some((min, max))
2263}
2264
2265pub fn face_frame(face: FaceRef) -> Result<Frame, String> {
2272 crate::with_registered_solid_str(face.handle, |solid| {
2273 let record = solid
2274 .shells
2275 .iter()
2276 .flat_map(|shell| &shell.faces)
2277 .find(|candidate| candidate.id == face.face_id)
2278 .ok_or_else(|| format!("face {} not found on solid", face.face_id))?;
2279
2280 let [u0, u1] = record.surface.domain_u()?;
2281 let [v0, v1] = record.surface.domain_v()?;
2282 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
2283
2284 let center_normal = record.surface.normal(um, vm)?;
2286 for (u, v) in [(u0, v0), (u1, v0), (u1, v1), (u0, v1)] {
2287 let normal = record.surface.normal(u, v)?;
2288 if normal.dot(center_normal).abs() < 1.0 - 1e-6 {
2289 return Err(
2290 "sketch/plane on a non-planar face is not yet migrated to the Rust pipeline"
2291 .into(),
2292 );
2293 }
2294 }
2295 let normal = if record.same_sense {
2297 center_normal
2298 } else {
2299 center_normal.scale(-1.0)
2300 };
2301
2302 let boundary = face_boundary_points(solid, record)?;
2304 let plane_point = record.surface.evaluate(um, vm)?;
2305 let center = match bounds_of(&boundary) {
2306 Some((min, max)) => min.add(max).scale(0.5),
2307 None => plane_point,
2308 };
2309 let unit = normal.normalized()?;
2311 let signed = center.sub(plane_point).dot(unit);
2312 let origin = center.sub(unit.scale(signed));
2313 Frame::from_origin_normal(origin, normal)
2314 })
2315}
2316
2317pub fn union_region_solids(solids: Vec<BrepSolid>) -> Result<BrepSolid, String> {
2327 union_solids_keeping(solids, &[])
2328}
2329
2330pub fn union_solids_keeping(
2344 solids: Vec<BrepSolid>,
2345 keep_unmerged: &[String],
2346) -> Result<BrepSolid, String> {
2347 let mut iter = solids.into_iter();
2348 let mut current = iter
2349 .next()
2350 .ok_or("profile produced no region solids to union")?;
2351 let options = BooleanOptions {
2352 merge_coplanar_faces: true,
2353 keep_unmerged_name_substrs: keep_unmerged.to_vec(),
2354 ..BooleanOptions::default()
2355 };
2356 for next in iter {
2357 let a = crate::register_solid_value(current);
2358 let b = crate::register_solid_value(next);
2359 let unioned = crate::with_two_registered_solids(a, b, |left, right| {
2360 crate::boolean_operation(left, right, BooleanOperation::Union, &options)
2361 });
2362 crate::free_registered_solid(a);
2363 crate::free_registered_solid(b);
2364 current = unioned.map_err(|error| format!("region union failed: {error}"))?;
2365 }
2366 Ok(current)
2367}
2368
2369
2370pub(super) fn primitive_transform_schema() -> serde_json::Value {
2372 serde_json::json!({
2373 "type": "transform",
2374 "default_value": {
2375 "position": [
2376 0,
2377 0,
2378 0
2379 ],
2380 "rotationEuler": [
2381 0,
2382 0,
2383 0
2384 ],
2385 "scale": [
2386 1,
2387 1,
2388 1
2389 ]
2390 },
2391 "referenceSelectionFilter": [
2392 "FACE",
2393 "EDGE",
2394 "VERTEX",
2395 "PLANE",
2396 "DATUM"
2397 ],
2398 "referenceLabel": "Start Reference",
2399 "referencePlaceholder": "Select point, edge, or face…",
2400 "hint": "Select a start reference, then position, rotate, and scale the solid relative to it."
2401 })
2402}
2403
2404pub(super) fn optional_boolean_schema() -> serde_json::Value {
2406 serde_json::json!({
2407 "type": "boolean_operation",
2408 "default_value": {
2409 "targets": [],
2410 "operation": "NONE",
2411 "mergeCoplanarFaces": true
2412 },
2413 "hint": "Optional boolean operation with selected solids"
2414 })
2415}
2416
2417
2418
2419