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