1use geop_core_geometry::nurb_surface::NurbSurface3D;
19use geop_core_math::{
20 geop_error::{GeopError, GeopResult, WithContext},
21 primitives::{Color10, Line, PrimitiveScene, TriangleFace},
22 scalars::Scalar,
23 vector::Vector3,
24};
25use geop_core_topology::{Coedge, CoedgeId, Model, boundary::BoundaryType};
26
27const COEDGE_INSET: f64 = 0.02;
30
31const ARROW_SIZE_FRACTION: f64 = 0.15 / 12.0;
34
35const FACE_NORMAL_LENGTH: f64 = 0.1;
37
38const MAX_TRIM_FRACTION: f64 = 0.45;
42
43const VERTEX_COLOR: Color10 = Color10::Red;
44const EDGE_COLOR: Color10 = Color10::Gray;
45const EDGE_MARKER_COLOR: Color10 = Color10::Pink;
46const COEDGE_OUTER_COLOR: Color10 = Color10::Orange;
51const COEDGE_OUTER_MARKER_COLOR: Color10 = Color10::Cyan;
52const COEDGE_HOLE_COLOR: Color10 = Color10::Purple;
53const COEDGE_HOLE_MARKER_COLOR: Color10 = Color10::Olive;
54const FACE_COLOR: Color10 = Color10::Blue;
55const FACE_OPACITY: f64 = 0.35;
56const FACE_NORMAL_COLOR: Color10 = Color10::Green;
57
58const AXIS_RADIUS: f64 = 0.01 / 3.0;
60const AXIS_STEP: f64 = 0.1;
62const AXIS_STEP_TICK_LENGTH: f64 = 0.03;
64const AXIS_X_COLOR: Color10 = Color10::Red;
65const AXIS_Y_COLOR: Color10 = Color10::Green;
66const AXIS_Z_COLOR: Color10 = Color10::Blue;
67
68fn coedge_tangent_3d<S: Scalar>(
72 coedge: &Coedge<S>,
73 surface: &NurbSurface3D<S>,
74 t: S,
75) -> GeopResult<Vector3<S>> {
76 let ctx = |e: GeopError| {
77 let (t0, t1) = coedge.pcurve.domain();
78 e.with_context(format!(
79 "coedge_tangent_3d(t={t:?}): pcurve domain=({t0:?}, {t1:?}), pcurve degree={}, pcurve knot_vector={:?}, pcurve control_points={:?}",
80 coedge.pcurve.degree, coedge.pcurve.knot_vector, coedge.pcurve.control_points
81 ))
82 };
83
84 let uv = coedge.pcurve.evaluate(t).with_context(&ctx)?;
85 let (ds_du, ds_dv) = surface.derivatives(uv[0], uv[1]).with_context(&ctx)?;
86 let d_uv = coedge.pcurve.tangent(t).with_context(&ctx)?;
87 Ok(ds_du.prod_scalar(d_uv[0]).add(&ds_dv.prod_scalar(d_uv[1])))
88}
89
90fn coedge_inset_point<S: Scalar>(
96 coedge: &Coedge<S>,
97 surface: &NurbSurface3D<S>,
98 t: S,
99 inset: S,
100) -> GeopResult<Vector3<S>> {
101 let uv = coedge.pcurve.evaluate(t)?;
102 let point = surface.evaluate(uv[0], uv[1])?;
103 let Ok(tangent) = coedge_tangent_3d(coedge, surface, t)?.normalize() else {
104 return Ok(point);
105 };
106 let Ok(normal) = surface.normal(uv[0], uv[1]) else {
107 return Ok(point);
108 };
109 let Ok(offset) = normal.prod_cross(&tangent).normalize() else {
110 return Ok(point);
111 };
112 Ok(point.add(&offset.prod_scalar(inset)))
113}
114
115fn try_tangent<S: Scalar>(
119 coedge: &Coedge<S>,
120 surface: &NurbSurface3D<S>,
121 t: S,
122) -> GeopResult<Option<Vector3<S>>> {
123 Ok(coedge_tangent_3d(coedge, surface, t)?.normalize().ok())
124}
125
126#[allow(clippy::too_many_arguments)]
137fn miter_trim_delta_t<S: Scalar>(
138 coedge: &Coedge<S>,
139 surface: &NurbSurface3D<S>,
140 t_this: S,
141 away_this: Vector3<S>,
142 neighbor: &Coedge<S>,
143 neighbor_surface: &NurbSurface3D<S>,
144 t_neighbor: S,
145 away_neighbor: Vector3<S>,
146 inset: S,
147) -> GeopResult<S> {
148 let speed = coedge_tangent_3d(coedge, surface, t_this)?.norm();
149 if speed.could_be_equal(S::ZERO) {
150 return Ok(S::ZERO);
151 }
152 let _ = (neighbor, neighbor_surface, t_neighbor);
156
157 let cos_theta = away_this.prod_dot(&away_neighbor).to_f64().clamp(-1.0, 1.0);
158 let theta = cos_theta.acos();
159 let half_tan = (theta / 2.0).tan();
160 if half_tan.abs() < 1e-6 {
161 return Ok(S::from_f64(f64::MAX));
164 }
165 let setback = S::from_f64(inset.to_f64() / half_tan);
166 setback.div(speed)
167}
168
169fn add_direction_arrow<S: Scalar>(
172 scene: &mut PrimitiveScene<S>,
173 tip: Vector3<S>,
174 dir: Vector3<S>,
175 size: S,
176 color: Color10,
177) -> GeopResult<()> {
178 let up = Vector3::from_array([S::ZERO, S::ZERO, S::ONE]);
179 let raw = dir.prod_cross(&up);
180 let perp = if raw.norm_sq().could_be_equal(S::ZERO) {
181 dir.prod_cross(&Vector3::from_array([S::ONE, S::ZERO, S::ZERO]))
182 .normalize()?
183 } else {
184 raw.normalize()?
185 };
186
187 let half = S::from_f64(0.5);
188 let back = tip.sub(&dir.prod_scalar(size));
189 let left = back.add(&perp.prod_scalar(size.mul(half)));
190 let right = back.sub(&perp.prod_scalar(size.mul(half)));
191
192 if let Ok(l) = Line::try_new(left, tip) {
193 scene.add_line(l, color);
194 }
195 if let Ok(l) = Line::try_new(right, tip) {
196 scene.add_line(l, color);
197 }
198 Ok(())
199}
200
201fn add_arrow<S: Scalar>(
205 scene: &mut PrimitiveScene<S>,
206 base: Vector3<S>,
207 dir: Vector3<S>,
208 length: S,
209 color: Color10,
210) -> GeopResult<()> {
211 let tip = base.add(&dir.prod_scalar(length));
212 if let Ok(l) = Line::try_new(base, tip) {
213 scene.add_line(l, color);
214 }
215 add_direction_arrow(
216 scene,
217 tip,
218 dir,
219 length.mul(S::from_f64(ARROW_SIZE_FRACTION)),
220 color,
221 )
222}
223
224fn add_coordinate_system<S: Scalar>(
229 scene: &mut PrimitiveScene<S>,
230 model: &Model<S>,
231) -> GeopResult<()> {
232 let axis_length = model
233 .vertices
234 .values()
235 .flat_map(|v| {
236 [
237 v.point[0].to_f64().abs(),
238 v.point[1].to_f64().abs(),
239 v.point[2].to_f64().abs(),
240 ]
241 })
242 .fold(1.0_f64, f64::max);
243
244 let origin = Vector3::from_array([S::ZERO, S::ZERO, S::ZERO]);
245 let axes: [(Vector3<S>, Vector3<S>, Color10); 3] = [
248 (
249 Vector3::from_array([S::from_f64(axis_length), S::ZERO, S::ZERO]),
250 Vector3::from_array([S::ZERO, S::ZERO, S::ONE]),
251 AXIS_X_COLOR,
252 ),
253 (
254 Vector3::from_array([S::ZERO, S::from_f64(axis_length), S::ZERO]),
255 Vector3::from_array([S::ONE, S::ZERO, S::ZERO]),
256 AXIS_Y_COLOR,
257 ),
258 (
259 Vector3::from_array([S::ZERO, S::ZERO, S::from_f64(axis_length)]),
260 Vector3::from_array([S::ZERO, S::ONE, S::ZERO]),
261 AXIS_Z_COLOR,
262 ),
263 ];
264
265 for (end, tick_dir, color) in axes {
266 scene.add_cylinder(origin, end, AXIS_RADIUS, color);
267
268 let dir = end.normalize()?;
269 let half_tick = S::from_f64(AXIS_STEP_TICK_LENGTH / 2.0);
270 let steps = (axis_length / AXIS_STEP).floor() as usize;
271 for step in 1..=steps {
272 let center = dir.prod_scalar(S::from_f64(step as f64 * AXIS_STEP));
273 let tick_start = center.sub(&tick_dir.prod_scalar(half_tick));
274 let tick_end = center.add(&tick_dir.prod_scalar(half_tick));
275 scene.add_cylinder(tick_start, tick_end, AXIS_RADIUS, color);
276 }
277 }
278 Ok(())
279}
280
281pub fn rasterize_topology<S: Scalar>(model: &Model<S>, n: usize) -> GeopResult<PrimitiveScene<S>> {
285 rasterize_topology_inner(model, n)
286 .with_context(&|e: GeopError| e.with_context(format!("rasterize_topology(n={n})")))
287}
288
289fn rasterize_topology_inner<S: Scalar>(
290 model: &Model<S>,
291 n: usize,
292) -> GeopResult<PrimitiveScene<S>> {
293 let mut scene = PrimitiveScene::new();
294
295 add_coordinate_system(&mut scene, model)?;
296
297 for (&id, vertex) in &model.vertices {
299 scene.add_point(vertex.point, VERTEX_COLOR);
300 scene.add_label(vertex.point, format!("V{}", id.0), VERTEX_COLOR);
301 }
302
303 for (&id, edge) in &model.edges {
305 let edge_ctx = |e: GeopError| {
306 let (t0, t1) = edge.curve.domain();
307 e.with_context(format!(
308 "rasterize_topology: edge {id}, domain=({t0:?}, {t1:?}), degree={}, knot_vector={:?}",
309 edge.curve.degree, edge.curve.knot_vector
310 ))
311 };
312
313 let (t0, t1) = edge.curve.domain();
314 scene
315 .add_curve(&edge.curve, t0, t1, EDGE_COLOR, n)
316 .with_context(&edge_ctx)?;
317
318 let length = edge
319 .curve
320 .evaluate(t1)
321 .with_context(&edge_ctx)?
322 .sub(&edge.curve.evaluate(t0).with_context(&edge_ctx)?)
323 .norm();
324 let marker_size = length.mul(S::from_f64(ARROW_SIZE_FRACTION));
325 for tenth in 1..10 {
326 let frac = S::from_f64(tenth as f64 / 10.0);
327 let t = t0.add(t1.sub(t0).mul(frac));
328 let tenth_ctx =
329 |e: GeopError| e.with_context(format!("tenth={tenth}, frac={frac:?}, t={t:?}"));
330 let p = edge
331 .curve
332 .evaluate(t)
333 .with_context(&edge_ctx)
334 .with_context(&tenth_ctx)?;
335 if let Ok(dir) = edge
336 .curve
337 .tangent(t)
338 .with_context(&edge_ctx)
339 .with_context(&tenth_ctx)?
340 .normalize()
341 {
342 add_direction_arrow(&mut scene, p, dir, marker_size, EDGE_MARKER_COLOR)?;
343 }
344 }
345
346 let mid = t0.add(t1.sub(t0).mul(S::from_f64(0.5)));
347 let mid_point = edge.curve.evaluate(mid).with_context(&edge_ctx)?;
348 if let Ok(dir) = edge.curve.tangent(mid).with_context(&edge_ctx)?.normalize() {
349 add_direction_arrow(&mut scene, mid_point, dir, marker_size, EDGE_COLOR)?;
350 }
351 scene.add_label(mid_point, format!("E{}", id.0), EDGE_COLOR);
352 }
353
354 let mut hole_coedges: std::collections::HashSet<CoedgeId> = std::collections::HashSet::new();
360 let cap = model.coedges.len() + 1;
361 for face in model.faces.values() {
362 for hole in &face.holes {
363 if let BoundaryType::Loop(anchor) = hole {
364 hole_coedges.extend(model.iterate_loop_coedges(*anchor).take(cap));
365 }
366 }
367 }
368
369 for (&id, coedge) in &model.coedges {
372 let Some(face) = model.faces.get(&coedge.face) else {
373 continue;
374 };
375 let surface = &face.surface;
376
377 let (coedge_color, marker_color) = if hole_coedges.contains(&id) {
378 (COEDGE_HOLE_COLOR, COEDGE_HOLE_MARKER_COLOR)
379 } else {
380 (COEDGE_OUTER_COLOR, COEDGE_OUTER_MARKER_COLOR)
381 };
382
383 (|| -> GeopResult<()> {
384 let (t0, t1) = coedge.pcurve.domain();
385 let inset = S::from_f64(COEDGE_INSET);
386 let max_trim = t1.sub(t0).mul(S::from_f64(MAX_TRIM_FRACTION));
387
388 let (t0_trim, t1_trim) = {
392 let neg_one = S::from_f64(-1.0);
393 let prev = model.get_coedge(coedge.prev)?;
394 let away_this_start = try_tangent(coedge, surface, t0)?;
395 let away_prev = try_tangent(prev, surface, prev.pcurve.domain().1)?
396 .map(|t| t.prod_scalar(neg_one));
397 let delta_start = match (away_this_start, away_prev) {
398 (Some(away_this_start), Some(away_prev)) => miter_trim_delta_t(
399 coedge,
400 surface,
401 t0,
402 away_this_start,
403 prev,
404 surface,
405 prev.pcurve.domain().1,
406 away_prev,
407 inset,
408 )?,
409 _ => S::ZERO,
412 };
413
414 let next = model.get_coedge(coedge.next)?;
415 let away_this_end =
416 try_tangent(coedge, surface, t1)?.map(|t| t.prod_scalar(neg_one));
417 let away_next = try_tangent(next, surface, next.pcurve.domain().0)?;
418 let delta_end = match (away_this_end, away_next) {
419 (Some(away_this_end), Some(away_next)) => miter_trim_delta_t(
420 coedge,
421 surface,
422 t1,
423 away_this_end,
424 next,
425 surface,
426 next.pcurve.domain().0,
427 away_next,
428 inset,
429 )?,
430 _ => S::ZERO,
431 };
432
433 let delta_start = if delta_start.definitely_greater(max_trim) {
434 max_trim
435 } else {
436 delta_start
437 };
438 let delta_end = if delta_end.definitely_greater(max_trim) {
439 max_trim
440 } else {
441 delta_end
442 };
443 (t0.add(delta_start), t1.sub(delta_end))
444 };
445
446 let mut trim_points = Vec::with_capacity(n);
447 for i in 0..n {
448 let frac = S::from_ratio(i as i64, (n - 1) as i64)?;
449 let t = t0_trim.add(t1_trim.sub(t0_trim).mul(frac));
450 trim_points.push(coedge_inset_point(coedge, surface, t, inset)?);
451 }
452 scene.add_polyline(&trim_points, coedge_color);
453
454 let length = trim_points
455 .last()
456 .unwrap()
457 .sub(trim_points.first().unwrap())
458 .norm();
459 let marker_size = length.mul(S::from_f64(ARROW_SIZE_FRACTION));
460 for tenth in 1..10 {
461 let frac = S::from_f64(tenth as f64 / 10.0);
462 let t = t0.add(t1.sub(t0).mul(frac));
463 let p = coedge_inset_point(coedge, surface, t, inset)?;
464 if let Ok(dir) = coedge_tangent_3d(coedge, surface, t)?.normalize() {
465 add_direction_arrow(&mut scene, p, dir, marker_size, marker_color)?;
466 }
467 }
468
469 let mid_t = t0_trim.add(t1_trim.sub(t0_trim).mul(S::from_f64(0.5)));
470 let mid_point = coedge_inset_point(coedge, surface, mid_t, inset)?;
471 if let Ok(dir) = coedge_tangent_3d(coedge, surface, mid_t)?.normalize() {
472 add_direction_arrow(&mut scene, mid_point, dir, marker_size, coedge_color)?;
473 }
474 scene.add_label(mid_point, format!("C{}", id.0), coedge_color);
475 Ok(())
476 })()
477 .with_context(&|e: GeopError| {
478 let (t0, t1) = coedge.pcurve.domain();
479 e.with_context(format!(
480 "rasterize_topology: coedge {id}, pcurve domain=({t0:?}, {t1:?}), pcurve degree={}, pcurve knot_vector={:?}, pcurve control_points={:?}",
481 coedge.pcurve.degree, coedge.pcurve.knot_vector, coedge.pcurve.control_points
482 ))
483 })?;
484 }
485
486 for (&id, face) in &model.faces {
488 let has_loop = face
489 .boundaries()
490 .any(|b| matches!(b, geop_core_topology::boundary::BoundaryType::Loop(_)));
491 if !has_loop {
492 continue;
493 }
494
495 for (uv_a, uv_b, uv_c) in super::face_triangles_uv(model, face, n)? {
496 let a = face.surface.evaluate(uv_a[0], uv_a[1])?;
497 let b = face.surface.evaluate(uv_b[0], uv_b[1])?;
498 let c = face.surface.evaluate(uv_c[0], uv_c[1])?;
499 if let Ok(t) = TriangleFace::try_new(a, b, c) {
500 scene.add_triangle_transparent(t, FACE_COLOR, FACE_OPACITY);
501 }
502 }
503
504 let (u0, u1) = face.surface.domain_u();
505 let (v0, v1) = face.surface.domain_v();
506 let mid_u = u0.add(u1.sub(u0).mul(S::from_f64(0.5)));
507 let mid_v = v0.add(v1.sub(v0).mul(S::from_f64(0.5)));
508 if let Ok(label_point) = face.surface.evaluate(mid_u, mid_v) {
509 scene.add_label(label_point, format!("F{}", id.0), FACE_COLOR);
510 if let Ok(normal) = face.surface.normal(mid_u, mid_v) {
511 add_arrow(
512 &mut scene,
513 label_point,
514 normal,
515 S::from_f64(FACE_NORMAL_LENGTH),
516 FACE_NORMAL_COLOR,
517 )?;
518 }
519 }
520 }
521
522 Ok(scene)
523}