1use crate::style::parse_css_hex;
55use crate::view::{Projection, ViewCamera};
56use brep_gizmos::datum::{DatumAxis, DatumPlane};
57use brep_gizmos::transform::{DragDelta, TransformGizmo};
58use brep_gizmos::view_cube::ViewCube;
59use brep_gizmos::{
60 curve_display, datum, dimension, Gizmo, GizmoCamera, HandleId, LineVertex, Overlay, TriVertex,
61 Vec3,
62};
63use serde_json::Value;
64
65pub fn gizmo_camera(view: &ViewCamera) -> GizmoCamera {
71 let resolved = view.resolve();
72 let (_, up, forward) = view.basis();
76 GizmoCamera {
77 view_proj: resolved.view_proj,
78 eye: v3f(view.eye),
79 forward: v3f(forward),
80 up: v3f(up),
81 viewport: [view.width as f32, view.height as f32],
82 orthographic: matches!(view.projection, Projection::Orthographic { .. }),
83 }
84}
85
86fn v3f(a: [f64; 3]) -> Vec3 {
87 Vec3::new(a[0] as f32, a[1] as f32, a[2] as f32)
88}
89
90fn vec3_of(v: &Value) -> Option<Vec3> {
91 let a = v.as_array()?;
92 Some(Vec3::new(
93 a.first()?.as_f64()? as f32,
94 a.get(1)?.as_f64()? as f32,
95 a.get(2)?.as_f64()? as f32,
96 ))
97}
98
99fn color_of(v: Option<&Value>, default: [f32; 4]) -> [f32; 4] {
100 match v.and_then(|v| v.as_str()).and_then(parse_css_hex) {
101 Some(rgb) => [rgb[0], rgb[1], rgb[2], 1.0],
102 None => default,
103 }
104}
105
106fn flag(v: &Value, key: &str) -> bool {
107 v.get(key).and_then(|b| b.as_bool()).unwrap_or(false)
108}
109
110fn f32_array(v: Option<&Value>) -> Vec<f32> {
112 v.and_then(|v| v.as_array())
113 .map(|a| a.iter().filter_map(|x| x.as_f64().map(|f| f as f32)).collect())
114 .unwrap_or_default()
115}
116
117fn rgb_at(colors: &[f32], i: usize) -> [f32; 4] {
120 let base = i * 3;
121 if base + 2 < colors.len() {
122 [colors[base], colors[base + 1], colors[base + 2], 1.0]
123 } else if colors.len() >= 3 {
124 let n = colors.len();
125 [colors[n - 3], colors[n - 2], colors[n - 1], 1.0]
126 } else {
127 [1.0, 1.0, 1.0, 1.0]
128 }
129}
130
131fn pos_at(positions: &[f32], i: usize) -> Vec3 {
133 let base = i * 3;
134 if base + 2 < positions.len() {
135 Vec3::new(positions[base], positions[base + 1], positions[base + 2])
136 } else {
137 Vec3::ZERO
138 }
139}
140
141fn push_point_quad(ov: &mut Overlay, center: Vec3, color: [f32; 4], size_px: f32, cam: &GizmoCamera) {
144 let half = 0.5 * size_px.max(1.0) * cam.world_per_pixel(center);
145 let right = cam.screen_right(center);
146 let up = right.cross(cam.forward).normalized();
147 let rx = right.scale(half);
148 let uy = up.scale(half);
149 let normal: [f32; 3] = cam.forward.scale(-1.0).into();
150 let a = center.sub(rx).sub(uy);
151 let b = center.add(rx).sub(uy);
152 let c = center.add(rx).add(uy);
153 let d = center.sub(rx).add(uy);
154 for p in [a, b, c, a, c, d] {
155 ov.tris.push(TriVertex { pos: p.into(), normal, color });
156 }
157}
158
159fn brighten(c: [f32; 4]) -> [f32; 4] {
162 [
163 (c[0] * 1.35 + 0.1).min(1.0),
164 (c[1] * 1.35 + 0.1).min(1.0),
165 (c[2] * 1.35 + 0.1).min(1.0),
166 c[3],
167 ]
168}
169
170const DEFAULT_AXIS_COLOR: [f32; 4] = [0.72, 0.74, 0.80, 1.0];
171
172struct PlaneW {
175 name: String,
176 origin: Vec3,
177 x: Vec3,
178 y: Vec3,
179 size: Option<f32>,
180 color: [f32; 4],
181 hot: bool,
182}
183
184struct AxisW {
185 name: String,
186 point: Vec3,
187 direction: Vec3,
188 length: f32,
189 color: [f32; 4],
190 hot: bool,
191}
192
193struct FrameW {
194 origin: Vec3,
195 x: Vec3,
196 y: Vec3,
197 z: Vec3,
198 px: f32,
199}
200
201struct CurveW {
202 points: Vec<Vec3>,
203 closed: bool,
204 color: [f32; 4],
205}
206
207struct OverlayPointW {
210 center: Vec3,
211 color: [f32; 4],
212}
213
214struct OverlayGroupW {
220 name: String,
221 render_order: i32,
222 tris: Vec<TriVertex>,
223 lines: Vec<LineVertex>,
224 points: Vec<OverlayPointW>,
225 point_size: f32,
226}
227
228const DEFAULT_OVERLAY_POINT_PX: f32 = 6.0;
230
231impl OverlayGroupW {
232 fn from_json(g: &Value) -> Self {
237 let name = g.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
238 let render_order = g.get("renderOrder").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
239
240 let mut tris = Vec::new();
243 if let Some(t) = g.get("tris") {
244 let positions = f32_array(t.get("positions"));
245 let colors = f32_array(t.get("colors"));
246 let normals = f32_array(t.get("normals"));
247 let vcount = positions.len() / 3;
248 let tri_count = vcount / 3;
249 for tri in 0..tri_count {
250 let vi = [tri * 3, tri * 3 + 1, tri * 3 + 2];
251 let p = [pos_at(&positions, vi[0]), pos_at(&positions, vi[1]), pos_at(&positions, vi[2])];
252 let face_n = p[1].sub(p[0]).cross(p[2].sub(p[0])).normalized();
253 for k in 0..3 {
254 let idx = vi[k];
255 let normal: [f32; 3] = if normals.len() >= (idx + 1) * 3 {
256 [normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]]
257 } else {
258 face_n.into()
259 };
260 tris.push(TriVertex {
261 pos: p[k].into(),
262 normal,
263 color: rgb_at(&colors, idx),
264 });
265 }
266 }
267 }
268
269 let mut lines = Vec::new();
272 if let Some(l) = g.get("lines") {
273 let positions = f32_array(l.get("positions"));
274 let colors = f32_array(l.get("colors"));
275 let vcount = positions.len() / 3;
276 let seg_count = vcount / 2;
277 for seg in 0..seg_count {
278 let a = seg * 2;
279 let b = seg * 2 + 1;
280 lines.push(LineVertex { pos: pos_at(&positions, a).into(), color: rgb_at(&colors, a) });
281 lines.push(LineVertex { pos: pos_at(&positions, b).into(), color: rgb_at(&colors, b) });
282 }
283 }
284
285 let mut points = Vec::new();
287 let mut point_size = DEFAULT_OVERLAY_POINT_PX;
288 if let Some(pt) = g.get("points") {
289 let positions = f32_array(pt.get("positions"));
290 let colors = f32_array(pt.get("colors"));
291 point_size = pt.get("size").and_then(|v| v.as_f64()).unwrap_or(DEFAULT_OVERLAY_POINT_PX as f64) as f32;
292 let count = positions.len() / 3;
293 for i in 0..count {
294 points.push(OverlayPointW { center: pos_at(&positions, i), color: rgb_at(&colors, i) });
295 }
296 }
297
298 Self { name, render_order, tris, lines, points, point_size }
299 }
300
301 fn is_empty(&self) -> bool {
302 self.tris.is_empty() && self.lines.is_empty() && self.points.is_empty()
303 }
304}
305
306enum DimW {
307 Linear {
308 id: String,
309 a: Vec3,
310 b: Vec3,
311 offset_dir: Vec3,
312 offset: f32,
313 color: [f32; 4],
314 },
315 Angular {
316 id: String,
317 vertex: Vec3,
318 dir_a: Vec3,
319 dir_b: Vec3,
320 radius: f32,
321 color: [f32; 4],
322 },
323 Radial {
324 id: String,
325 center: Vec3,
326 point: Vec3,
327 color: [f32; 4],
328 },
329}
330
331pub struct WidgetRegistry {
333 pub viewcube_enabled: bool,
336 viewcube: ViewCube,
337 viewcube_hover: Option<HandleId>,
338 planes: Vec<PlaneW>,
339 axes: Vec<AxisW>,
340 frames: Vec<FrameW>,
341 curves: Vec<CurveW>,
342 dims: Vec<DimW>,
343 overlay_groups: Vec<OverlayGroupW>,
346 transform: Option<TransformGizmo>,
347 transform_hover: Option<HandleId>,
348 transform_active: Option<HandleId>,
349}
350
351impl Default for WidgetRegistry {
352 fn default() -> Self {
353 Self {
354 viewcube_enabled: false,
355 viewcube: ViewCube::new(),
356 viewcube_hover: None,
357 planes: Vec::new(),
358 axes: Vec::new(),
359 frames: Vec::new(),
360 curves: Vec::new(),
361 dims: Vec::new(),
362 overlay_groups: Vec::new(),
363 transform: None,
364 transform_hover: None,
365 transform_active: None,
366 }
367 }
368}
369
370pub struct ViewCubeFrame {
373 pub overlay: Overlay,
374 pub view_proj: [[f32; 4]; 4],
376 pub forward: [f32; 3],
377 pub rect_css: [f32; 4],
379}
380
381pub struct WidgetOverlay {
386 pub main: Overlay,
387 pub plane_tri_verts: usize,
391 pub viewcube: Option<ViewCubeFrame>,
392}
393
394impl WidgetOverlay {
395 pub fn is_empty(&self) -> bool {
397 self.main.lines.is_empty() && self.main.tris.is_empty() && self.viewcube.is_none()
398 }
399
400 pub fn world_bbox(&self) -> crate::camera::Aabb {
412 let mut bbox = crate::camera::Aabb::empty();
413 let mut fold = |pos: &[f32; 3]| {
414 if pos.iter().any(|c| c.abs() > 1.0e6) {
415 return;
416 }
417 bbox.expand([pos[0] as f64, pos[1] as f64, pos[2] as f64]);
418 };
419 for v in &self.main.tris {
420 fold(&v.pos);
421 }
422 for v in &self.main.lines {
423 fold(&v.pos);
424 }
425 bbox
426 }
427}
428
429impl WidgetRegistry {
430 pub fn new() -> Self {
431 Self::default()
432 }
433
434 pub fn has_main_overlay(&self) -> bool {
437 !self.planes.is_empty()
438 || !self.axes.is_empty()
439 || !self.frames.is_empty()
440 || !self.curves.is_empty()
441 || !self.dims.is_empty()
442 || !self.overlay_groups.is_empty()
443 || self.transform.is_some()
444 }
445
446 pub fn overlay_groups_bbox(&self) -> crate::camera::Aabb {
453 let mut bbox = crate::camera::Aabb::empty();
454 for group in &self.overlay_groups {
455 for v in &group.tris {
456 bbox.expand([v.pos[0] as f64, v.pos[1] as f64, v.pos[2] as f64]);
457 }
458 for v in &group.lines {
459 bbox.expand([v.pos[0] as f64, v.pos[1] as f64, v.pos[2] as f64]);
460 }
461 for point in &group.points {
462 let c: [f32; 3] = point.center.into();
463 bbox.expand([c[0] as f64, c[1] as f64, c[2] as f64]);
464 }
465 }
466 bbox
467 }
468
469 pub fn set_datums_json(&mut self, json: &str) -> Result<(), String> {
477 let value: Value =
478 serde_json::from_str(json).map_err(|e| format!("datums parse: {e}"))?;
479 self.planes.clear();
480 self.axes.clear();
481 self.frames.clear();
482 self.curves.clear();
483
484 if let Some(list) = value.get("planes").and_then(|v| v.as_array()) {
485 for p in list {
486 let (Some(origin), Some(x), Some(y)) = (
487 p.get("origin").and_then(vec3_of),
488 p.get("x").and_then(vec3_of),
489 p.get("y").and_then(vec3_of),
490 ) else {
491 continue;
492 };
493 self.planes.push(PlaneW {
494 name: p.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
495 origin,
496 x,
497 y,
498 size: p.get("size").and_then(|v| v.as_f64()).map(|s| s as f32),
499 color: color_of(p.get("color"), datum::PLANE_COLOR),
500 hot: flag(p, "selected") || flag(p, "hovered"),
501 });
502 }
503 }
504 if let Some(list) = value.get("axes").and_then(|v| v.as_array()) {
505 for a in list {
506 let (Some(point), Some(direction)) = (
507 a.get("point").and_then(vec3_of),
508 a.get("direction").and_then(vec3_of),
509 ) else {
510 continue;
511 };
512 self.axes.push(AxisW {
513 name: a.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
514 point,
515 direction,
516 length: a.get("length").and_then(|v| v.as_f64()).unwrap_or(10.0) as f32,
517 color: color_of(a.get("color"), DEFAULT_AXIS_COLOR),
518 hot: flag(a, "selected") || flag(a, "hovered"),
519 });
520 }
521 }
522 if let Some(list) = value.get("frames").and_then(|v| v.as_array()) {
523 for f in list {
524 let (Some(origin), Some(x), Some(y), Some(z)) = (
525 f.get("origin").and_then(vec3_of),
526 f.get("x").and_then(vec3_of),
527 f.get("y").and_then(vec3_of),
528 f.get("z").and_then(vec3_of),
529 ) else {
530 continue;
531 };
532 self.frames.push(FrameW {
533 origin,
534 x,
535 y,
536 z,
537 px: f.get("px").and_then(|v| v.as_f64()).unwrap_or(datum::DEFAULT_FRAME_PX as f64)
538 as f32,
539 });
540 }
541 }
542 if let Some(list) = value.get("curves").and_then(|v| v.as_array()) {
543 for c in list {
544 let points: Vec<Vec3> = c
545 .get("points")
546 .and_then(|v| v.as_array())
547 .map(|arr| arr.iter().filter_map(vec3_of).collect())
548 .unwrap_or_default();
549 if points.len() < 2 {
550 continue;
551 }
552 let mut color = color_of(c.get("color"), curve_display::CURVE_COLOR);
553 if flag(c, "selected") || flag(c, "hovered") {
554 color = brighten(color);
555 }
556 self.curves.push(CurveW {
557 points,
558 closed: flag(c, "closed"),
559 color,
560 });
561 }
562 }
563 Ok(())
564 }
565
566 pub fn set_overlay_json(&mut self, json: &str) -> Result<(), String> {
572 let value: Value =
573 serde_json::from_str(json).map_err(|e| format!("overlay parse: {e}"))?;
574 let Some(list) = value.get("groups").and_then(|v| v.as_array()) else {
575 self.overlay_groups.clear();
576 return Ok(());
577 };
578 if list.is_empty() {
579 self.overlay_groups.clear();
580 return Ok(());
581 }
582 for g in list {
583 let group = OverlayGroupW::from_json(g);
584 self.overlay_groups.retain(|x| x.name != group.name);
586 if !group.is_empty() {
587 self.overlay_groups.push(group);
588 }
589 }
590 Ok(())
591 }
592
593 pub fn overlay_group_names(&self) -> Vec<&str> {
597 self.overlay_groups.iter().map(|g| g.name.as_str()).collect()
598 }
599
600 pub fn datum_plane_names(&self) -> Vec<(&str, bool)> {
605 self.planes.iter().map(|p| (p.name.as_str(), p.hot)).collect()
606 }
607
608 pub fn set_dimensions_json(&mut self, json: &str) -> Result<(), String> {
612 let value: Value =
613 serde_json::from_str(json).map_err(|e| format!("dimensions parse: {e}"))?;
614 self.dims.clear();
615 let Some(list) = value.as_array() else {
616 return Ok(());
617 };
618 for d in list {
619 let id = d.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
620 let color = color_of(d.get("color"), dimension::DIMENSION_COLOR);
621 match d.get("type").and_then(|v| v.as_str()) {
622 Some("linear") => {
623 if let (Some(a), Some(b), Some(offset_dir)) = (
624 d.get("a").and_then(vec3_of),
625 d.get("b").and_then(vec3_of),
626 d.get("offsetDir").and_then(vec3_of),
627 ) {
628 self.dims.push(DimW::Linear {
629 id,
630 a,
631 b,
632 offset_dir,
633 offset: d.get("offset").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
634 color,
635 });
636 }
637 }
638 Some("angular") => {
639 if let (Some(vertex), Some(dir_a), Some(dir_b)) = (
640 d.get("vertex").and_then(vec3_of),
641 d.get("dirA").and_then(vec3_of),
642 d.get("dirB").and_then(vec3_of),
643 ) {
644 self.dims.push(DimW::Angular {
645 id,
646 vertex,
647 dir_a,
648 dir_b,
649 radius: d.get("radius").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
650 color,
651 });
652 }
653 }
654 Some("radial") => {
655 if let (Some(center), Some(point)) = (
656 d.get("center").and_then(vec3_of),
657 d.get("pointOnCircle").and_then(vec3_of),
658 ) {
659 self.dims.push(DimW::Radial {
660 id,
661 center,
662 point,
663 color,
664 });
665 }
666 }
667 _ => {}
668 }
669 }
670 Ok(())
671 }
672
673 pub fn set_transform_json(&mut self, json: &str) -> Result<(), String> {
678 let value: Value =
679 serde_json::from_str(json).map_err(|e| format!("transform parse: {e}"))?;
680 if value.is_null() {
681 self.transform = None;
682 self.transform_hover = None;
683 self.transform_active = None;
684 return Ok(());
685 }
686 let origin = value.get("origin").and_then(vec3_of).unwrap_or(Vec3::ZERO);
687 let x = value.get("x").and_then(vec3_of).unwrap_or(Vec3::X);
688 let y = value.get("y").and_then(vec3_of).unwrap_or(Vec3::Y);
689 let z = value.get("z").and_then(vec3_of).unwrap_or(Vec3::Z);
690 let mut gz = TransformGizmo::default();
691 gz.set_frame(origin, x, y, z);
692 let flag = |key: &str| value.get(key).and_then(|v| v.as_bool()).unwrap_or(true);
693 gz.show_center = flag("showCenter");
694 gz.show_axes = flag("showAxes");
695 gz.show_rings = flag("showRings");
696 self.transform = Some(gz);
697 Ok(())
698 }
699
700 pub fn set_viewcube_enabled(&mut self, enabled: bool) {
701 self.viewcube_enabled = enabled;
702 if !enabled {
703 self.viewcube_hover = None;
704 }
705 }
706
707 pub fn set_viewcube_size(&mut self, size_px: f32) {
714 if size_px.is_finite() {
715 self.viewcube.size = size_px.max(1.0);
716 }
717 }
718
719 pub fn build_main_overlay(&self, cam: &GizmoCamera) -> (Overlay, usize) {
727 let mut ov = Overlay::new();
728
729 for p in &self.planes {
730 let color = if p.hot { brighten(p.color) } else { p.color };
731 let plane = match p.size {
732 Some(size) => datum::datum_plane(p.origin, p.x, p.y, size, color),
733 None => datum::datum_plane_screen(
734 p.origin,
735 p.x,
736 p.y,
737 datum::DEFAULT_PLANE_SCREEN_PX,
738 color,
739 cam,
740 ),
741 };
742 ov.extend(&plane);
743 }
744 let plane_tri_verts = ov.tris.len();
747 for a in &self.axes {
748 let color = if a.hot { brighten(a.color) } else { a.color };
749 ov.extend(&datum::datum_axis(a.point, a.direction, a.length, color));
750 }
751 for f in &self.frames {
752 ov.extend(&datum::datum_frame(f.origin, f.x, f.y, f.z, f.px, cam));
753 }
754 for c in &self.curves {
755 ov.extend(&curve_display::polyline_display(&c.points, c.color, c.closed));
756 }
757 for d in &self.dims {
758 ov.extend(&self.build_dim(d, cam).overlay);
759 }
760 if let Some(gz) = &self.transform {
761 ov.extend(&gz.geometry(cam, self.transform_hover, self.transform_active));
762 }
763 let mut order: Vec<&OverlayGroupW> = self.overlay_groups.iter().collect();
768 order.sort_by_key(|g| g.render_order);
769 for g in order {
770 ov.tris.extend_from_slice(&g.tris);
771 ov.lines.extend_from_slice(&g.lines);
772 for p in &g.points {
773 push_point_quad(&mut ov, p.center, p.color, g.point_size, cam);
774 }
775 }
776 (ov, plane_tri_verts)
777 }
778
779 fn build_dim(&self, d: &DimW, cam: &GizmoCamera) -> dimension::DimensionAnnotation {
780 match d {
781 DimW::Linear {
782 a,
783 b,
784 offset_dir,
785 offset,
786 color,
787 ..
788 } => dimension::linear_dimension_colored(*a, *b, *offset_dir, *offset, cam, *color),
789 DimW::Angular {
790 vertex,
791 dir_a,
792 dir_b,
793 radius,
794 color,
795 ..
796 } => dimension::angular_dimension_colored(*vertex, *dir_a, *dir_b, *radius, cam, *color),
797 DimW::Radial {
798 center,
799 point,
800 color,
801 ..
802 } => dimension::radial_dimension_colored(*center, *point, cam, *color),
803 }
804 }
805
806 pub fn dimension_anchors(&self, cam: &GizmoCamera) -> Vec<(String, [f32; 3])> {
809 self.dims
810 .iter()
811 .map(|d| {
812 let id = match d {
813 DimW::Linear { id, .. } | DimW::Angular { id, .. } | DimW::Radial { id, .. } => {
814 id.clone()
815 }
816 };
817 (id, self.build_dim(d, cam).label_anchor.into())
818 })
819 .collect()
820 }
821
822 pub fn any_visible(&self) -> bool {
824 self.has_main_overlay() || self.viewcube_enabled
825 }
826
827 pub fn build_overlay(&self, cam: &GizmoCamera) -> WidgetOverlay {
829 let (main, plane_tri_verts) = self.build_main_overlay(cam);
830 WidgetOverlay {
831 main,
832 plane_tri_verts,
833 viewcube: self.build_viewcube(cam),
834 }
835 }
836
837 pub fn build_viewcube(&self, cam: &GizmoCamera) -> Option<ViewCubeFrame> {
840 if !self.viewcube_enabled {
841 return None;
842 }
843 let overlay = self.viewcube.geometry(cam, self.viewcube_hover, None);
844 let mini = self.viewcube.mini_camera(cam);
845 Some(ViewCubeFrame {
846 overlay,
847 view_proj: mini.view_proj,
848 forward: mini.forward.into(),
849 rect_css: self.viewcube.sub_rect(cam.viewport),
850 })
851 }
852
853 pub fn viewcube_rect(&self, cam: &GizmoCamera) -> [f32; 4] {
858 self.viewcube.sub_rect(cam.viewport)
859 }
860
861 pub fn viewcube_hit(&self, cam: &GizmoCamera, local_x: f32, local_y: f32) -> Option<HandleId> {
863 if !self.viewcube_enabled {
864 return None;
865 }
866 self.viewcube.hit(cam, [local_x, local_y])
867 }
868
869 pub fn set_viewcube_hover(&mut self, handle: Option<HandleId>) -> bool {
872 if self.viewcube_hover != handle {
873 self.viewcube_hover = handle;
874 true
875 } else {
876 false
877 }
878 }
879
880 pub fn viewcube_target(&self, handle: HandleId) -> ([f32; 3], [f32; 3]) {
882 (
883 ViewCube::target_view(handle).into(),
884 ViewCube::target_up(handle).into(),
885 )
886 }
887
888 pub fn datum_plane_hits(&self, cam: &GizmoCamera, x: f32, y: f32) -> Vec<(String, [f32; 3])> {
906 self.planes
907 .iter()
908 .filter(|p| !p.name.is_empty())
909 .filter_map(|p| {
910 let gz = DatumPlane {
911 origin: p.origin,
912 x_axis: p.x,
913 y_axis: p.y,
914 size: p.size,
915 color: p.color,
916 handle: 1,
917 };
918 let point = gz.hit_point(cam, [x, y])?;
919 Some((p.name.clone(), [point.x, point.y, point.z]))
920 })
921 .collect()
922 }
923
924 pub fn datum_pick(&self, cam: &GizmoCamera, x: f32, y: f32) -> Option<String> {
926 for a in &self.axes {
929 let gz = DatumAxis {
930 point: a.point,
931 direction: a.direction,
932 length: a.length,
933 color: a.color,
934 handle: 1,
935 };
936 if gz.hit(cam, [x, y]).is_some() && !a.name.is_empty() {
937 return Some(a.name.clone());
938 }
939 }
940 for p in &self.planes {
941 let gz = DatumPlane {
942 origin: p.origin,
943 x_axis: p.x,
944 y_axis: p.y,
945 size: p.size,
946 color: p.color,
947 handle: 1,
948 };
949 if gz.hit(cam, [x, y]).is_some() && !p.name.is_empty() {
950 return Some(p.name.clone());
951 }
952 }
953 None
954 }
955
956 pub fn has_transform(&self) -> bool {
957 self.transform.is_some()
958 }
959
960 pub fn transform_origin(&self) -> Option<[f32; 3]> {
965 self.transform.as_ref().map(|gz| [gz.origin.x, gz.origin.y, gz.origin.z])
966 }
967
968 pub fn transform_hit(&self, cam: &GizmoCamera, x: f32, y: f32) -> HandleId {
970 self.transform
971 .as_ref()
972 .and_then(|gz| gz.hit(cam, [x, y]))
973 .unwrap_or(0)
974 }
975
976 pub fn transform_axis_seg(&self, cam: &GizmoCamera, i: usize) -> Option<(Vec3, Vec3)> {
982 self.transform.as_ref().map(|gz| gz.axis_seg(cam, i))
983 }
984
985 pub fn transform_center_grab(&self) -> Option<Vec3> {
989 self.transform.as_ref().and_then(|gz| gz.center_grab_point())
990 }
991
992 pub fn transform_ring_grabs(&self, cam: &GizmoCamera) -> Option<[Vec3; 3]> {
996 self.transform.as_ref().map(|gz| gz.ring_grab_points(cam))
997 }
998
999 pub fn transform_hit_regions(
1004 &self,
1005 cam: &GizmoCamera,
1006 ) -> Vec<(HandleId, brep_gizmos::hit_region::HitShape)> {
1007 self.transform
1008 .as_ref()
1009 .map(|gz| gz.hit_regions(cam))
1010 .unwrap_or_default()
1011 }
1012
1013 pub fn set_transform_hover(&mut self, handle: HandleId) -> bool {
1014 let handle = (handle != 0).then_some(handle);
1015 if self.transform_hover != handle {
1016 self.transform_hover = handle;
1017 true
1018 } else {
1019 false
1020 }
1021 }
1022
1023 pub fn set_transform_active(&mut self, handle: HandleId) {
1024 self.transform_active = (handle != 0).then_some(handle);
1025 }
1026
1027 pub fn transform_drag_json(
1031 &self,
1032 cam: &GizmoCamera,
1033 handle: HandleId,
1034 sx: f32,
1035 sy: f32,
1036 cx: f32,
1037 cy: f32,
1038 ) -> String {
1039 let Some(gz) = &self.transform else {
1040 return "{\"kind\":\"none\"}".to_string();
1041 };
1042 drag_delta_json(gz, cam, handle, sx, sy, cx, cy)
1043 }
1044
1045 pub fn transform_drag_json_with_frame(
1051 &self,
1052 cam: &GizmoCamera,
1053 frame_json: &str,
1054 handle: HandleId,
1055 sx: f32,
1056 sy: f32,
1057 cx: f32,
1058 cy: f32,
1059 ) -> String {
1060 let value: Value = match serde_json::from_str(frame_json) {
1061 Ok(v) => v,
1062 Err(_) => return "{\"kind\":\"none\"}".to_string(),
1063 };
1064 let origin = value.get("origin").and_then(vec3_of).unwrap_or(Vec3::ZERO);
1065 let x = value.get("x").and_then(vec3_of).unwrap_or(Vec3::X);
1066 let y = value.get("y").and_then(vec3_of).unwrap_or(Vec3::Y);
1067 let z = value.get("z").and_then(vec3_of).unwrap_or(Vec3::Z);
1068 let mut gz = TransformGizmo::default();
1069 gz.set_frame(origin, x, y, z);
1070 drag_delta_json(&gz, cam, handle, sx, sy, cx, cy)
1071 }
1072}
1073
1074fn drag_delta_json(
1077 gz: &TransformGizmo,
1078 cam: &GizmoCamera,
1079 handle: HandleId,
1080 sx: f32,
1081 sy: f32,
1082 cx: f32,
1083 cy: f32,
1084) -> String {
1085 let start = cam.ray_from_screen(sx, sy);
1086 let current = cam.ray_from_screen(cx, cy);
1087 match gz.drag_delta(cam, handle, start, current) {
1088 DragDelta::Translate(v) => {
1089 let world = gz.ex.scale(v.x).add(gz.ey.scale(v.y)).add(gz.ez.scale(v.z));
1090 serde_json::json!({
1091 "kind": "translate",
1092 "local": [v.x, v.y, v.z],
1093 "world": [world.x, world.y, world.z],
1094 })
1095 .to_string()
1096 }
1097 DragDelta::Rotate { axis_index, radians } => {
1098 let axis = gz.axis(axis_index);
1099 serde_json::json!({
1100 "kind": "rotate",
1101 "axisIndex": axis_index,
1102 "axisWorld": [axis.x, axis.y, axis.z],
1103 "radians": radians,
1104 })
1105 .to_string()
1106 }
1107 DragDelta::None => "{\"kind\":\"none\"}".to_string(),
1108 }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114 use crate::view::ViewCamera;
1115
1116 fn cam() -> ViewCamera {
1117 let mut c = ViewCamera::default();
1118 c.width = 800.0;
1119 c.height = 600.0;
1120 c.eye = [0.0, 0.0, 40.0];
1121 c.target = [0.0, 0.0, 0.0];
1122 c.up = [0.0, 1.0, 0.0];
1123 c.projection = Projection::Orthographic { half_height: 20.0 };
1124 c
1125 }
1126
1127 #[test]
1128 fn datums_build_overlay_and_pick() {
1129 let mut reg = WidgetRegistry::new();
1130 reg.set_datums_json(
1131 r#"{"planes":[{"name":"P1","origin":[0,0,0],"x":[1,0,0],"y":[0,1,0],"size":10.0}],
1132 "axes":[{"name":"A1","point":[-5,0,0],"direction":[1,0,0],"length":10.0}]}"#,
1133 )
1134 .unwrap();
1135 assert!(reg.has_main_overlay());
1136 let gc = gizmo_camera(&cam());
1137 let (ov, _) = reg.build_main_overlay(&gc);
1138 assert!(!ov.tris.is_empty(), "plane fill tris");
1139 assert!(!ov.lines.is_empty(), "plane border + axis lines");
1140 let hit = reg.datum_pick(&gc, 400.0, 300.0);
1142 assert!(hit.is_some(), "center pixel picks a datum");
1143 }
1144
1145 #[test]
1149 fn plane_tri_verts_is_the_no_depth_prefix() {
1150 let mut reg = WidgetRegistry::new();
1151 reg.set_datums_json(
1152 r#"{"planes":[{"name":"P1","origin":[0,0,0],"x":[1,0,0],"y":[0,1,0],"size":10.0}]}"#,
1153 )
1154 .unwrap();
1155 let gc = gizmo_camera(&cam());
1156 let (plane_only, verts) = reg.build_main_overlay(&gc);
1157 assert!(verts > 0, "the plane contributes tris");
1158 assert_eq!(verts, plane_only.tris.len(), "with only a plane, every tri is the plane's");
1159
1160 reg.set_dimensions_json(
1163 r#"[{"id":"d1","type":"linear","a":[0,0,0],"b":[0,0,5],"offsetDir":[0,-1,0],"offset":2.0}]"#,
1164 )
1165 .unwrap();
1166 let (with_dim, verts2) = reg.build_main_overlay(&gc);
1167 assert_eq!(verts2, verts, "the plane prefix is unchanged by adding a dimension");
1168 assert!(with_dim.tris.len() > verts, "dimension tris follow the plane prefix");
1169 }
1170
1171 #[test]
1172 fn transform_feeds_and_drags() {
1173 let mut reg = WidgetRegistry::new();
1174 reg.set_transform_json(r#"{"origin":[0,0,0],"x":[1,0,0],"y":[0,1,0],"z":[0,0,1]}"#)
1175 .unwrap();
1176 assert!(reg.has_transform());
1177 let gc = gizmo_camera(&cam());
1178 let out = reg.transform_drag_json(
1180 &gc,
1181 brep_gizmos::transform::HANDLE_AXIS_X,
1182 400.0,
1183 300.0,
1184 440.0,
1185 300.0,
1186 );
1187 let v: Value = serde_json::from_str(&out).unwrap();
1188 assert_eq!(v["kind"], "translate");
1189 assert!(v["world"][0].as_f64().unwrap() > 0.0, "world dx > 0: {out}");
1190 reg.set_transform_json("null").unwrap();
1192 assert!(!reg.has_transform());
1193 }
1194
1195 #[test]
1196 fn viewcube_frame_and_snap() {
1197 let mut reg = WidgetRegistry::new();
1198 assert!(reg.build_viewcube(&gizmo_camera(&cam())).is_none());
1199 reg.set_viewcube_enabled(true);
1200 let gc = gizmo_camera(&cam());
1201 let frame = reg.build_viewcube(&gc).expect("enabled");
1202 assert!(!frame.overlay.tris.is_empty());
1203 let [x, _y, w, h] = reg.viewcube_rect(&gc);
1204 assert!(x > 600.0 && (w - 135.0).abs() < 1e-3 && (h - 135.0).abs() < 1e-3);
1206 if let Some(handle) = reg.viewcube_hit(&gc, w / 2.0, h / 2.0) {
1209 let (dir, _up) = reg.viewcube_target(handle);
1210 assert!((dir[0].powi(2) + dir[1].powi(2) + dir[2].powi(2) - 1.0).abs() < 1e-3);
1211 }
1212 }
1213
1214 #[test]
1223 fn viewcube_orientation_matches_camera_for_standard_and_rolled_poses() {
1224 use brep_gizmos::view_cube::ViewCube;
1225 let mut poses: Vec<(String, ViewCamera)> = Vec::new();
1226 for name in ["FRONT", "TOP", "RIGHT", "ISO"] {
1227 let mut c = cam();
1228 assert!(c.standard_view(name), "{name}");
1229 poses.push((name.to_string(), c));
1230 }
1231 let mut rolled = cam();
1234 rolled.standard_view("FRONT");
1235 let (_, _, fwd) = rolled.basis();
1236 rolled.up = crate::view::rotate3(rolled.up, fwd, 30f64.to_radians());
1237 poses.push(("FRONT+30°roll".to_string(), rolled));
1238
1239 let cube = ViewCube::new();
1240 for (label, view) in &poses {
1241 let gc = gizmo_camera(view);
1242 let mini = cube.mini_camera(&gc);
1243 for (axis, world) in [
1244 ("X", [1.0f64, 0.0, 0.0]),
1245 ("Y", [0.0, 1.0, 0.0]),
1246 ("Z", [0.0, 0.0, 1.0]),
1247 ] {
1248 let (ox, oy, _) = view.project([0.0, 0.0, 0.0]);
1250 let (ax, ay, _) = view.project(world);
1251 let dm = [ax - ox, ay - oy];
1252 let c0 = mini.world_to_screen(Vec3::ZERO).expect("origin visible");
1254 let c1 = mini
1255 .world_to_screen(Vec3::new(world[0] as f32 * 0.4, world[1] as f32 * 0.4, world[2] as f32 * 0.4))
1256 .expect("axis tip visible");
1257 let dc = [(c1[0] - c0[0]) as f64, (c1[1] - c0[1]) as f64];
1258 let lm = (dm[0] * dm[0] + dm[1] * dm[1]).sqrt();
1259 let lc = (dc[0] * dc[0] + dc[1] * dc[1]).sqrt();
1260 if lm < 1e-3 || lc < 1e-3 {
1261 assert!(
1264 lm < 1.0 && lc < 1.0,
1265 "{label}/{axis}: axis vanishes in one camera only (main {lm}, cube {lc})"
1266 );
1267 continue;
1268 }
1269 let dot = (dm[0] * dc[0] + dm[1] * dc[1]) / (lm * lc);
1270 assert!(
1271 dot > 0.999,
1272 "{label}: world {axis} axis points a different way on the cube \
1273 (main dir {dm:?}, cube dir {dc:?}, dot {dot})"
1274 );
1275 }
1276 }
1277 }
1278
1279 #[test]
1287 fn viewcube_click_targets_round_trip_standard_views() {
1288 use brep_gizmos::view_cube::ViewCube;
1289 let mut reg = WidgetRegistry::new();
1290 reg.set_viewcube_enabled(true);
1291 let cube_faces = [
1292 ("FRONT", ViewCube::FRONT),
1293 ("BACK", ViewCube::BACK),
1294 ("RIGHT", ViewCube::RIGHT),
1295 ("LEFT", ViewCube::LEFT),
1296 ("TOP", ViewCube::TOP),
1297 ("BOTTOM", ViewCube::BOTTOM),
1298 ];
1299 for (name, want) in cube_faces {
1300 let mut view = cam();
1301 assert!(view.standard_view(name));
1302 let gc = gizmo_camera(&view);
1303 let [_, _, w, h] = reg.viewcube_rect(&gc);
1304 let hit = reg.viewcube_hit(&gc, w / 2.0, h / 2.0);
1305 assert_eq!(hit, Some(want), "{name}: center hit");
1306 let (dir, up) = reg.viewcube_target(want);
1308 let (_, _, fwd) = view.basis();
1309 for k in 0..3 {
1310 assert!((dir[k] as f64 - fwd[k]).abs() < 1e-6, "{name}: dir");
1311 assert!((up[k] as f64 - view.up[k]).abs() < 1e-6, "{name}: up");
1312 }
1313 }
1314 let mut view = cam();
1317 view.standard_view("TOP");
1318 let gc = gizmo_camera(&view);
1319 let [_, _, w, h] = reg.viewcube_rect(&gc);
1320 let hit = reg.viewcube_hit(&gc, w / 2.0, h * 0.66).expect("hit the cube");
1321 assert!(
1322 ViewCube::region_name(hit).contains("FRONT"),
1323 "TOP view: below-center click must reach toward +Z/FRONT, got {}",
1324 ViewCube::region_name(hit)
1325 );
1326 }
1327
1328 #[test]
1329 fn overlay_groups_build_upsert_and_clear() {
1330 let mut reg = WidgetRegistry::new();
1331 reg.set_overlay_json(
1333 r#"{"groups":[{"name":"prev","renderOrder":2,
1334 "tris":{"positions":[0,0,0, 1,0,0, 0,1,0],"colors":[1,0,0, 0,1,0, 0,0,1]},
1335 "lines":{"positions":[0,0,0, 2,2,2],"colors":[1,1,0, 1,1,0]},
1336 "points":{"positions":[3,3,3],"colors":[0,1,1],"size":8}}]}"#,
1337 )
1338 .unwrap();
1339 assert!(reg.has_main_overlay());
1340 let gc = gizmo_camera(&cam());
1341 let (ov, _) = reg.build_main_overlay(&gc);
1342 assert_eq!(ov.tris.len(), 3 + 6, "tri verts (triangle + point quad)");
1344 assert_eq!(ov.lines.len(), 2, "one line segment (a pair)");
1345 assert_eq!(ov.tris[0].color, [1.0, 0.0, 0.0, 1.0]);
1347
1348 reg.set_overlay_json(
1350 r#"{"groups":[{"name":"prev","lines":{"positions":[0,0,0, 1,1,1],"colors":[1,1,1, 1,1,1]}}]}"#,
1351 )
1352 .unwrap();
1353 let (ov, _) = reg.build_main_overlay(&gc);
1354 assert!(ov.tris.is_empty(), "upsert replaced geometry (no tris)");
1355 assert_eq!(ov.lines.len(), 2);
1356
1357 reg.set_overlay_json(r#"{"groups":[{"name":"prev"}]}"#).unwrap();
1359 assert!(!reg.has_main_overlay(), "empty group removed the only overlay");
1360
1361 reg.set_overlay_json(r#"{"groups":[{"name":"a","lines":{"positions":[0,0,0,1,0,0],"colors":[1,1,1,1,1,1]}}]}"#).unwrap();
1363 assert!(reg.has_main_overlay());
1364 reg.set_overlay_json(r#"{"groups":[]}"#).unwrap();
1365 assert!(!reg.has_main_overlay(), "empty groups cleared all");
1366 }
1367
1368 #[test]
1369 fn overlay_groups_bbox_covers_pushed_geometry() {
1370 let mut reg = WidgetRegistry::new();
1371 assert!(reg.overlay_groups_bbox().is_empty(), "no groups -> empty bbox");
1372 reg.set_overlay_json(
1374 r#"{"groups":[{"name":"sk",
1375 "lines":{"positions":[-5,-5,-5, 10,20,30],"colors":[1,1,1, 1,1,1]},
1376 "points":{"positions":[100,0,-2],"colors":[0,1,0],"size":8}}]}"#,
1377 )
1378 .unwrap();
1379 let bbox = reg.overlay_groups_bbox();
1380 assert!(bbox.min[0] <= -5.0 && bbox.min[1] <= -5.0 && bbox.min[2] <= -5.0, "min {:?}", bbox.min);
1383 assert!(bbox.max[0] >= 100.0 && bbox.max[1] >= 20.0 && bbox.max[2] >= 30.0, "max {:?}", bbox.max);
1384 }
1385
1386 #[test]
1391 fn world_bbox_excludes_viewcube() {
1392 let mut reg = WidgetRegistry::new();
1393 reg.set_viewcube_enabled(true);
1394 let overlay = reg.build_overlay(&gizmo_camera(&cam()));
1395 assert!(overlay.viewcube.is_some(), "viewcube built");
1397 assert!(
1399 overlay.world_bbox().is_empty(),
1400 "viewcube-only overlay must have an empty WORLD bbox"
1401 );
1402 }
1403
1404 #[test]
1408 fn world_bbox_covers_main_overlay_geometry() {
1409 let mut reg = WidgetRegistry::new();
1410 reg.set_datums_json(
1411 r#"{"planes":[{"name":"P1","origin":[20,0,0],"x":[1,0,0],"y":[0,1,0],"size":10.0}],
1412 "axes":[{"name":"A1","point":[-5,-5,-5],"direction":[1,0,0],"length":10.0}]}"#,
1413 )
1414 .unwrap();
1415 let overlay = reg.build_overlay(&gizmo_camera(&cam()));
1416 let bbox = overlay.world_bbox();
1417 assert!(!bbox.is_empty(), "datum geometry -> non-empty world bbox");
1418 assert!(bbox.min[0] <= -5.0, "min {:?}", bbox.min);
1420 assert!(bbox.max[0] >= 25.0, "max {:?}", bbox.max);
1421 }
1422
1423 #[test]
1424 fn dimensions_build_and_expose_anchors() {
1425 let mut reg = WidgetRegistry::new();
1426 reg.set_dimensions_json(
1427 r#"[{"id":"d1","type":"linear","a":[-3,0,0],"b":[3,0,0],"offsetDir":[0,-1,0],"offset":2.0}]"#,
1428 )
1429 .unwrap();
1430 let gc = gizmo_camera(&cam());
1431 let (ov, _) = reg.build_main_overlay(&gc);
1432 assert!(!ov.lines.is_empty() && !ov.tris.is_empty());
1433 let anchors = reg.dimension_anchors(&gc);
1434 assert_eq!(anchors.len(), 1);
1435 assert_eq!(anchors[0].0, "d1");
1436 }
1437}