1use super::*;
2
3pub struct DisplaySolidPayload {
5 pub faces: Vec<(u64, Option<String>)>,
8 pub mesh: Mesh,
10 pub edges: Vec<(u64, Option<String>, Vec<Vec3>)>,
12 pub vertices: Vec<(u64, Vec3)>,
14 pub chord_tolerance: f64,
16}
17
18pub fn display_chord_tolerance(solid: &BrepSolid, lod_factor: f64) -> f64 {
23 let mut extent = 0.0f64;
24 for vertex in &solid.vertices {
25 extent = extent
26 .max(vertex.point.x.abs())
27 .max(vertex.point.y.abs())
28 .max(vertex.point.z.abs());
29 }
30 if extent <= 0.0 {
31 let mut control_point_extent = 0.0f64;
32 for shell in &solid.shells {
33 for face in &shell.faces {
34 for row in &face.surface.control_points {
35 for cp in row {
36 let w = if cp.w != 0.0 { cp.w } else { 1.0 };
37 control_point_extent = control_point_extent
38 .max((cp.x / w).abs())
39 .max((cp.y / w).abs())
40 .max((cp.z / w).abs());
41 }
42 }
43 }
44 }
45 extent = control_point_extent / std::f64::consts::SQRT_2;
46 }
47 extent.max(1e-9) * 1.5e-3 * lod_factor
48}
49
50pub fn display_payload_handle_native(
56 handle: u32,
57 lod_factor: f64,
58) -> Result<DisplaySolidPayload, String> {
59 with_registered_solid_str(handle, |solid| display_payload_of_solid(solid, lod_factor))
60}
61
62pub fn display_payloads_handles_native(
83 handles: &[u32],
84 lod_factor: f64,
85) -> Vec<Result<DisplaySolidPayload, String>> {
86 let keys: Vec<Option<(u64, f64)>> = handles
89 .iter()
90 .map(|&handle| {
91 with_registered_solid_str(handle, |solid| {
92 Ok((congruence_signature(solid), display_chord_tolerance(solid, lod_factor)))
93 })
94 .ok()
95 })
96 .collect();
97 let mut base_of_signature: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
98 for (index, key) in keys.iter().enumerate() {
99 let Some((signature, chord)) = *key else { continue };
100 let base = base_of_signature.entry(signature).or_insert(index);
101 if chord < keys[*base].map_or(f64::INFINITY, |(_, base_chord)| base_chord) {
102 *base = index;
103 }
104 }
105 let mut posed: Vec<Option<(usize, RigidPose)>> = vec![None; handles.len()];
106 for (index, key) in keys.iter().enumerate() {
107 let Some((signature, _)) = *key else { continue };
108 let base = base_of_signature[&signature];
109 if base == index {
110 continue;
111 }
112 posed[index] = with_registered_solid_str(handles[base], |base_solid| {
113 with_registered_solid_str(handles[index], |solid| Ok(congruent_pose(base_solid, solid)))
114 })
115 .ok()
116 .flatten()
117 .map(|pose| (base, pose));
118 }
119 let meshed_indices: Vec<usize> = (0..handles.len()).filter(|&index| posed[index].is_none()).collect();
121 let meshed_handles: Vec<u32> = meshed_indices.iter().map(|&index| handles[index]).collect();
122 let profile_started = crate::profile::profile_enabled().then(web_time::Instant::now);
124 let mut payloads: Vec<Option<Result<DisplaySolidPayload, String>>> = (0..handles.len()).map(|_| None).collect();
125 for (index, payload) in meshed_indices.iter().zip(mesh_display_payloads(&meshed_handles, lod_factor)) {
126 payloads[*index] = Some(payload);
127 }
128 let mesh_ms = profile_started.map(|started| started.elapsed().as_secs_f64() * 1_000.0);
129 for index in 0..handles.len() {
131 let Some((base, pose)) = posed[index].take() else { continue };
132 let result = match &payloads[base] {
133 Some(Ok(base_payload)) => {
134 with_registered_solid_str(handles[index], |solid| Ok(posed_payload(base_payload, &pose, solid)))
135 }
136 _ => display_payload_handle_native(handles[index], lod_factor),
137 };
138 payloads[index] = Some(result);
139 }
140 if let Some(started) = profile_started {
141 profile_line!(
142 "display.batch solids={} meshed={} posed={} mesh_ms={:.1} total_ms={:.1}",
143 handles.len(),
144 meshed_indices.len(),
145 handles.len() - meshed_indices.len(),
146 mesh_ms.unwrap_or(0.0),
147 started.elapsed().as_secs_f64() * 1_000.0
148 );
149 }
150 payloads.into_iter().map(|payload| payload.expect("every slot meshed or posed")).collect()
151}
152
153fn mesh_display_payloads(handles: &[u32], lod_factor: f64) -> Vec<Result<DisplaySolidPayload, String>> {
155 #[cfg(feature = "parallel")]
156 {
157 use rayon::prelude::*;
158 const CHUNK: usize = 256;
160 let mut payloads = Vec::with_capacity(handles.len());
161 for chunk in handles.chunks(CHUNK) {
162 let solids: Vec<Result<BrepSolid, String>> =
163 chunk.iter().map(|&handle| registered_solid_clone(handle)).collect();
164 payloads.extend(
165 solids
166 .into_par_iter()
167 .with_max_len(1)
168 .map(|solid| solid.and_then(|solid| display_payload_of_solid(&solid, lod_factor)))
169 .collect::<Vec<_>>(),
170 );
171 }
172 payloads
173 }
174 #[cfg(not(feature = "parallel"))]
175 {
176 handles.iter().map(|&handle| display_payload_handle_native(handle, lod_factor)).collect()
177 }
178}
179
180#[derive(Clone, Copy, Debug)]
182struct RigidPose {
183 rotation: [[f64; 3]; 3],
184 translation: [f64; 3],
185}
186
187impl RigidPose {
188 fn point(&self, p: Vec3) -> Vec3 {
189 let r = &self.rotation;
190 Vec3::new(
191 r[0][0] * p.x + r[0][1] * p.y + r[0][2] * p.z + self.translation[0],
192 r[1][0] * p.x + r[1][1] * p.y + r[1][2] * p.z + self.translation[1],
193 r[2][0] * p.x + r[2][1] * p.y + r[2][2] * p.z + self.translation[2],
194 )
195 }
196
197 fn direction(&self, d: Vec3) -> Vec3 {
198 let r = &self.rotation;
199 Vec3::new(
200 r[0][0] * d.x + r[0][1] * d.y + r[0][2] * d.z,
201 r[1][0] * d.x + r[1][1] * d.y + r[1][2] * d.z,
202 r[2][0] * d.x + r[2][1] * d.y + r[2][2] * d.z,
203 )
204 }
205}
206
207fn congruence_signature(solid: &BrepSolid) -> u64 {
212 use std::hash::{Hash, Hasher};
213 let mut h = rustc_hash::FxHasher::default();
214 std::mem::discriminant(&solid.kind).hash(&mut h);
215 solid.genus.hash(&mut h);
216 solid.cells.len().hash(&mut h);
217 solid.vertices.len().hash(&mut h);
218 for vertex in &solid.vertices {
219 vertex.id.hash(&mut h);
220 }
221 solid.edges.len().hash(&mut h);
222 for edge in &solid.edges {
223 (edge.id, edge.start_vertex_id, edge.end_vertex_id, edge.degenerate).hash(&mut h);
224 (edge.t0.to_bits(), edge.t1.to_bits(), edge.curve.degree).hash(&mut h);
225 edge.curve.knots.iter().for_each(|k| k.to_bits().hash(&mut h));
226 edge.curve.control_points.iter().for_each(|c| c.w.to_bits().hash(&mut h));
227 }
228 solid.shells.len().hash(&mut h);
229 for shell in &solid.shells {
230 (shell.id, shell.faces.len()).hash(&mut h);
231 for face in &shell.faces {
232 let surface = &face.surface;
233 (face.id, face.same_sense, surface.degree_u, surface.degree_v, face.loops.len()).hash(&mut h);
234 surface.knots_u.iter().chain(&surface.knots_v).for_each(|k| k.to_bits().hash(&mut h));
235 surface.control_points.len().hash(&mut h);
236 for row in &surface.control_points {
237 row.len().hash(&mut h);
238 row.iter().for_each(|c| c.w.to_bits().hash(&mut h));
239 }
240 for loop_record in &face.loops {
241 (loop_record.id, loop_record.coedges.len()).hash(&mut h);
242 for coedge in &loop_record.coedges {
243 (coedge.id, coedge.edge_id, coedge.forward, coedge.pcurve.degree).hash(&mut h);
244 coedge.pcurve.knots.iter().for_each(|k| k.to_bits().hash(&mut h));
245 for c in &coedge.pcurve.control_points {
246 [c.x, c.y, c.z, c.w].iter().for_each(|v| v.to_bits().hash(&mut h));
247 }
248 }
249 }
250 }
251 }
252 h.finish()
253}
254
255fn congruent_pose(base: &BrepSolid, solid: &BrepSolid) -> Option<RigidPose> {
262 if !same_but_positions(base, solid) {
263 return None;
264 }
265 let points = |s: &BrepSolid| -> Option<Vec<(Vec3, f64)>> {
266 let mut out: Vec<(Vec3, f64)> = s.vertices.iter().map(|v| (v.point, 1.0)).collect();
267 for edge in &s.edges {
268 for c in &edge.curve.control_points {
269 out.push((c.point().ok()?, c.w));
270 }
271 }
272 for shell in &s.shells {
273 for face in &shell.faces {
274 for row in &face.surface.control_points {
275 for c in row {
276 out.push((c.point().ok()?, c.w));
277 }
278 }
279 }
280 }
281 Some(out)
282 };
283 let from = points(base)?;
284 let to = points(solid)?;
285 if from.len() != to.len() || from.len() < 3 {
286 return None;
287 }
288 let pose = frame_pose(&from, &to)?;
289 let magnitude = from
290 .iter()
291 .chain(&to)
292 .map(|(p, _)| p.x.abs().max(p.y.abs()).max(p.z.abs()))
293 .fold(1.0f64, f64::max);
294 let tolerance = 1e-9 * magnitude;
295 from.iter()
296 .zip(&to)
297 .all(|((p, wp), (q, wq))| wp.to_bits() == wq.to_bits() && pose.point(*p).sub(*q).length() <= tolerance)
298 .then_some(pose)
299}
300
301fn frame_pose(from: &[(Vec3, f64)], to: &[(Vec3, f64)]) -> Option<RigidPose> {
305 let p0 = from[0].0;
306 let (i1, d1) = from
307 .iter()
308 .enumerate()
309 .map(|(i, (p, _))| (i, p.sub(p0).length()))
310 .fold((0, 0.0), |best, next| if next.1 > best.1 { next } else { best });
311 if !(d1 > 0.0) {
312 return None;
313 }
314 let axis = from[i1].0.sub(p0).scale(1.0 / d1);
315 let (i2, d2) = from
316 .iter()
317 .enumerate()
318 .map(|(i, (p, _))| {
319 let d = p.sub(p0);
320 (i, d.sub(axis.scale(d.dot(axis))).length())
321 })
322 .fold((0, 0.0), |best, next| if next.1 > best.1 { next } else { best });
323 if !(d2 > 1e-6 * d1) {
324 return None;
325 }
326 let frame = |a: Vec3, b: Vec3, c: Vec3| -> Option<[Vec3; 3]> {
327 let e1 = b.sub(a);
328 let e1 = e1.scale(1.0 / e1.length());
329 let d = c.sub(a);
330 let e2 = d.sub(e1.scale(d.dot(e1)));
331 let length = e2.length();
332 if !(length > 0.0) {
333 return None;
334 }
335 let e2 = e2.scale(1.0 / length);
336 Some([e1, e2, e1.cross(e2)])
337 };
338 let f = frame(p0, from[i1].0, from[i2].0)?;
339 let g = frame(to[0].0, to[i1].0, to[i2].0)?;
340 let mut rotation = [[0.0f64; 3]; 3];
342 for k in 0..3 {
343 let (gk, fk) = ([g[k].x, g[k].y, g[k].z], [f[k].x, f[k].y, f[k].z]);
344 for row in 0..3 {
345 for col in 0..3 {
346 rotation[row][col] += gk[row] * fk[col];
347 }
348 }
349 }
350 let mut pose = RigidPose { rotation, translation: [0.0; 3] };
351 let moved = pose.point(p0);
352 let q0 = to[0].0;
353 pose.translation = [q0.x - moved.x, q0.y - moved.y, q0.z - moved.z];
354 pose.translation.iter().all(|t| t.is_finite()).then_some(pose)
355}
356
357fn same_but_positions(a: &BrepSolid, b: &BrepSolid) -> bool {
359 let same_bits = |x: &[f64], y: &[f64]| x.len() == y.len() && x.iter().zip(y).all(|(p, q)| p.to_bits() == q.to_bits());
360 let same_weights = |x: &[Vec4], y: &[Vec4]| x.len() == y.len() && x.iter().zip(y).all(|(p, q)| p.w.to_bits() == q.w.to_bits());
361 let same_pcurve = |x: &NurbsCurve, y: &NurbsCurve| {
362 x.degree == y.degree
363 && same_bits(&x.knots, &y.knots)
364 && x.control_points.len() == y.control_points.len()
365 && x.control_points.iter().zip(&y.control_points).all(|(p, q)| {
366 [p.x, p.y, p.z, p.w].iter().zip([q.x, q.y, q.z, q.w]).all(|(u, v)| u.to_bits() == v.to_bits())
367 })
368 };
369 a.kind == b.kind
370 && a.genus == b.genus
371 && a.cells.len() == b.cells.len()
372 && a.cells.iter().zip(&b.cells).all(|(x, y)| x.id == y.id && x.void == y.void && x.faces == y.faces)
373 && a.vertices.len() == b.vertices.len()
374 && a.vertices.iter().zip(&b.vertices).all(|(x, y)| x.id == y.id)
375 && a.edges.len() == b.edges.len()
376 && a.edges.iter().zip(&b.edges).all(|(x, y)| {
377 x.id == y.id
378 && x.start_vertex_id == y.start_vertex_id
379 && x.end_vertex_id == y.end_vertex_id
380 && x.degenerate == y.degenerate
381 && x.t0.to_bits() == y.t0.to_bits()
382 && x.t1.to_bits() == y.t1.to_bits()
383 && x.curve.degree == y.curve.degree
384 && same_bits(&x.curve.knots, &y.curve.knots)
385 && same_weights(&x.curve.control_points, &y.curve.control_points)
386 })
387 && a.shells.len() == b.shells.len()
388 && a.shells.iter().zip(&b.shells).all(|(x, y)| {
389 x.id == y.id
390 && x.faces.len() == y.faces.len()
391 && x.faces.iter().zip(&y.faces).all(|(f, g)| {
392 f.id == g.id
393 && f.same_sense == g.same_sense
394 && f.surface.degree_u == g.surface.degree_u
395 && f.surface.degree_v == g.surface.degree_v
396 && same_bits(&f.surface.knots_u, &g.surface.knots_u)
397 && same_bits(&f.surface.knots_v, &g.surface.knots_v)
398 && f.surface.control_points.len() == g.surface.control_points.len()
399 && f.surface.control_points.iter().zip(&g.surface.control_points).all(|(r, s)| same_weights(r, s))
400 && f.loops.len() == g.loops.len()
401 && f.loops.iter().zip(&g.loops).all(|(l, m)| {
402 l.id == m.id
403 && l.coedges.len() == m.coedges.len()
404 && l.coedges.iter().zip(&m.coedges).all(|(c, d)| {
405 c.id == d.id && c.edge_id == d.edge_id && c.forward == d.forward && same_pcurve(&c.pcurve, &d.pcurve)
406 })
407 })
408 })
409 })
410}
411
412fn posed_payload(base: &DisplaySolidPayload, pose: &RigidPose, solid: &BrepSolid) -> DisplaySolidPayload {
416 let mut mesh = base.mesh.clone();
417 for p in mesh.positions.chunks_exact_mut(3) {
418 let q = pose.point(Vec3::new(p[0], p[1], p[2]));
419 p.copy_from_slice(&[q.x, q.y, q.z]);
420 }
421 for n in mesh.normals.chunks_exact_mut(3) {
422 let q = pose.direction(Vec3::new(n[0], n[1], n[2]));
423 n.copy_from_slice(&[q.x, q.y, q.z]);
424 }
425 let faces = solid
426 .shells
427 .iter()
428 .flat_map(|shell| shell.faces.iter().map(|face| (face.id, face.name.clone())))
429 .collect();
430 let edge_names: std::collections::HashMap<u64, &String> =
431 solid.edges.iter().filter_map(|edge| edge.name.as_ref().map(|name| (edge.id, name))).collect();
432 let edges = base
433 .edges
434 .iter()
435 .map(|(id, _, points)| (*id, edge_names.get(id).map(|name| (*name).clone()), points.iter().map(|p| pose.point(*p)).collect()))
436 .collect();
437 let vertices = solid.vertices.iter().map(|vertex| (vertex.id, vertex.point)).collect();
438 DisplaySolidPayload { faces, mesh, edges, vertices, chord_tolerance: base.chord_tolerance }
439}
440
441fn display_payload_of_solid(solid: &BrepSolid, lod_factor: f64) -> Result<DisplaySolidPayload, String> {
443 let chord = display_chord_tolerance(solid, lod_factor);
444 let mesh = tessellate_brep_watertight(solid, chord)?;
445 let mut faces = Vec::new();
446 for shell in &solid.shells {
447 for face in &shell.faces {
448 faces.push((face.id, face.name.clone()));
449 }
450 }
451 let edge_names: std::collections::HashMap<u64, String> = solid
452 .edges
453 .iter()
454 .filter_map(|edge| edge.name.as_ref().map(|name| (edge.id, name.clone())))
455 .collect();
456 let edges = sample_edge_polylines(solid, chord)?
457 .into_iter()
458 .map(|(id, points)| (id, edge_names.get(&id).cloned(), points))
459 .collect();
460 let vertices = solid
461 .vertices
462 .iter()
463 .map(|vertex| (vertex.id, vertex.point))
464 .collect();
465 Ok(DisplaySolidPayload {
466 faces,
467 mesh,
468 edges,
469 vertices,
470 chord_tolerance: chord,
471 })
472}
473
474pub fn sketch_profile_display_payload(
489 profile: &crate::feature_pipeline::SketchProfile,
490) -> DisplaySolidPayload {
491 const SEGMENTS: usize = 24;
494 let origin = profile.origin;
495 let x_axis = profile.x_axis;
496 let y_axis = profile.y_axis;
497 let normal = profile.z_axis;
498 let to_uv = |p: Vec3| {
499 let d = p.sub(origin);
500 [d.dot(x_axis), d.dot(y_axis)]
501 };
502
503 let mut mesh = Mesh::default();
504 let mut edges: Vec<(u64, Option<String>, Vec<Vec3>)> = Vec::new();
505 let mut vertices: Vec<(u64, Vec3)> = Vec::new();
506 let mut next_edge_id: u64 = 0;
507 let mut next_vertex_id: u64 = 0;
508 let mut sketch_id: Option<String> = None;
509
510 for region in &profile.regions {
511 let Some((outer, holes)) = region.split_first() else {
512 continue;
513 };
514 let boundary = |lp: &crate::feature_pipeline::ProfileLoop| -> Vec<([f64; 2], Vec3)> {
517 let mut out = Vec::new();
518 for curve in &lp.curves {
519 let Ok([t0, t1]) = curve.domain() else { continue };
520 for step in 0..SEGMENTS {
521 let t = t0 + (t1 - t0) * step as f64 / SEGMENTS as f64;
522 if let Ok(p) = curve.evaluate(t) {
523 out.push((to_uv(p), p));
524 }
525 }
526 }
527 out
528 };
529 let outer_b = boundary(outer);
530 let holes_b: Vec<Vec<([f64; 2], Vec3)>> = holes.iter().map(boundary).collect();
531 for [a, b, c] in watertight_tessellation::triangulate_planar_region(&outer_b, &holes_b) {
532 let base = (mesh.positions.len() / 3) as u32;
533 for p in [a, b, c] {
534 mesh.positions.extend([p.x, p.y, p.z]);
535 mesh.normals.extend([normal.x, normal.y, normal.z]);
536 }
537 mesh.indices.extend([base, base + 1, base + 2]);
538 mesh.face_ids.push(0);
539 }
540
541 for lp in region {
543 for (index, curve) in lp.curves.iter().enumerate() {
544 let Ok([t0, t1]) = curve.domain() else { continue };
545 let mut polyline = Vec::with_capacity(SEGMENTS + 1);
546 for step in 0..=SEGMENTS {
547 let t = t0 + (t1 - t0) * step as f64 / SEGMENTS as f64;
548 if let Ok(p) = curve.evaluate(t) {
549 polyline.push(p);
550 }
551 }
552 let name = lp.edge_names.get(index).cloned().flatten();
553 if sketch_id.is_none() {
554 sketch_id = name
556 .as_deref()
557 .and_then(|n| n.split_once(":G").map(|(id, _)| id.to_string()));
558 }
559 if polyline.len() >= 2 {
560 edges.push((next_edge_id, name, polyline));
561 next_edge_id += 1;
562 }
563 if let Ok(p) = curve.evaluate(t0) {
564 vertices.push((next_vertex_id, p));
565 next_vertex_id += 1;
566 }
567 }
568 }
569 }
570
571 let faces = if mesh.face_ids.is_empty() {
573 Vec::new()
574 } else {
575 let face_name = sketch_id.map(|id| format!("{id}:FACE"));
576 vec![(0u64, face_name)]
577 };
578 DisplaySolidPayload {
579 faces,
580 mesh,
581 edges,
582 vertices,
583 chord_tolerance: 0.0,
584 }
585}
586
587pub fn sketch_display_payload(
617 profile: Option<&crate::feature_pipeline::SketchProfile>,
618 segments: &[(String, Vec<NurbsCurve>)],
619 points: &[Vec3],
620) -> DisplaySolidPayload {
621 const SEGMENTS: usize = 24;
627 const SAMPLES_PER_SPAN: usize = 4;
628 const MAX_SEGMENTS: usize = 4096;
629 const SAME_POINT: f64 = 1e-9;
632
633 let mut payload = match profile {
634 Some(profile) => sketch_profile_display_payload(profile),
635 None => DisplaySolidPayload {
636 faces: Vec::new(),
637 mesh: Mesh::default(),
638 edges: Vec::new(),
639 vertices: Vec::new(),
640 chord_tolerance: 0.0,
641 },
642 };
643
644 let drawn: std::collections::HashSet<String> = payload
645 .edges
646 .iter()
647 .filter_map(|(_, name, _)| name.clone())
648 .collect();
649 let mut next_edge_id = payload
650 .edges
651 .iter()
652 .map(|(id, _, _)| id + 1)
653 .max()
654 .unwrap_or(0);
655 let mut next_vertex_id = payload.vertices.iter().map(|(id, _)| id + 1).max().unwrap_or(0);
656
657 for (name, curves) in segments {
658 if drawn.contains(name) {
659 continue;
660 }
661 for curve in curves {
662 let Ok([t0, t1]) = curve.domain() else { continue };
663 let spans = curve
664 .knots
665 .windows(2)
666 .filter(|pair| pair[1] > pair[0])
667 .count();
668 let segments = (spans * SAMPLES_PER_SPAN).clamp(SEGMENTS, MAX_SEGMENTS);
669 let mut polyline = Vec::with_capacity(segments + 1);
670 for step in 0..=segments {
671 let t = t0 + (t1 - t0) * step as f64 / segments as f64;
672 if let Ok(point) = curve.evaluate(t) {
673 polyline.push(point);
674 }
675 }
676 if polyline.len() < 2 {
677 continue;
678 }
679 for end in [polyline[0], polyline[polyline.len() - 1]] {
680 if payload
681 .vertices
682 .iter()
683 .any(|(_, point)| point.sub(end).length() <= SAME_POINT)
684 {
685 continue;
686 }
687 payload.vertices.push((next_vertex_id, end));
688 next_vertex_id += 1;
689 }
690 payload.edges.push((next_edge_id, Some(name.clone()), polyline));
691 next_edge_id += 1;
692 }
693 }
694
695 for &point in points {
698 if payload
699 .vertices
700 .iter()
701 .any(|(_, drawn)| drawn.sub(point).length() <= SAME_POINT)
702 {
703 continue;
704 }
705 payload.vertices.push((next_vertex_id, point));
706 next_vertex_id += 1;
707 }
708 payload
709}
710
711pub fn mass_properties_handle_native(
721 handle: u32,
722 density: f64,
723) -> Result<DensityMassProperties, String> {
724 with_registered_solid_str(handle, |solid| {
725 Ok(solid_mass_properties_full(solid)?.with_density(density))
726 })
727}
728
729pub fn mass_measures_handle_native(handle: u32) -> Result<MassProperties, String> {
742 with_registered_solid_str(handle, |solid| {
743 let _caller = crate::mass_caller("abi.mass_measures");
744 let properties = solid_mass_properties(solid)?;
745 if properties.volume.abs() <= 1e-30 {
746 return Err("solid_mass_properties_full: non-positive volume".into());
747 }
748 Ok(properties)
749 })
750}
751
752pub fn validate_handle_native(handle: u32) -> Result<Vec<(String, String)>, String> {
759 with_registered_solid_str(handle, |solid| {
760 Ok(solid
761 .validate()
762 .into_iter()
763 .map(|issue| (issue.severity.to_string(), issue.message))
764 .collect())
765 })
766}
767
768pub fn topology_counts_native(handle: u32) -> Result<(usize, usize, usize, usize), String> {
792 with_registered_solid_str(handle, |solid| {
793 Ok((
794 solid.shells.iter().map(|shell| shell.faces.len()).sum(),
795 solid.edges.len(),
796 solid.edges.iter().filter(|edge| !edge.degenerate).count(),
797 solid.vertices.len(),
798 ))
799 })
800}
801
802pub fn connectivity_handle_native(handle: u32) -> Result<crate::ConnectivityReport, String> {
808 with_registered_solid_str(handle, |solid| Ok(crate::solid_connectivity(solid)))
809}
810
811pub fn euler_handle_native(handle: u32) -> Result<crate::EulerReport, String> {
817 with_registered_solid_str(handle, |solid| Ok(crate::solid_euler(solid)))
818}
819
820pub fn topology_kind_handle_native(handle: u32) -> Result<crate::KindReport, String> {
824 with_registered_solid_str(handle, |solid| Ok(crate::topology_kind(solid)))
825}
826
827pub fn body_kind_handle_native(handle: u32) -> Result<crate::BodyKind, String> {
831 with_registered_solid_str(handle, |solid| Ok(solid.kind))
832}
833
834pub fn loop_crossings_handle_native(handle: u32) -> Result<crate::LoopCrossingReport, String> {
842 with_registered_solid_str(handle, |solid| Ok(crate::loop_self_crossings(solid)))
843}
844
845pub fn vector_area_handle_native(handle: u32) -> Result<crate::VectorAreaReport, String> {
851 with_registered_solid_str(handle, |solid| Ok(crate::shell_vector_areas(solid)))
852}
853
854pub fn self_intersections_handle_native(
861 handle: u32,
862 chord_tolerance: f64,
863) -> Result<crate::SelfIntersectionReport, String> {
864 with_registered_solid_str(handle, |solid| {
865 let mut options = crate::SelfIntersectionOptions::for_solid(solid);
866 if chord_tolerance > 0.0 && chord_tolerance.is_finite() {
867 options.chord_tolerance = chord_tolerance;
868 }
869 crate::solid_self_intersections(solid, options)
870 })
871}
872
873pub fn transform_pivot_native(handle: u32) -> Result<[f64; 3], String> {
877 with_registered_solid_str(handle, |solid| {
878 Ok(crate::feature_pipeline::transform_bbox_center(solid))
879 })
880}
881
882pub fn solid_edge_length_total_native(handle: u32) -> Result<f64, String> {
887 with_registered_solid_str(handle, solid_edge_length_total)
888}
889
890pub fn face_measurements_native(
897 handle: u32,
898 face_name: &str,
899) -> Result<(f64, f64, &'static str), String> {
900 with_registered_solid_str(handle, |solid| {
901 let face = solid
902 .shells
903 .iter()
904 .flat_map(|shell| &shell.faces)
905 .find(|face| face.name.as_deref() == Some(face_name))
906 .ok_or_else(|| format!("face '{face_name}' not found"))?;
907 let surface_type = face
908 .surface
909 .analytic()
910 .map(|analytic| analytic.kind_label())
911 .unwrap_or("NURBS");
912 Ok((
913 face_area(face)?,
914 face_boundary_length(solid, face)?,
915 surface_type,
916 ))
917 })
918}
919
920pub fn edge_length_native(handle: u32, edge_name: &str) -> Result<f64, String> {
923 with_registered_solid_str(handle, |solid| {
924 let edge = solid
925 .edges
926 .iter()
927 .find(|edge| edge.name.as_deref() == Some(edge_name))
928 .ok_or_else(|| format!("edge '{edge_name}' not found"))?;
929 edge_arc_length(edge)
930 })
931}
932
933pub fn face_carrier_dimensions_native(
939 handle: u32,
940 face_name: &str,
941) -> Result<Option<crate::CarrierDimensions>, String> {
942 with_registered_solid_str(handle, |solid| {
943 let face = solid
944 .shells
945 .iter()
946 .flat_map(|shell| &shell.faces)
947 .find(|face| face.name.as_deref() == Some(face_name))
948 .ok_or_else(|| format!("face '{face_name}' not found"))?;
949 Ok(crate::surface_carrier_dimensions(&face.surface))
950 })
951}
952
953pub fn edge_carrier_dimensions_native(
958 handle: u32,
959 edge_name: &str,
960) -> Result<Option<crate::CarrierDimensions>, String> {
961 with_registered_solid_str(handle, |solid| {
962 let edge = solid
963 .edges
964 .iter()
965 .find(|edge| edge.name.as_deref() == Some(edge_name))
966 .ok_or_else(|| format!("edge '{edge_name}' not found"))?;
967 if edge.degenerate {
968 return Ok(None);
969 }
970 Ok(crate::curve_carrier_dimensions(&edge.curve, edge.t0, edge.t1))
971 })
972}
973
974#[wasm_bindgen]
978pub fn solid_handle_to_buffer(handle: u32) -> Result<WasmSolidBuffer, JsValue> {
979 with_registered_solid(handle, |solid| solid_buffer(solid, "{}".into()))
980}
981
982pub fn export_step_handles(
990 handles: &[u32],
991 name: &str,
992 unit: &str,
993 timestamp: &str,
994) -> Result<String, String> {
995 if handles.is_empty() {
996 return Err("export_step_handles: no solids to export".into());
997 }
998 let mut solids = Vec::with_capacity(handles.len());
999 for &handle in handles {
1000 let solid: BrepSolid = with_registered_solid_str(handle, |solid| Ok(solid.clone()))?;
1001 solids.push(solid);
1002 }
1003 export_step(&solids, name, unit, timestamp)
1004}
1005
1006pub fn export_step_named_handles(
1010 named: &[(String, u32)],
1011 name: &str,
1012 unit: &str,
1013 timestamp: &str,
1014 pmi: Option<&crate::StepPmi<'_>>,
1015 colors: Option<&crate::StepColors>,
1016) -> Result<crate::StepExportReport, String> {
1017 if named.is_empty() {
1018 return Err("export_step_named_handles: no solids to export".into());
1019 }
1020 let mut solids = Vec::with_capacity(named.len());
1021 for (solid_name, handle) in named {
1022 let solid: BrepSolid = with_registered_solid_str(*handle, |solid| Ok(solid.clone()))?;
1023 solids.push((solid_name.clone(), solid));
1024 }
1025 let borrowed: Vec<(String, &BrepSolid)> = solids
1026 .iter()
1027 .map(|(solid_name, solid)| (solid_name.clone(), solid))
1028 .collect();
1029 crate::export_step_report_named(&borrowed, name, unit, timestamp, pmi, colors)
1030}
1031
1032pub fn export_step_assembly_handles(
1043 document_name: &str,
1044 named: &[(String, u32)],
1045 components: &[(String, String, crate::Mat4)],
1046 unit: &str,
1047 timestamp: &str,
1048 pmi: Option<&crate::StepPmi<'_>>,
1049 colors: Option<&crate::StepColors>,
1050) -> Result<crate::StepExportReport, String> {
1051 if named.is_empty() {
1052 return Err("export_step_assembly_handles: no solids to export".into());
1053 }
1054 let mut solids = Vec::with_capacity(named.len());
1055 for (solid_name, handle) in named {
1056 let solid: BrepSolid = with_registered_solid_str(*handle, |solid| Ok(solid.clone()))?;
1057 solids.push((solid_name.clone(), solid));
1058 }
1059 let assembly = crate::assembly_export_tree(document_name, solids, components)?;
1060 crate::export_step_assembly_report(&assembly, unit, timestamp, pmi, colors)
1061}
1062
1063pub fn export_iges_handles(
1069 handles: &[u32],
1070 name: &str,
1071 unit: &str,
1072 timestamp: &str,
1073) -> Result<String, String> {
1074 if handles.is_empty() {
1075 return Err("export_iges_handles: no solids to export".into());
1076 }
1077 let mut solids = Vec::with_capacity(handles.len());
1078 for &handle in handles {
1079 let solid: BrepSolid = with_registered_solid_str(handle, |solid| Ok(solid.clone()))?;
1080 solids.push(solid);
1081 }
1082 export_iges(&solids, name, unit, timestamp)
1083}