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 viewcube: Option<ViewCubeFrame>,
388}
389
390impl WidgetOverlay {
391 pub fn is_empty(&self) -> bool {
393 self.main.lines.is_empty() && self.main.tris.is_empty() && self.viewcube.is_none()
394 }
395}
396
397impl WidgetRegistry {
398 pub fn new() -> Self {
399 Self::default()
400 }
401
402 pub fn has_main_overlay(&self) -> bool {
405 !self.planes.is_empty()
406 || !self.axes.is_empty()
407 || !self.frames.is_empty()
408 || !self.curves.is_empty()
409 || !self.dims.is_empty()
410 || !self.overlay_groups.is_empty()
411 || self.transform.is_some()
412 }
413
414 pub fn overlay_groups_bbox(&self) -> crate::camera::Aabb {
421 let mut bbox = crate::camera::Aabb::empty();
422 for group in &self.overlay_groups {
423 for v in &group.tris {
424 bbox.expand([v.pos[0] as f64, v.pos[1] as f64, v.pos[2] as f64]);
425 }
426 for v in &group.lines {
427 bbox.expand([v.pos[0] as f64, v.pos[1] as f64, v.pos[2] as f64]);
428 }
429 for point in &group.points {
430 let c: [f32; 3] = point.center.into();
431 bbox.expand([c[0] as f64, c[1] as f64, c[2] as f64]);
432 }
433 }
434 bbox
435 }
436
437 pub fn set_datums_json(&mut self, json: &str) -> Result<(), String> {
445 let value: Value =
446 serde_json::from_str(json).map_err(|e| format!("datums parse: {e}"))?;
447 self.planes.clear();
448 self.axes.clear();
449 self.frames.clear();
450 self.curves.clear();
451
452 if let Some(list) = value.get("planes").and_then(|v| v.as_array()) {
453 for p in list {
454 let (Some(origin), Some(x), Some(y)) = (
455 p.get("origin").and_then(vec3_of),
456 p.get("x").and_then(vec3_of),
457 p.get("y").and_then(vec3_of),
458 ) else {
459 continue;
460 };
461 self.planes.push(PlaneW {
462 name: p.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
463 origin,
464 x,
465 y,
466 size: p.get("size").and_then(|v| v.as_f64()).map(|s| s as f32),
467 color: color_of(p.get("color"), datum::PLANE_COLOR),
468 hot: flag(p, "selected") || flag(p, "hovered"),
469 });
470 }
471 }
472 if let Some(list) = value.get("axes").and_then(|v| v.as_array()) {
473 for a in list {
474 let (Some(point), Some(direction)) = (
475 a.get("point").and_then(vec3_of),
476 a.get("direction").and_then(vec3_of),
477 ) else {
478 continue;
479 };
480 self.axes.push(AxisW {
481 name: a.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
482 point,
483 direction,
484 length: a.get("length").and_then(|v| v.as_f64()).unwrap_or(10.0) as f32,
485 color: color_of(a.get("color"), DEFAULT_AXIS_COLOR),
486 hot: flag(a, "selected") || flag(a, "hovered"),
487 });
488 }
489 }
490 if let Some(list) = value.get("frames").and_then(|v| v.as_array()) {
491 for f in list {
492 let (Some(origin), Some(x), Some(y), Some(z)) = (
493 f.get("origin").and_then(vec3_of),
494 f.get("x").and_then(vec3_of),
495 f.get("y").and_then(vec3_of),
496 f.get("z").and_then(vec3_of),
497 ) else {
498 continue;
499 };
500 self.frames.push(FrameW {
501 origin,
502 x,
503 y,
504 z,
505 px: f.get("px").and_then(|v| v.as_f64()).unwrap_or(datum::DEFAULT_FRAME_PX as f64)
506 as f32,
507 });
508 }
509 }
510 if let Some(list) = value.get("curves").and_then(|v| v.as_array()) {
511 for c in list {
512 let points: Vec<Vec3> = c
513 .get("points")
514 .and_then(|v| v.as_array())
515 .map(|arr| arr.iter().filter_map(vec3_of).collect())
516 .unwrap_or_default();
517 if points.len() < 2 {
518 continue;
519 }
520 let mut color = color_of(c.get("color"), curve_display::CURVE_COLOR);
521 if flag(c, "selected") || flag(c, "hovered") {
522 color = brighten(color);
523 }
524 self.curves.push(CurveW {
525 points,
526 closed: flag(c, "closed"),
527 color,
528 });
529 }
530 }
531 Ok(())
532 }
533
534 pub fn set_overlay_json(&mut self, json: &str) -> Result<(), String> {
540 let value: Value =
541 serde_json::from_str(json).map_err(|e| format!("overlay parse: {e}"))?;
542 let Some(list) = value.get("groups").and_then(|v| v.as_array()) else {
543 self.overlay_groups.clear();
544 return Ok(());
545 };
546 if list.is_empty() {
547 self.overlay_groups.clear();
548 return Ok(());
549 }
550 for g in list {
551 let group = OverlayGroupW::from_json(g);
552 self.overlay_groups.retain(|x| x.name != group.name);
554 if !group.is_empty() {
555 self.overlay_groups.push(group);
556 }
557 }
558 Ok(())
559 }
560
561 pub fn overlay_group_names(&self) -> Vec<&str> {
565 self.overlay_groups.iter().map(|g| g.name.as_str()).collect()
566 }
567
568 pub fn datum_plane_names(&self) -> Vec<(&str, bool)> {
573 self.planes.iter().map(|p| (p.name.as_str(), p.hot)).collect()
574 }
575
576 pub fn set_dimensions_json(&mut self, json: &str) -> Result<(), String> {
580 let value: Value =
581 serde_json::from_str(json).map_err(|e| format!("dimensions parse: {e}"))?;
582 self.dims.clear();
583 let Some(list) = value.as_array() else {
584 return Ok(());
585 };
586 for d in list {
587 let id = d.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
588 let color = color_of(d.get("color"), dimension::DIMENSION_COLOR);
589 match d.get("type").and_then(|v| v.as_str()) {
590 Some("linear") => {
591 if let (Some(a), Some(b), Some(offset_dir)) = (
592 d.get("a").and_then(vec3_of),
593 d.get("b").and_then(vec3_of),
594 d.get("offsetDir").and_then(vec3_of),
595 ) {
596 self.dims.push(DimW::Linear {
597 id,
598 a,
599 b,
600 offset_dir,
601 offset: d.get("offset").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
602 color,
603 });
604 }
605 }
606 Some("angular") => {
607 if let (Some(vertex), Some(dir_a), Some(dir_b)) = (
608 d.get("vertex").and_then(vec3_of),
609 d.get("dirA").and_then(vec3_of),
610 d.get("dirB").and_then(vec3_of),
611 ) {
612 self.dims.push(DimW::Angular {
613 id,
614 vertex,
615 dir_a,
616 dir_b,
617 radius: d.get("radius").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
618 color,
619 });
620 }
621 }
622 Some("radial") => {
623 if let (Some(center), Some(point)) = (
624 d.get("center").and_then(vec3_of),
625 d.get("pointOnCircle").and_then(vec3_of),
626 ) {
627 self.dims.push(DimW::Radial {
628 id,
629 center,
630 point,
631 color,
632 });
633 }
634 }
635 _ => {}
636 }
637 }
638 Ok(())
639 }
640
641 pub fn set_transform_json(&mut self, json: &str) -> Result<(), String> {
644 let value: Value =
645 serde_json::from_str(json).map_err(|e| format!("transform parse: {e}"))?;
646 if value.is_null() {
647 self.transform = None;
648 self.transform_hover = None;
649 self.transform_active = None;
650 return Ok(());
651 }
652 let origin = value.get("origin").and_then(vec3_of).unwrap_or(Vec3::ZERO);
653 let x = value.get("x").and_then(vec3_of).unwrap_or(Vec3::X);
654 let y = value.get("y").and_then(vec3_of).unwrap_or(Vec3::Y);
655 let z = value.get("z").and_then(vec3_of).unwrap_or(Vec3::Z);
656 let mut gz = TransformGizmo::default();
657 gz.set_frame(origin, x, y, z);
658 gz.show_center = value
659 .get("showCenter")
660 .and_then(|v| v.as_bool())
661 .unwrap_or(true);
662 self.transform = Some(gz);
663 Ok(())
664 }
665
666 pub fn set_viewcube_enabled(&mut self, enabled: bool) {
667 self.viewcube_enabled = enabled;
668 if !enabled {
669 self.viewcube_hover = None;
670 }
671 }
672
673 pub fn build_main_overlay(&self, cam: &GizmoCamera) -> Overlay {
677 let mut ov = Overlay::new();
678
679 for p in &self.planes {
680 let color = if p.hot { brighten(p.color) } else { p.color };
681 let plane = match p.size {
682 Some(size) => datum::datum_plane(p.origin, p.x, p.y, size, color),
683 None => datum::datum_plane_screen(
684 p.origin,
685 p.x,
686 p.y,
687 datum::DEFAULT_PLANE_SCREEN_PX,
688 color,
689 cam,
690 ),
691 };
692 ov.extend(&plane);
693 }
694 for a in &self.axes {
695 let color = if a.hot { brighten(a.color) } else { a.color };
696 ov.extend(&datum::datum_axis(a.point, a.direction, a.length, color));
697 }
698 for f in &self.frames {
699 ov.extend(&datum::datum_frame(f.origin, f.x, f.y, f.z, f.px, cam));
700 }
701 for c in &self.curves {
702 ov.extend(&curve_display::polyline_display(&c.points, c.color, c.closed));
703 }
704 for d in &self.dims {
705 ov.extend(&self.build_dim(d, cam).overlay);
706 }
707 if let Some(gz) = &self.transform {
708 ov.extend(&gz.geometry(cam, self.transform_hover, self.transform_active));
709 }
710 let mut order: Vec<&OverlayGroupW> = self.overlay_groups.iter().collect();
715 order.sort_by_key(|g| g.render_order);
716 for g in order {
717 ov.tris.extend_from_slice(&g.tris);
718 ov.lines.extend_from_slice(&g.lines);
719 for p in &g.points {
720 push_point_quad(&mut ov, p.center, p.color, g.point_size, cam);
721 }
722 }
723 ov
724 }
725
726 fn build_dim(&self, d: &DimW, cam: &GizmoCamera) -> dimension::DimensionAnnotation {
727 match d {
728 DimW::Linear {
729 a,
730 b,
731 offset_dir,
732 offset,
733 color,
734 ..
735 } => dimension::linear_dimension_colored(*a, *b, *offset_dir, *offset, cam, *color),
736 DimW::Angular {
737 vertex,
738 dir_a,
739 dir_b,
740 radius,
741 color,
742 ..
743 } => dimension::angular_dimension_colored(*vertex, *dir_a, *dir_b, *radius, cam, *color),
744 DimW::Radial {
745 center,
746 point,
747 color,
748 ..
749 } => dimension::radial_dimension_colored(*center, *point, cam, *color),
750 }
751 }
752
753 pub fn dimension_anchors(&self, cam: &GizmoCamera) -> Vec<(String, [f32; 3])> {
756 self.dims
757 .iter()
758 .map(|d| {
759 let id = match d {
760 DimW::Linear { id, .. } | DimW::Angular { id, .. } | DimW::Radial { id, .. } => {
761 id.clone()
762 }
763 };
764 (id, self.build_dim(d, cam).label_anchor.into())
765 })
766 .collect()
767 }
768
769 pub fn any_visible(&self) -> bool {
771 self.has_main_overlay() || self.viewcube_enabled
772 }
773
774 pub fn build_overlay(&self, cam: &GizmoCamera) -> WidgetOverlay {
776 WidgetOverlay {
777 main: self.build_main_overlay(cam),
778 viewcube: self.build_viewcube(cam),
779 }
780 }
781
782 pub fn build_viewcube(&self, cam: &GizmoCamera) -> Option<ViewCubeFrame> {
785 if !self.viewcube_enabled {
786 return None;
787 }
788 let overlay = self.viewcube.geometry(cam, self.viewcube_hover, None);
789 let mini = self.viewcube.mini_camera(cam);
790 Some(ViewCubeFrame {
791 overlay,
792 view_proj: mini.view_proj,
793 forward: mini.forward.into(),
794 rect_css: self.viewcube.sub_rect(cam.viewport),
795 })
796 }
797
798 pub fn viewcube_rect(&self, cam: &GizmoCamera) -> [f32; 4] {
803 self.viewcube.sub_rect(cam.viewport)
804 }
805
806 pub fn viewcube_hit(&self, cam: &GizmoCamera, local_x: f32, local_y: f32) -> Option<HandleId> {
808 if !self.viewcube_enabled {
809 return None;
810 }
811 self.viewcube.hit(cam, [local_x, local_y])
812 }
813
814 pub fn set_viewcube_hover(&mut self, handle: Option<HandleId>) -> bool {
817 if self.viewcube_hover != handle {
818 self.viewcube_hover = handle;
819 true
820 } else {
821 false
822 }
823 }
824
825 pub fn viewcube_target(&self, handle: HandleId) -> ([f32; 3], [f32; 3]) {
827 (
828 ViewCube::target_view(handle).into(),
829 ViewCube::target_up(handle).into(),
830 )
831 }
832
833 pub fn datum_pick(&self, cam: &GizmoCamera, x: f32, y: f32) -> Option<String> {
835 for a in &self.axes {
838 let gz = DatumAxis {
839 point: a.point,
840 direction: a.direction,
841 length: a.length,
842 color: a.color,
843 handle: 1,
844 };
845 if gz.hit(cam, [x, y]).is_some() && !a.name.is_empty() {
846 return Some(a.name.clone());
847 }
848 }
849 for p in &self.planes {
850 let gz = DatumPlane {
851 origin: p.origin,
852 x_axis: p.x,
853 y_axis: p.y,
854 size: p.size,
855 color: p.color,
856 handle: 1,
857 };
858 if gz.hit(cam, [x, y]).is_some() && !p.name.is_empty() {
859 return Some(p.name.clone());
860 }
861 }
862 None
863 }
864
865 pub fn has_transform(&self) -> bool {
866 self.transform.is_some()
867 }
868
869 pub fn transform_origin(&self) -> Option<[f32; 3]> {
874 self.transform.as_ref().map(|gz| [gz.origin.x, gz.origin.y, gz.origin.z])
875 }
876
877 pub fn transform_hit(&self, cam: &GizmoCamera, x: f32, y: f32) -> HandleId {
879 self.transform
880 .as_ref()
881 .and_then(|gz| gz.hit(cam, [x, y]))
882 .unwrap_or(0)
883 }
884
885 pub fn set_transform_hover(&mut self, handle: HandleId) -> bool {
886 let handle = (handle != 0).then_some(handle);
887 if self.transform_hover != handle {
888 self.transform_hover = handle;
889 true
890 } else {
891 false
892 }
893 }
894
895 pub fn set_transform_active(&mut self, handle: HandleId) {
896 self.transform_active = (handle != 0).then_some(handle);
897 }
898
899 pub fn transform_drag_json(
903 &self,
904 cam: &GizmoCamera,
905 handle: HandleId,
906 sx: f32,
907 sy: f32,
908 cx: f32,
909 cy: f32,
910 ) -> String {
911 let Some(gz) = &self.transform else {
912 return "{\"kind\":\"none\"}".to_string();
913 };
914 drag_delta_json(gz, cam, handle, sx, sy, cx, cy)
915 }
916
917 pub fn transform_drag_json_with_frame(
923 &self,
924 cam: &GizmoCamera,
925 frame_json: &str,
926 handle: HandleId,
927 sx: f32,
928 sy: f32,
929 cx: f32,
930 cy: f32,
931 ) -> String {
932 let value: Value = match serde_json::from_str(frame_json) {
933 Ok(v) => v,
934 Err(_) => return "{\"kind\":\"none\"}".to_string(),
935 };
936 let origin = value.get("origin").and_then(vec3_of).unwrap_or(Vec3::ZERO);
937 let x = value.get("x").and_then(vec3_of).unwrap_or(Vec3::X);
938 let y = value.get("y").and_then(vec3_of).unwrap_or(Vec3::Y);
939 let z = value.get("z").and_then(vec3_of).unwrap_or(Vec3::Z);
940 let mut gz = TransformGizmo::default();
941 gz.set_frame(origin, x, y, z);
942 drag_delta_json(&gz, cam, handle, sx, sy, cx, cy)
943 }
944}
945
946fn drag_delta_json(
949 gz: &TransformGizmo,
950 cam: &GizmoCamera,
951 handle: HandleId,
952 sx: f32,
953 sy: f32,
954 cx: f32,
955 cy: f32,
956) -> String {
957 let start = cam.ray_from_screen(sx, sy);
958 let current = cam.ray_from_screen(cx, cy);
959 match gz.drag_delta(cam, handle, start, current) {
960 DragDelta::Translate(v) => {
961 let world = gz.ex.scale(v.x).add(gz.ey.scale(v.y)).add(gz.ez.scale(v.z));
962 serde_json::json!({
963 "kind": "translate",
964 "local": [v.x, v.y, v.z],
965 "world": [world.x, world.y, world.z],
966 })
967 .to_string()
968 }
969 DragDelta::Rotate { axis_index, radians } => {
970 let axis = gz.axis(axis_index);
971 serde_json::json!({
972 "kind": "rotate",
973 "axisIndex": axis_index,
974 "axisWorld": [axis.x, axis.y, axis.z],
975 "radians": radians,
976 })
977 .to_string()
978 }
979 DragDelta::None => "{\"kind\":\"none\"}".to_string(),
980 }
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986 use crate::view::ViewCamera;
987
988 fn cam() -> ViewCamera {
989 let mut c = ViewCamera::default();
990 c.width = 800.0;
991 c.height = 600.0;
992 c.eye = [0.0, 0.0, 40.0];
993 c.target = [0.0, 0.0, 0.0];
994 c.up = [0.0, 1.0, 0.0];
995 c.projection = Projection::Orthographic { half_height: 20.0 };
996 c
997 }
998
999 #[test]
1000 fn datums_build_overlay_and_pick() {
1001 let mut reg = WidgetRegistry::new();
1002 reg.set_datums_json(
1003 r#"{"planes":[{"name":"P1","origin":[0,0,0],"x":[1,0,0],"y":[0,1,0],"size":10.0}],
1004 "axes":[{"name":"A1","point":[-5,0,0],"direction":[1,0,0],"length":10.0}]}"#,
1005 )
1006 .unwrap();
1007 assert!(reg.has_main_overlay());
1008 let gc = gizmo_camera(&cam());
1009 let ov = reg.build_main_overlay(&gc);
1010 assert!(!ov.tris.is_empty(), "plane fill tris");
1011 assert!(!ov.lines.is_empty(), "plane border + axis lines");
1012 let hit = reg.datum_pick(&gc, 400.0, 300.0);
1014 assert!(hit.is_some(), "center pixel picks a datum");
1015 }
1016
1017 #[test]
1018 fn transform_feeds_and_drags() {
1019 let mut reg = WidgetRegistry::new();
1020 reg.set_transform_json(r#"{"origin":[0,0,0],"x":[1,0,0],"y":[0,1,0],"z":[0,0,1]}"#)
1021 .unwrap();
1022 assert!(reg.has_transform());
1023 let gc = gizmo_camera(&cam());
1024 let out = reg.transform_drag_json(
1026 &gc,
1027 brep_gizmos::transform::HANDLE_AXIS_X,
1028 400.0,
1029 300.0,
1030 440.0,
1031 300.0,
1032 );
1033 let v: Value = serde_json::from_str(&out).unwrap();
1034 assert_eq!(v["kind"], "translate");
1035 assert!(v["world"][0].as_f64().unwrap() > 0.0, "world dx > 0: {out}");
1036 reg.set_transform_json("null").unwrap();
1038 assert!(!reg.has_transform());
1039 }
1040
1041 #[test]
1042 fn viewcube_frame_and_snap() {
1043 let mut reg = WidgetRegistry::new();
1044 assert!(reg.build_viewcube(&gizmo_camera(&cam())).is_none());
1045 reg.set_viewcube_enabled(true);
1046 let gc = gizmo_camera(&cam());
1047 let frame = reg.build_viewcube(&gc).expect("enabled");
1048 assert!(!frame.overlay.tris.is_empty());
1049 let [x, _y, w, h] = reg.viewcube_rect(&gc);
1050 assert!(x > 600.0 && (w - 135.0).abs() < 1e-3 && (h - 135.0).abs() < 1e-3);
1052 if let Some(handle) = reg.viewcube_hit(&gc, w / 2.0, h / 2.0) {
1055 let (dir, _up) = reg.viewcube_target(handle);
1056 assert!((dir[0].powi(2) + dir[1].powi(2) + dir[2].powi(2) - 1.0).abs() < 1e-3);
1057 }
1058 }
1059
1060 #[test]
1069 fn viewcube_orientation_matches_camera_for_standard_and_rolled_poses() {
1070 use brep_gizmos::view_cube::ViewCube;
1071 let mut poses: Vec<(String, ViewCamera)> = Vec::new();
1072 for name in ["FRONT", "TOP", "RIGHT", "ISO"] {
1073 let mut c = cam();
1074 assert!(c.standard_view(name), "{name}");
1075 poses.push((name.to_string(), c));
1076 }
1077 let mut rolled = cam();
1080 rolled.standard_view("FRONT");
1081 let (_, _, fwd) = rolled.basis();
1082 rolled.up = crate::view::rotate3(rolled.up, fwd, 30f64.to_radians());
1083 poses.push(("FRONT+30°roll".to_string(), rolled));
1084
1085 let cube = ViewCube::new();
1086 for (label, view) in &poses {
1087 let gc = gizmo_camera(view);
1088 let mini = cube.mini_camera(&gc);
1089 for (axis, world) in [
1090 ("X", [1.0f64, 0.0, 0.0]),
1091 ("Y", [0.0, 1.0, 0.0]),
1092 ("Z", [0.0, 0.0, 1.0]),
1093 ] {
1094 let (ox, oy, _) = view.project([0.0, 0.0, 0.0]);
1096 let (ax, ay, _) = view.project(world);
1097 let dm = [ax - ox, ay - oy];
1098 let c0 = mini.world_to_screen(Vec3::ZERO).expect("origin visible");
1100 let c1 = mini
1101 .world_to_screen(Vec3::new(world[0] as f32 * 0.4, world[1] as f32 * 0.4, world[2] as f32 * 0.4))
1102 .expect("axis tip visible");
1103 let dc = [(c1[0] - c0[0]) as f64, (c1[1] - c0[1]) as f64];
1104 let lm = (dm[0] * dm[0] + dm[1] * dm[1]).sqrt();
1105 let lc = (dc[0] * dc[0] + dc[1] * dc[1]).sqrt();
1106 if lm < 1e-3 || lc < 1e-3 {
1107 assert!(
1110 lm < 1.0 && lc < 1.0,
1111 "{label}/{axis}: axis vanishes in one camera only (main {lm}, cube {lc})"
1112 );
1113 continue;
1114 }
1115 let dot = (dm[0] * dc[0] + dm[1] * dc[1]) / (lm * lc);
1116 assert!(
1117 dot > 0.999,
1118 "{label}: world {axis} axis points a different way on the cube \
1119 (main dir {dm:?}, cube dir {dc:?}, dot {dot})"
1120 );
1121 }
1122 }
1123 }
1124
1125 #[test]
1133 fn viewcube_click_targets_round_trip_standard_views() {
1134 use brep_gizmos::view_cube::ViewCube;
1135 let mut reg = WidgetRegistry::new();
1136 reg.set_viewcube_enabled(true);
1137 let cube_faces = [
1138 ("FRONT", ViewCube::FRONT),
1139 ("BACK", ViewCube::BACK),
1140 ("RIGHT", ViewCube::RIGHT),
1141 ("LEFT", ViewCube::LEFT),
1142 ("TOP", ViewCube::TOP),
1143 ("BOTTOM", ViewCube::BOTTOM),
1144 ];
1145 for (name, want) in cube_faces {
1146 let mut view = cam();
1147 assert!(view.standard_view(name));
1148 let gc = gizmo_camera(&view);
1149 let [_, _, w, h] = reg.viewcube_rect(&gc);
1150 let hit = reg.viewcube_hit(&gc, w / 2.0, h / 2.0);
1151 assert_eq!(hit, Some(want), "{name}: center hit");
1152 let (dir, up) = reg.viewcube_target(want);
1154 let (_, _, fwd) = view.basis();
1155 for k in 0..3 {
1156 assert!((dir[k] as f64 - fwd[k]).abs() < 1e-6, "{name}: dir");
1157 assert!((up[k] as f64 - view.up[k]).abs() < 1e-6, "{name}: up");
1158 }
1159 }
1160 let mut view = cam();
1163 view.standard_view("TOP");
1164 let gc = gizmo_camera(&view);
1165 let [_, _, w, h] = reg.viewcube_rect(&gc);
1166 let hit = reg.viewcube_hit(&gc, w / 2.0, h * 0.66).expect("hit the cube");
1167 assert!(
1168 ViewCube::region_name(hit).contains("FRONT"),
1169 "TOP view: below-center click must reach toward +Z/FRONT, got {}",
1170 ViewCube::region_name(hit)
1171 );
1172 }
1173
1174 #[test]
1175 fn overlay_groups_build_upsert_and_clear() {
1176 let mut reg = WidgetRegistry::new();
1177 reg.set_overlay_json(
1179 r#"{"groups":[{"name":"prev","renderOrder":2,
1180 "tris":{"positions":[0,0,0, 1,0,0, 0,1,0],"colors":[1,0,0, 0,1,0, 0,0,1]},
1181 "lines":{"positions":[0,0,0, 2,2,2],"colors":[1,1,0, 1,1,0]},
1182 "points":{"positions":[3,3,3],"colors":[0,1,1],"size":8}}]}"#,
1183 )
1184 .unwrap();
1185 assert!(reg.has_main_overlay());
1186 let gc = gizmo_camera(&cam());
1187 let ov = reg.build_main_overlay(&gc);
1188 assert_eq!(ov.tris.len(), 3 + 6, "tri verts (triangle + point quad)");
1190 assert_eq!(ov.lines.len(), 2, "one line segment (a pair)");
1191 assert_eq!(ov.tris[0].color, [1.0, 0.0, 0.0, 1.0]);
1193
1194 reg.set_overlay_json(
1196 r#"{"groups":[{"name":"prev","lines":{"positions":[0,0,0, 1,1,1],"colors":[1,1,1, 1,1,1]}}]}"#,
1197 )
1198 .unwrap();
1199 let ov = reg.build_main_overlay(&gc);
1200 assert!(ov.tris.is_empty(), "upsert replaced geometry (no tris)");
1201 assert_eq!(ov.lines.len(), 2);
1202
1203 reg.set_overlay_json(r#"{"groups":[{"name":"prev"}]}"#).unwrap();
1205 assert!(!reg.has_main_overlay(), "empty group removed the only overlay");
1206
1207 reg.set_overlay_json(r#"{"groups":[{"name":"a","lines":{"positions":[0,0,0,1,0,0],"colors":[1,1,1,1,1,1]}}]}"#).unwrap();
1209 assert!(reg.has_main_overlay());
1210 reg.set_overlay_json(r#"{"groups":[]}"#).unwrap();
1211 assert!(!reg.has_main_overlay(), "empty groups cleared all");
1212 }
1213
1214 #[test]
1215 fn overlay_groups_bbox_covers_pushed_geometry() {
1216 let mut reg = WidgetRegistry::new();
1217 assert!(reg.overlay_groups_bbox().is_empty(), "no groups -> empty bbox");
1218 reg.set_overlay_json(
1220 r#"{"groups":[{"name":"sk",
1221 "lines":{"positions":[-5,-5,-5, 10,20,30],"colors":[1,1,1, 1,1,1]},
1222 "points":{"positions":[100,0,-2],"colors":[0,1,0],"size":8}}]}"#,
1223 )
1224 .unwrap();
1225 let bbox = reg.overlay_groups_bbox();
1226 assert!(bbox.min[0] <= -5.0 && bbox.min[1] <= -5.0 && bbox.min[2] <= -5.0, "min {:?}", bbox.min);
1229 assert!(bbox.max[0] >= 100.0 && bbox.max[1] >= 20.0 && bbox.max[2] >= 30.0, "max {:?}", bbox.max);
1230 }
1231
1232 #[test]
1233 fn dimensions_build_and_expose_anchors() {
1234 let mut reg = WidgetRegistry::new();
1235 reg.set_dimensions_json(
1236 r#"[{"id":"d1","type":"linear","a":[-3,0,0],"b":[3,0,0],"offsetDir":[0,-1,0],"offset":2.0}]"#,
1237 )
1238 .unwrap();
1239 let gc = gizmo_camera(&cam());
1240 let ov = reg.build_main_overlay(&gc);
1241 assert!(!ov.lines.is_empty() && !ov.tris.is_empty());
1242 let anchors = reg.dimension_anchors(&gc);
1243 assert_eq!(anchors.len(), 1);
1244 assert_eq!(anchors[0].0, "d1");
1245 }
1246}