Feature history on the left side, with contiguous folders (#449)
The feature history can sit in a panel on the left, with folders that hold one unbroken run of features, drag and drop as one undo step, and a resizable divider. Timeline folders save under format 17.
This commit is contained in:
1 parent
be55a49b04
commit
a8b3b21fd0
17 files changed
+2336
-37
No files matched your search
@@ -7,7 +7,7 @@ use solidcraft_engine::doc::FeatureKind;
|
||||
use crate::theme::Tokens;
|
||||
use crate::{SolveApp, icons};
|
||||
|
||||
fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
pub(crate) fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
match k {
|
||||
FeatureKind::Sketch { .. } => "sketch",
|
||||
FeatureKind::Extrude { .. } => "extrude",
|
||||
@@ -37,8 +37,7 @@ fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a drag in the timeline is moving.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Drag {
|
||||
Marker,
|
||||
Feature(u64),
|
||||
@@ -57,16 +56,24 @@ fn slot_at(xs: &[f32], end: f32, x: f32) -> usize {
|
||||
}
|
||||
|
||||
/// Ask before deleting a feature others depend on; delete right away otherwise.
|
||||
fn delete_feature(app: &mut SolveApp, id: u64) {
|
||||
let deps = app.session.execute("timeline.dependents", &json!({ "feature": id })).unwrap_or_default();
|
||||
let list = |k: &str| -> Vec<String> {
|
||||
deps.get(k).and_then(|v| v.as_array()).into_iter().flatten().filter_map(|x| x.as_str().map(String::from)).collect()
|
||||
};
|
||||
let (with, fail) = (list("deleted_with_it"), list("would_fail"));
|
||||
pub(crate) fn delete_feature(app: &mut SolveApp, id: u64) {
|
||||
delete_features(app, vec![id]);
|
||||
}
|
||||
|
||||
pub(crate) fn delete_features(app: &mut SolveApp, ids: Vec<u64>) {
|
||||
let mut with = std::collections::BTreeSet::new();
|
||||
let mut fail = std::collections::BTreeSet::new();
|
||||
for id in &ids {
|
||||
let deps = app.session.execute("timeline.dependents", &json!({ "feature": id })).unwrap_or_default();
|
||||
for (key, output) in [("deleted_with_it", &mut with), ("would_fail", &mut fail)] {
|
||||
output
|
||||
.extend(deps.get(key).and_then(|value| value.as_array()).into_iter().flatten().filter_map(|value| value.as_str().map(String::from)));
|
||||
}
|
||||
}
|
||||
if with.is_empty() && fail.is_empty() {
|
||||
let _ = app.run("timeline.delete", json!({ "features": [id.to_string()] }));
|
||||
let _ = app.run("timeline.delete", json!({ "features": ids }));
|
||||
} else {
|
||||
app.dialog = Some(crate::dialogs::Dialog::confirm_delete(id, with, fail));
|
||||
app.dialog = Some(crate::dialogs::Dialog::confirm_delete_many(ids, with.into_iter().collect(), fail.into_iter().collect()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,12 +288,12 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// The scroll range follows the drawn content (the buttons don't take layout space themselves).
|
||||
ui.allocate_exact_size(vec2((content_end - origin).max(0.0), r.height()), Sense::hover());
|
||||
// Drag feedback, and the drop.
|
||||
if let (Some(dg), Some(pp)) = (drag, pointer) {
|
||||
if let (Some(dg), Some(pp)) = (drag.as_ref(), pointer) {
|
||||
let slot = slot_at(&xs, end, pp.x);
|
||||
let sx = xs.get(slot).copied().unwrap_or(end) - 4.0;
|
||||
p.line_segment(
|
||||
[pos2(sx, r.top() + 3.0), pos2(sx, r.bottom() - 3.0)],
|
||||
Stroke::new(2.0, if dg == Drag::Marker { t.accent } else { t.sketch_accent }),
|
||||
Stroke::new(2.0, if matches!(dg, Drag::Marker) { t.accent } else { t.sketch_accent }),
|
||||
);
|
||||
}
|
||||
let released = ui.input(|i| i.pointer.any_released());
|
||||
@@ -312,7 +319,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
ui.data_mut(|d| match drag {
|
||||
Some(dg) => {
|
||||
d.insert_temp(drag_id, dg);
|
||||
d.insert_temp(drag_id, dg.clone());
|
||||
}
|
||||
None => d.remove::<Drag>(drag_id),
|
||||
});
|
||||
@@ -334,7 +341,7 @@ fn find_in_browser(app: &mut SolveApp, id: u64) {
|
||||
}
|
||||
|
||||
/// Transport buttons drawn as shapes (the UI font has no media glyphs).
|
||||
fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
|
||||
pub(crate) fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
|
||||
let tri = |dir: f32, dx: f32| {
|
||||
let pts = vec![pos2(c.x + dx - 4.0 * dir, c.y - 5.0), pos2(c.x + dx + 4.0 * dir, c.y), pos2(c.x + dx - 4.0 * dir, c.y + 5.0)];
|
||||
p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE));
|
||||
|
||||
Reference in new issue
Block a user