133 lines
6.7 KiB
Rust
133 lines
6.7 KiB
Rust
//! Sketch patterns are children of their sketch, rather than separate timeline features.
|
|
|
|
use serde_json::json;
|
|
use solidcraft_engine::{Sel, sketch::SketchPatternKind};
|
|
|
|
use crate::{
|
|
SolveApp,
|
|
browser::{Row, draw_row},
|
|
};
|
|
|
|
pub(crate) fn has_patterns(app: &SolveApp, sketch: u64) -> bool {
|
|
app.session.doc.sketch(sketch).is_ok_and(|sk| !sk.patterns.is_empty())
|
|
}
|
|
|
|
/// Shared by the parts browser and the vertical feature history. The stored document is the
|
|
/// source, so rolled-back sketches retain their children without showing preview-only copies.
|
|
pub(crate) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, sketch: u64, depth: usize, scope: &str, occurrence: Option<(u64, &[u64])>) {
|
|
let patterns: Vec<_> = app.session.doc.sketch(sketch).map(|sk| sk.patterns.clone()).unwrap_or_default();
|
|
for pattern in patterns {
|
|
let (name, icon) = match pattern.kind {
|
|
SketchPatternKind::Rectangular { .. } => ("Rectangular Pattern", "sketch_rect_pattern"),
|
|
SketchPatternKind::Circular { .. } => ("Circular Pattern", "sketch_circ_pattern"),
|
|
};
|
|
let suffix = pattern.id.strip_prefix("pat").unwrap_or(&pattern.id);
|
|
let label = format!("{} {suffix}", crate::i18n::tr_label(name));
|
|
let active = app.session.active_sketch == Some(sketch);
|
|
let curves: Vec<_> = app
|
|
.session
|
|
.doc
|
|
.sketch(sketch)
|
|
.map(|sk| {
|
|
pattern
|
|
.entities
|
|
.iter()
|
|
.chain(pattern.instances.iter().flat_map(|i| &i.curves))
|
|
.map(|id| if sk.curve_index(id).is_some() { Sel::SketchCurve { id: id.clone() } } else { Sel::SketchPoint { id: id.clone() } })
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
let selected = active && !curves.is_empty() && curves.iter().all(|curve| app.session.selection.contains(curve));
|
|
let response = draw_row(
|
|
ui,
|
|
ui.id().with(("sketch_pattern", sketch, &pattern.id)),
|
|
&Row { depth, icon, label: &label, name: true, selected, ..Default::default() },
|
|
);
|
|
if let Some(rect) = response.rect {
|
|
crate::scenario::publish_handle(&format!("{scope}:sketch-pattern:{sketch}:{}", pattern.id), rect.center());
|
|
}
|
|
if response.double {
|
|
if let Some((component, path)) = occurrence {
|
|
let _ = app.run("component.activate", json!({"component":component,"occurrence":path.last(),"path":path}));
|
|
}
|
|
app.edit_sketch(sketch);
|
|
if app.session.active_sketch == Some(sketch) {
|
|
crate::sketch_modify_ui::edit_pattern(app, &pattern.id);
|
|
}
|
|
} else if response.clicked {
|
|
let items = if active { curves } else { vec![Sel::Feature { id: sketch }] };
|
|
let _ = app.run("select.set", json!({"items":items}));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{dialogs::Kind, scenario::Harness, sketch_modify_ui::Mod};
|
|
|
|
fn harness(left_history: bool) -> (Harness, u64, String, String) {
|
|
let mut h = Harness::new();
|
|
h.app.home.open = false;
|
|
h.app.ui.show_browser = true;
|
|
h.app.preferences.timeline_left = left_history;
|
|
h.app.run("sketch.create", json!({"plane":"XY","name":"Patterned Sketch"})).unwrap();
|
|
let sketch = h.app.session.active_sketch.unwrap();
|
|
h.app.run("sketch.line", json!({"points":[[10,5],[15,5]],"ids":["source"]})).unwrap();
|
|
let rectangular = h
|
|
.app
|
|
.run("sketch.pattern.rectangular", json!({"entities":["source"],"count":4,"spacing":20,"alignment":"center","reversed":true}))
|
|
.unwrap();
|
|
let circular = h.app.run("sketch.pattern.circular", json!({"entities":["source"],"center":"origin","count":3})).unwrap();
|
|
h.app.finish_sketch();
|
|
h.frames(6);
|
|
(h, sketch, rectangular["pattern"].as_str().unwrap().into(), circular["pattern"].as_str().unwrap().into())
|
|
}
|
|
|
|
#[test]
|
|
fn browser_patterns_fold_with_the_sketch_and_double_click_opens_the_saved_editor() {
|
|
let (mut h, sketch, rectangular, circular) = harness(false);
|
|
let handle = format!("browser:sketch-pattern:{sketch}:{rectangular}");
|
|
let child = crate::scenario::handle_at(&handle).unwrap();
|
|
let parent = crate::scenario::handle_at("row:Patterned Sketch").unwrap();
|
|
assert!(child.y > parent.y, "patterns belong below their sketch");
|
|
assert!(crate::scenario::handle_at(&format!("browser:sketch-pattern:{sketch}:{circular}")).is_some());
|
|
h.step(&json!({"click":{"handle":"fold:Patterned Sketch"}})).unwrap();
|
|
h.frames(3);
|
|
assert!(crate::scenario::handle_at(&handle).is_none());
|
|
h.step(&json!({"click":{"handle":"fold:Patterned Sketch"}})).unwrap();
|
|
// Let the fold clicks leave egui's triple-click interval before double-clicking a child.
|
|
h.frames(40);
|
|
h.step(&json!({"click":{"handle":handle},"double":true})).unwrap();
|
|
h.frames(3);
|
|
assert_eq!(h.app.session.active_sketch, Some(sketch), "selection={:?}, status={:?}", h.app.session.selection, h.app.status);
|
|
let dialog = h.app.dialog.as_ref().unwrap();
|
|
assert_eq!(dialog.extra["pattern"], rectangular);
|
|
assert!(matches!(&dialog.kind, Kind::SketchModify(Mod::PatternRect { count, reversed: true, .. }) if count == "4"));
|
|
}
|
|
|
|
#[test]
|
|
fn left_history_lists_both_pattern_kinds_and_tracks_deletion_and_undo() {
|
|
let (mut h, sketch, rectangular, circular) = harness(true);
|
|
let handle = format!("timeline:sketch-pattern:{sketch}:{circular}");
|
|
assert!(crate::scenario::handle_at(&format!("timeline:sketch-pattern:{sketch}:{rectangular}")).is_some());
|
|
assert!(crate::scenario::handle_at(&handle).is_some());
|
|
h.step(&json!({"click":{"handle":"timeline-sketch-fold:Patterned Sketch"}})).unwrap();
|
|
h.frames(3);
|
|
assert!(crate::scenario::handle_at(&handle).is_none());
|
|
h.step(&json!({"click":{"handle":"timeline-sketch-fold:Patterned Sketch"}})).unwrap();
|
|
h.frames(40);
|
|
h.step(&json!({"click":{"handle":handle},"double":true})).unwrap();
|
|
h.frames(3);
|
|
assert!(matches!(h.app.dialog.as_ref().unwrap().kind, Kind::SketchModify(Mod::PatternCirc { .. })));
|
|
assert_eq!(h.app.dialog.as_ref().unwrap().extra["pattern"], circular);
|
|
h.step(&json!({"key":"Escape"})).unwrap();
|
|
h.app.run("sketch.delete", json!({"entities":["source"]})).unwrap();
|
|
h.frames(3);
|
|
assert!(crate::scenario::handle_at(&handle).is_none());
|
|
h.app.run("edit.undo", json!({})).unwrap();
|
|
h.frames(3);
|
|
assert!(crate::scenario::handle_at(&handle).is_some());
|
|
}
|
|
}
|