1use super::transform_gizmo::{rotate_euler_xyz_f64, TransformPose};
23use super::*;
24use brep_kernel::feature_pipeline::nurbs_solid;
25use brep_kernel::{CagePoint, ControlCage, Vec3};
26use serde_json::Value;
27
28const OVERLAY_GROUP: &str = "cage-edit";
30const CAGE_LINE: [f32; 3] = [0.30, 0.48, 0.80];
32const DOT_PLAIN: [f32; 3] = [0.70, 0.84, 1.0];
33const DOT_SELECTED: [f32; 3] = [0.435, 0.886, 0.435];
34const DOT_SIZE_PX: f32 = 9.0;
35pub const CAGE_PICK_RADIUS_PX: f64 = 10.0;
37
38#[derive(Clone, Debug, Default)]
40pub struct CageArm {
41 pub(super) selected: Vec<usize>,
43 pub(super) grab: Option<(Vec<[f64; 3]>, [f64; 3])>,
46 pub(super) live: Option<TransformPose>,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum CageSelect {
53 Replace,
55 Toggle,
57 Add,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct CageSummary {
64 pub segments: usize,
65 pub rings: usize,
66 pub points: usize,
67 pub persisted: bool,
70}
71
72fn array3(v: Vec3) -> [f64; 3] {
73 [v.x, v.y, v.z]
74}
75
76fn vec3(a: [f64; 3]) -> Vec3 {
77 Vec3::new(a[0], a[1], a[2])
78}
79
80fn centroid(points: &[[f64; 3]]) -> [f64; 3] {
81 if points.is_empty() {
82 return [0.0; 3];
83 }
84 let mut sum = [0.0; 3];
85 for p in points {
86 for k in 0..3 {
87 sum[k] += p[k];
88 }
89 }
90 let n = points.len() as f64;
91 [sum[0] / n, sum[1] / n, sum[2] / n]
92}
93
94impl EngineState {
95 pub fn is_cage_feature(&self, feature_id: &str) -> bool {
99 self.history
100 .index_of(feature_id)
101 .and_then(|index| self.history.feature_type(index))
102 .is_some_and(|ty| ty.eq_ignore_ascii_case("NS"))
103 }
104
105 pub fn cage_of(&self, feature_id: &str) -> Result<ControlCage, String> {
108 let index = self
109 .history
110 .index_of(feature_id)
111 .filter(|_| self.is_cage_feature(feature_id))
112 .ok_or_else(|| format!("'{feature_id}' is not a NURBS solid"))?;
113 let params = self.history.feature_params(index).unwrap_or(Value::Null);
114 let persistent = self.history.feature_persistent_data(index).unwrap_or(Value::Null);
115 let env = brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator())
116 .map_err(|error| format!("expressions: {error}"))?;
117 nurbs_solid::resolve_cage(&env, ¶ms, &persistent)
118 }
119
120 pub fn cage_points(&self, feature_id: &str) -> Vec<[f64; 3]> {
123 self.cage_of(feature_id)
124 .map(|cage| cage.points().into_iter().map(array3).collect())
125 .unwrap_or_default()
126 }
127
128 pub fn cage_summary(&self, feature_id: &str) -> Option<CageSummary> {
130 let cage = self.cage_of(feature_id).ok()?;
131 let index = self.history.index_of(feature_id)?;
132 let persistent = self.history.feature_persistent_data(index).unwrap_or(Value::Null);
133 Some(CageSummary {
134 segments: cage.segments(),
135 rings: cage.ring_count(),
136 points: cage.point_count(),
137 persisted: nurbs_solid::has_persisted_cage(&persistent),
138 })
139 }
140
141 pub fn cage_edit_feature(&self) -> Option<&str> {
144 self.cage_edit_feature.as_deref()
145 }
146
147 pub fn cage_selected(&self) -> Vec<usize> {
150 self.transform_gizmo
151 .cage
152 .as_ref()
153 .map(|cage| cage.selected.clone())
154 .unwrap_or_default()
155 }
156
157 pub fn cage_armed_for(&self, feature_id: &str) -> bool {
159 self.transform_gizmo.cage.is_some()
160 && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
161 }
162
163 pub fn cage_select(&mut self, feature_id: &str, indices: &[usize], mode: CageSelect) {
169 let count = self.cage_points(feature_id).len();
170 if count == 0 {
171 return;
172 }
173 let mut selected = if self.cage_armed_for(feature_id) {
174 self.cage_selected()
175 } else {
176 Vec::new()
177 };
178 match mode {
179 CageSelect::Replace => {
180 selected = indices.iter().copied().filter(|i| *i < count).collect();
181 }
182 CageSelect::Add => {
183 selected.extend(indices.iter().copied().filter(|i| *i < count));
184 }
185 CageSelect::Toggle => {
186 for &index in indices.iter().filter(|i| **i < count) {
187 if let Some(at) = selected.iter().position(|s| *s == index) {
188 selected.remove(at);
189 } else {
190 selected.push(index);
191 }
192 }
193 }
194 }
195 selected.sort_unstable();
196 selected.dedup();
197 if selected.is_empty() {
198 if self.cage_armed_for(feature_id) {
199 self.disarm_transform();
200 }
201 return;
202 }
203 self.component_move_reset();
204 self.transform_gizmo.feature_id = Some(feature_id.to_string());
205 self.transform_gizmo.mode = GizmoMode::Transform;
206 self.transform_gizmo.drag = None;
207 self.transform_gizmo.anchor = None;
208 self.transform_gizmo.port = None;
209 self.transform_gizmo.cage = Some(CageArm {
210 selected,
211 grab: None,
212 live: None,
213 });
214 self.clear_feature_dimension_overlay();
215 self.sync_transform_gizmo();
216 }
217
218 pub fn cage_select_all(&mut self, feature_id: &str) {
220 let all: Vec<usize> = (0..self.cage_points(feature_id).len()).collect();
221 self.cage_select(feature_id, &all, CageSelect::Replace);
222 }
223
224 pub fn cage_clear_selection(&mut self, feature_id: &str) {
226 self.cage_select(feature_id, &[], CageSelect::Replace);
227 }
228
229 pub fn cage_select_rings_of_selection(&mut self, feature_id: &str) {
232 let Ok(cage) = self.cage_of(feature_id) else {
233 return;
234 };
235 let mut grown = Vec::new();
236 for index in self.cage_selected() {
237 if let Some(CagePoint::Ring { ring, .. }) = cage.locate(index) {
238 grown.extend((0..cage.segments()).map(|c| cage.index_of(ring, c)));
239 }
240 }
241 self.cage_select(feature_id, &grown, CageSelect::Add);
242 }
243
244 pub fn cage_select_columns_of_selection(&mut self, feature_id: &str) {
247 let Ok(cage) = self.cage_of(feature_id) else {
248 return;
249 };
250 let mut grown = Vec::new();
251 for index in self.cage_selected() {
252 if let Some(CagePoint::Ring { column, .. }) = cage.locate(index) {
253 grown.extend((0..cage.ring_count()).map(|r| cage.index_of(r, column)));
254 }
255 }
256 self.cage_select(feature_id, &grown, CageSelect::Add);
257 }
258
259 pub fn cage_point_pick_at(&mut self, x: f64, y: f64, toggle: bool) -> Option<usize> {
266 let feature_id = self.cage_edit_feature.clone()?;
267 let points = self.cage_points(&feature_id);
268 let mut best: Option<(usize, f64)> = None;
269 for (index, point) in points.iter().enumerate() {
270 if !self.camera.projectable(*point) {
271 continue;
272 }
273 let (sx, sy, _) = self.camera.project(*point);
274 let d = ((sx - x).powi(2) + (sy - y).powi(2)).sqrt();
275 if d <= CAGE_PICK_RADIUS_PX && best.is_none_or(|(_, bd)| d < bd) {
276 best = Some((index, d));
277 }
278 }
279 let (index, _) = best?;
280 let mode = if toggle { CageSelect::Toggle } else { CageSelect::Replace };
281 self.cage_select(&feature_id, &[index], mode);
282 Some(index)
283 }
284
285 pub fn cage_point_screen_points(&self) -> Vec<(usize, (f64, f64))> {
288 let Some(feature_id) = self.cage_edit_feature.as_deref() else {
289 return Vec::new();
290 };
291 self.cage_points(feature_id)
292 .into_iter()
293 .enumerate()
294 .filter(|(_, point)| self.camera.projectable(*point))
295 .map(|(index, point)| {
296 let (sx, sy, _) = self.camera.project(point);
297 (index, (sx, sy))
298 })
299 .collect()
300 }
301
302 pub(super) fn cage_pose(&self) -> Option<TransformPose> {
306 let arm = self.transform_gizmo.cage.as_ref()?;
307 if self.transform_gizmo.drag.is_some() {
308 if let Some(live) = arm.live {
309 return Some(live);
310 }
311 }
312 let feature_id = self.transform_gizmo.feature_id.as_deref()?;
313 let points = self.cage_points(feature_id);
314 let selected: Vec<[f64; 3]> = arm
315 .selected
316 .iter()
317 .filter_map(|i| points.get(*i).copied())
318 .collect();
319 if selected.is_empty() {
320 return None;
321 }
322 Some(TransformPose {
323 position: centroid(&selected),
324 rotation_deg: [0.0; 3],
325 scale: [1.0; 3],
326 })
327 }
328
329 pub(super) fn cage_shows_rings(&self) -> Option<bool> {
332 self.transform_gizmo
333 .cage
334 .as_ref()
335 .map(|arm| arm.selected.len() >= 2)
336 }
337
338 pub(super) fn cage_grab_begin(&mut self) {
340 let Some(feature_id) = self.transform_gizmo.feature_id.clone() else {
341 return;
342 };
343 if self.transform_gizmo.cage.is_none() {
344 return;
345 }
346 let points = self.cage_points(&feature_id);
347 if let Some(arm) = self.transform_gizmo.cage.as_mut() {
348 let snapshot: Vec<[f64; 3]> = arm
349 .selected
350 .iter()
351 .filter_map(|i| points.get(*i).copied())
352 .collect();
353 let center = centroid(&snapshot);
354 arm.grab = Some((snapshot, center));
355 arm.live = None;
356 }
357 }
358
359 pub(super) fn cage_grab_end(&mut self) {
361 if let Some(arm) = self.transform_gizmo.cage.as_mut() {
362 arm.grab = None;
363 arm.live = None;
364 }
365 }
366
367 pub(super) fn write_cage_pose(&mut self, pose: &TransformPose) {
372 let Some(feature_id) = self.transform_gizmo.feature_id.clone() else {
373 return;
374 };
375 let Some(arm) = self.transform_gizmo.cage.as_ref() else {
376 return;
377 };
378 let Some((snapshot, center)) = arm.grab.clone() else {
379 return;
380 };
381 let selected = arm.selected.clone();
382 let dragging = self.transform_dragging();
383 let euler = [
384 pose.rotation_deg[0].to_radians(),
385 pose.rotation_deg[1].to_radians(),
386 pose.rotation_deg[2].to_radians(),
387 ];
388 let shift = [
389 pose.position[0] - center[0],
390 pose.position[1] - center[1],
391 pose.position[2] - center[2],
392 ];
393 let Ok(mut cage) = self.cage_of(&feature_id) else {
394 return;
395 };
396 for (index, start) in selected.iter().zip(snapshot.iter()) {
397 let offset = [start[0] - center[0], start[1] - center[1], start[2] - center[2]];
398 let turned = rotate_euler_xyz_f64(offset, euler);
399 let moved: [f64; 3] = std::array::from_fn(|axis| {
400 let after = center[axis] + turned[axis] + shift[axis];
401 if dragging {
402 crate::formatting::snap_drag_moved(start[axis], after)
403 } else {
404 after
405 }
406 });
407 let moved = Vec3::new(moved[0], moved[1], moved[2]);
408 let _ = cage.set_point(*index, moved);
409 }
410 if let Some(arm) = self.transform_gizmo.cage.as_mut() {
411 arm.live = Some(*pose);
412 }
413 let key = format!("cage-drag:{feature_id}");
414 self.write_cage(&feature_id, &cage, Some(&key));
415 }
416
417 fn write_cage(&mut self, feature_id: &str, cage: &ControlCage, coalesce: Option<&str>) {
422 let Some(index) = self.history.index_of(feature_id) else {
423 return;
424 };
425 self.history.set_feature_persistent_field_coalesced(
426 index,
427 nurbs_solid::PERSISTENT_KEY,
428 cage.to_json(),
429 coalesce,
430 );
431 self.rerun_history();
432 }
433
434 pub fn cage_set_point_position(
436 &mut self,
437 feature_id: &str,
438 index: usize,
439 position: [f64; 3],
440 ) -> Result<(), String> {
441 let mut cage = self.cage_of(feature_id)?;
442 cage.set_point(index, vec3(position))?;
443 let key = format!("cage-point:{feature_id}:{index}");
444 self.write_cage(feature_id, &cage, Some(&key));
445 self.sync_transform_gizmo();
446 Ok(())
447 }
448
449 pub fn cage_insert_ring(&mut self, feature_id: &str, above: bool) -> Result<usize, String> {
454 let mut cage = self.cage_of(feature_id)?;
455 let anchor = self
456 .cage_selected()
457 .first()
458 .copied()
459 .ok_or("select a point of the cage first: the ring goes beside it")?;
460 let row = match cage.locate(anchor) {
462 Some(CagePoint::Pole(0)) => 0,
463 Some(CagePoint::Pole(_)) => cage.ring_count(),
464 Some(CagePoint::Ring { ring, .. }) => {
465 if above {
466 ring + 1
467 } else {
468 ring
469 }
470 }
471 None => return Err(format!("no cage point {anchor}")),
472 };
473 let new_ring = cage.insert_ring_between(row)?;
474 self.write_cage(feature_id, &cage, None);
475 let loop_points: Vec<usize> = (0..cage.segments()).map(|c| cage.index_of(new_ring, c)).collect();
476 self.cage_select(feature_id, &loop_points, CageSelect::Replace);
477 Ok(new_ring)
478 }
479
480 pub fn cage_insert_column(&mut self, feature_id: &str, next: bool) -> Result<usize, String> {
486 let mut cage = self.cage_of(feature_id)?;
487 let selected = self.cage_selected();
488 let column = selected
489 .iter()
490 .find_map(|index| match cage.locate(*index) {
491 Some(CagePoint::Ring { column, .. }) => Some(column),
492 _ => None,
493 })
494 .ok_or("select a ring point of the cage first: the column goes beside it")?;
495 let n = cage.segments();
496 let after = if next { column } else { (column + n - 1) % n };
497 let new_column = cage.insert_column_between(after)?;
498 self.write_cage(feature_id, &cage, None);
499 let loop_points: Vec<usize> = (0..cage.ring_count()).map(|r| cage.index_of(r, new_column)).collect();
500 self.cage_select(feature_id, &loop_points, CageSelect::Replace);
501 Ok(new_column)
502 }
503
504 pub fn cage_reset(&mut self, feature_id: &str) -> Result<(), String> {
507 let index = self
508 .history
509 .index_of(feature_id)
510 .filter(|_| self.is_cage_feature(feature_id))
511 .ok_or_else(|| format!("'{feature_id}' is not a NURBS solid"))?;
512 if self.cage_armed_for(feature_id) {
513 self.disarm_transform();
514 }
515 self.history
516 .set_feature_persistent_field(index, nurbs_solid::PERSISTENT_KEY, Value::Null);
517 self.rerun_history();
518 Ok(())
519 }
520
521 pub fn refresh_cage_edit_overlay(&mut self, feature_id: Option<&str>) {
527 if self.cage_edit_feature.as_deref() != feature_id {
528 self.cage_edit_feature = feature_id.map(str::to_string);
529 }
530 let selected = feature_id
531 .filter(|id| self.cage_armed_for(id))
532 .map(|_| self.cage_selected())
533 .unwrap_or_default();
534 let key = feature_id.map(|id| (id.to_string(), selected.clone(), self.applied_generation));
535 if self.cage_overlay_key == key {
536 return;
537 }
538 self.cage_overlay_key = key;
539 let Some(feature_id) = feature_id else {
540 let _ = self
541 .widgets
542 .set_overlay_json(&serde_json::json!({ "groups": [{ "name": OVERLAY_GROUP }] }).to_string());
543 self.dirty = true;
544 return;
545 };
546 let Ok(cage) = self.cage_of(feature_id) else {
547 let _ = self
548 .widgets
549 .set_overlay_json(&serde_json::json!({ "groups": [{ "name": OVERLAY_GROUP }] }).to_string());
550 self.dirty = true;
551 return;
552 };
553 let mut line_positions: Vec<f64> = Vec::new();
554 let mut line_colors: Vec<f32> = Vec::new();
555 let mut push_line = |a: Vec3, b: Vec3| {
556 line_positions.extend_from_slice(&array3(a));
557 line_positions.extend_from_slice(&array3(b));
558 line_colors.extend_from_slice(&CAGE_LINE);
559 line_colors.extend_from_slice(&CAGE_LINE);
560 };
561 let n = cage.segments();
562 for ring in &cage.rings {
563 for c in 0..n {
564 push_line(ring[c], ring[(c + 1) % n]);
565 }
566 }
567 for c in 0..n {
568 let mut previous = cage.poles[0];
569 for ring in &cage.rings {
570 push_line(previous, ring[c]);
571 previous = ring[c];
572 }
573 push_line(previous, cage.poles[1]);
574 }
575 let mut dot_positions: Vec<f64> = Vec::new();
576 let mut dot_colors: Vec<f32> = Vec::new();
577 for (index, point) in cage.points().iter().enumerate() {
578 dot_positions.extend_from_slice(&array3(*point));
579 let color = if selected.binary_search(&index).is_ok() {
580 DOT_SELECTED
581 } else {
582 DOT_PLAIN
583 };
584 dot_colors.extend_from_slice(&color);
585 }
586 let group = serde_json::json!({
587 "name": OVERLAY_GROUP,
588 "renderOrder": 6,
589 "lines": { "positions": line_positions, "colors": line_colors },
590 "points": { "positions": dot_positions, "colors": dot_colors, "size": DOT_SIZE_PX },
591 });
592 let _ = self
593 .widgets
594 .set_overlay_json(&serde_json::json!({ "groups": [group] }).to_string());
595 self.dirty = true;
596 }
597}
598