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 euler = [
383 pose.rotation_deg[0].to_radians(),
384 pose.rotation_deg[1].to_radians(),
385 pose.rotation_deg[2].to_radians(),
386 ];
387 let shift = [
388 pose.position[0] - center[0],
389 pose.position[1] - center[1],
390 pose.position[2] - center[2],
391 ];
392 let Ok(mut cage) = self.cage_of(&feature_id) else {
393 return;
394 };
395 for (index, start) in selected.iter().zip(snapshot.iter()) {
396 let offset = [start[0] - center[0], start[1] - center[1], start[2] - center[2]];
397 let turned = rotate_euler_xyz_f64(offset, euler);
398 let moved = Vec3::new(
399 center[0] + turned[0] + shift[0],
400 center[1] + turned[1] + shift[1],
401 center[2] + turned[2] + shift[2],
402 );
403 let _ = cage.set_point(*index, moved);
404 }
405 if let Some(arm) = self.transform_gizmo.cage.as_mut() {
406 arm.live = Some(*pose);
407 }
408 let key = format!("cage-drag:{feature_id}");
409 self.write_cage(&feature_id, &cage, Some(&key));
410 }
411
412 fn write_cage(&mut self, feature_id: &str, cage: &ControlCage, coalesce: Option<&str>) {
417 let Some(index) = self.history.index_of(feature_id) else {
418 return;
419 };
420 self.history.set_feature_persistent_field_coalesced(
421 index,
422 nurbs_solid::PERSISTENT_KEY,
423 cage.to_json(),
424 coalesce,
425 );
426 self.rerun_history();
427 }
428
429 pub fn cage_set_point_position(
431 &mut self,
432 feature_id: &str,
433 index: usize,
434 position: [f64; 3],
435 ) -> Result<(), String> {
436 let mut cage = self.cage_of(feature_id)?;
437 cage.set_point(index, vec3(position))?;
438 let key = format!("cage-point:{feature_id}:{index}");
439 self.write_cage(feature_id, &cage, Some(&key));
440 self.sync_transform_gizmo();
441 Ok(())
442 }
443
444 pub fn cage_insert_ring(&mut self, feature_id: &str, above: bool) -> Result<usize, String> {
449 let mut cage = self.cage_of(feature_id)?;
450 let anchor = self
451 .cage_selected()
452 .first()
453 .copied()
454 .ok_or("select a point of the cage first: the ring goes beside it")?;
455 let row = match cage.locate(anchor) {
457 Some(CagePoint::Pole(0)) => 0,
458 Some(CagePoint::Pole(_)) => cage.ring_count(),
459 Some(CagePoint::Ring { ring, .. }) => {
460 if above {
461 ring + 1
462 } else {
463 ring
464 }
465 }
466 None => return Err(format!("no cage point {anchor}")),
467 };
468 let new_ring = cage.insert_ring_between(row)?;
469 self.write_cage(feature_id, &cage, None);
470 let loop_points: Vec<usize> = (0..cage.segments()).map(|c| cage.index_of(new_ring, c)).collect();
471 self.cage_select(feature_id, &loop_points, CageSelect::Replace);
472 Ok(new_ring)
473 }
474
475 pub fn cage_insert_column(&mut self, feature_id: &str, next: bool) -> Result<usize, String> {
481 let mut cage = self.cage_of(feature_id)?;
482 let selected = self.cage_selected();
483 let column = selected
484 .iter()
485 .find_map(|index| match cage.locate(*index) {
486 Some(CagePoint::Ring { column, .. }) => Some(column),
487 _ => None,
488 })
489 .ok_or("select a ring point of the cage first: the column goes beside it")?;
490 let n = cage.segments();
491 let after = if next { column } else { (column + n - 1) % n };
492 let new_column = cage.insert_column_between(after)?;
493 self.write_cage(feature_id, &cage, None);
494 let loop_points: Vec<usize> = (0..cage.ring_count()).map(|r| cage.index_of(r, new_column)).collect();
495 self.cage_select(feature_id, &loop_points, CageSelect::Replace);
496 Ok(new_column)
497 }
498
499 pub fn cage_reset(&mut self, feature_id: &str) -> Result<(), String> {
502 let index = self
503 .history
504 .index_of(feature_id)
505 .filter(|_| self.is_cage_feature(feature_id))
506 .ok_or_else(|| format!("'{feature_id}' is not a NURBS solid"))?;
507 if self.cage_armed_for(feature_id) {
508 self.disarm_transform();
509 }
510 self.history
511 .set_feature_persistent_field(index, nurbs_solid::PERSISTENT_KEY, Value::Null);
512 self.rerun_history();
513 Ok(())
514 }
515
516 pub fn refresh_cage_edit_overlay(&mut self, feature_id: Option<&str>) {
522 if self.cage_edit_feature.as_deref() != feature_id {
523 self.cage_edit_feature = feature_id.map(str::to_string);
524 }
525 let selected = feature_id
526 .filter(|id| self.cage_armed_for(id))
527 .map(|_| self.cage_selected())
528 .unwrap_or_default();
529 let key = feature_id.map(|id| (id.to_string(), selected.clone(), self.applied_generation));
530 if self.cage_overlay_key == key {
531 return;
532 }
533 self.cage_overlay_key = key;
534 let Some(feature_id) = feature_id else {
535 let _ = self
536 .widgets
537 .set_overlay_json(&serde_json::json!({ "groups": [{ "name": OVERLAY_GROUP }] }).to_string());
538 self.dirty = true;
539 return;
540 };
541 let Ok(cage) = self.cage_of(feature_id) else {
542 let _ = self
543 .widgets
544 .set_overlay_json(&serde_json::json!({ "groups": [{ "name": OVERLAY_GROUP }] }).to_string());
545 self.dirty = true;
546 return;
547 };
548 let mut line_positions: Vec<f64> = Vec::new();
549 let mut line_colors: Vec<f32> = Vec::new();
550 let mut push_line = |a: Vec3, b: Vec3| {
551 line_positions.extend_from_slice(&array3(a));
552 line_positions.extend_from_slice(&array3(b));
553 line_colors.extend_from_slice(&CAGE_LINE);
554 line_colors.extend_from_slice(&CAGE_LINE);
555 };
556 let n = cage.segments();
557 for ring in &cage.rings {
558 for c in 0..n {
559 push_line(ring[c], ring[(c + 1) % n]);
560 }
561 }
562 for c in 0..n {
563 let mut previous = cage.poles[0];
564 for ring in &cage.rings {
565 push_line(previous, ring[c]);
566 previous = ring[c];
567 }
568 push_line(previous, cage.poles[1]);
569 }
570 let mut dot_positions: Vec<f64> = Vec::new();
571 let mut dot_colors: Vec<f32> = Vec::new();
572 for (index, point) in cage.points().iter().enumerate() {
573 dot_positions.extend_from_slice(&array3(*point));
574 let color = if selected.binary_search(&index).is_ok() {
575 DOT_SELECTED
576 } else {
577 DOT_PLAIN
578 };
579 dot_colors.extend_from_slice(&color);
580 }
581 let group = serde_json::json!({
582 "name": OVERLAY_GROUP,
583 "renderOrder": 6,
584 "lines": { "positions": line_positions, "colors": line_colors },
585 "points": { "positions": dot_positions, "colors": dot_colors, "size": DOT_SIZE_PX },
586 });
587 let _ = self
588 .widgets
589 .set_overlay_json(&serde_json::json!({ "groups": [group] }).to_string());
590 self.dirty = true;
591 }
592}
593