Avoid per-frame mesh copies in both timeline layouts (#1907)

Thanks @crono2250.
This commit is contained in:
cronos authored and GitHub committed 2026-10-18 02:34:05 -04:00
1 parent 932efbf341
commit 8f768b65c8
2 files changed
+61 -19

No files matched your search

+50 -8
View File
@@ -4,13 +4,39 @@ pub(crate) use crate::icons::glyphs::transport_glyph;
use egui::{Rect, Sense, Stroke, UiBuilder, pos2, vec2};
use serde_json::json;
use solidcraft_engine::doc::{
FeatureKind, PlaneRef,
FeatureKind, PlaneRef, SketchId,
construct::{AxisDef, PointDef},
};
use crate::theme::Tokens;
use crate::{SolveApp, icons};
// Snapshot only the data used by timeline interactions. Cloning FeatureKind here would
// also copy imported mesh vertices and triangles on every frame.
#[derive(Clone, Copy)]
pub(crate) enum TimelineKind {
Sketch,
Profile(SketchId),
Other,
}
impl TimelineKind {
pub(crate) fn of(kind: &FeatureKind) -> Self {
match kind {
FeatureKind::Sketch { .. } => Self::Sketch,
FeatureKind::Extrude { sketch, .. } | FeatureKind::Revolve { sketch, .. } | FeatureKind::Sweep { sketch, .. } => Self::Profile(*sketch),
_ => Self::Other,
}
}
pub(crate) fn profile_sketch(self) -> Option<SketchId> {
match self {
Self::Profile(sketch) => Some(sketch),
_ => None,
}
}
}
pub(crate) fn icon_of(k: &FeatureKind) -> &'static str {
match k {
FeatureKind::Sketch { .. } => "sketch",
@@ -263,7 +289,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
.doc
.features
.iter()
.map(|f| (f.id, f.name.clone(), icon_of(&f.kind), f.suppressed, f.kind.type_name(), f.kind.clone()))
.map(|f| (f.id, f.name.clone(), icon_of(&f.kind), f.suppressed, f.kind.type_name(), TimelineKind::of(&f.kind)))
.collect();
// The feature whose body is under the cursor in the viewport.
let st = app.session.world_state();
@@ -373,12 +399,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
if resp.double_clicked() {
edit = Some(*id);
}
let sketch_of = match kind {
FeatureKind::Extrude { sketch, .. } | FeatureKind::Revolve { sketch, .. } | FeatureKind::Sweep { sketch, .. } => {
Some(*sketch)
}
_ => None,
};
let sketch_of = kind.profile_sketch();
resp.context_menu(|ui| {
if ui.button(crate::i18n::tr_label("Edit Feature")).clicked() {
edit = Some(*id);
@@ -596,6 +617,27 @@ mod tests {
h
}
#[test]
fn the_feature_menu_edits_its_profile_sketch_in_both_timelines() {
for left in [false, true] {
let mut h = Harness::new();
h.app.preferences.timeline_left = left;
h.app.run("sketch.create", json!({"plane": "XY", "name": "Base"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("solid.extrude", json!({"sketch": "Base", "distance": 10, "operation": "new", "name": "Depth"})).unwrap();
let sketch = h.app.session.doc.find_feature("Base").map(|f| f.id).unwrap();
h.frames(4);
h.step(&json!({"click": {"handle": "timeline:Base"}, "button": "right"})).unwrap();
assert!(h.widget("Edit Profile Sketch").is_none(), "left: {left}");
h.step(&json!({"key": "Escape"})).unwrap();
h.step(&json!({"click": {"handle": "timeline:Depth"}, "button": "right"})).unwrap();
h.step(&json!({"widget": "Edit Profile Sketch"})).unwrap();
assert_eq!(h.app.session.active_sketch, Some(sketch), "left: {left}");
}
}
fn copied(h: &Harness) -> Vec<String> {
h.app.session.clipboard.iter().map(|f| f.name.clone()).collect()
}
+11 -11
View File
@@ -2,10 +2,11 @@
use egui::{Color32, Rect, RichText, Sense, Stroke, pos2, vec2};
use serde_json::json;
use solidcraft_engine::doc::FeatureKind;
use crate::theme::Tokens;
use crate::timeline::{clipboard_buttons, delete_feature, delete_features, icon_of, message_button, selected_feature_ids, transport_glyph};
use crate::timeline::{
TimelineKind, clipboard_buttons, delete_feature, delete_features, icon_of, message_button, selected_feature_ids, transport_glyph,
};
use crate::{SolveApp, icons};
const FOLDER_INDENT: f32 = 14.0;
@@ -427,7 +428,7 @@ fn group_ancestors_are_open(app: &SolveApp, groups: &[solidcraft_engine::doc::Ti
fn group_first_feature(
groups: &[solidcraft_engine::doc::TimelineGroup],
features: &[(u64, String, &'static str, bool, &'static str, FeatureKind)],
features: &[(u64, String, &'static str, bool, &'static str, TimelineKind)],
id: u64,
) -> Option<usize> {
let group = groups.iter().find(|group| group.id == id)?;
@@ -438,7 +439,7 @@ fn group_first_feature(
fn group_last_feature(
groups: &[solidcraft_engine::doc::TimelineGroup],
features: &[(u64, String, &'static str, bool, &'static str, FeatureKind)],
features: &[(u64, String, &'static str, bool, &'static str, TimelineKind)],
id: u64,
) -> Option<usize> {
let group = groups.iter().find(|group| group.id == id)?;
@@ -449,7 +450,7 @@ fn group_last_feature(
fn visible_order(
app: &SolveApp,
features: &[(u64, String, &'static str, bool, &'static str, FeatureKind)],
features: &[(u64, String, &'static str, bool, &'static str, TimelineKind)],
groups: &[solidcraft_engine::doc::TimelineGroup],
) -> Vec<TimelineItem> {
let mut order = Vec::new();
@@ -615,7 +616,9 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui) {
.doc
.features
.iter()
.map(|feature| (feature.id, feature.name.clone(), icon_of(&feature.kind), feature.suppressed, feature.kind.type_name(), feature.kind.clone()))
.map(|feature| {
(feature.id, feature.name.clone(), icon_of(&feature.kind), feature.suppressed, feature.kind.type_name(), TimelineKind::of(&feature.kind))
})
.collect();
let groups = app.session.doc.timeline_groups.clone();
let order = visible_order(app, &features, &groups);
@@ -685,7 +688,7 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui) {
}
slots.push(VerticalSlot { y: ui.cursor().top(), position: index, group: group_id, indent_group });
let indent = group_id.map(|id| (group_depth(&groups, id) + 1) as f32 * 14.0).unwrap_or(0.0);
let has_patterns = matches!(kind, FeatureKind::Sketch { .. }) && crate::sketch_pattern_tree::has_patterns(app, *id);
let has_patterns = matches!(kind, TimelineKind::Sketch) && crate::sketch_pattern_tree::has_patterns(app, *id);
let pattern_key = format!("timeline/sketch/{id}/patterns");
let patterns_open = crate::browser::is_open(app, &pattern_key, true);
let (rect, response) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::click_and_drag());
@@ -771,10 +774,7 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui) {
if response.double_clicked() && !on_fold {
edit = Some(*id);
}
let sketch = match kind {
FeatureKind::Extrude { sketch, .. } | FeatureKind::Revolve { sketch, .. } | FeatureKind::Sweep { sketch, .. } => Some(*sketch),
_ => None,
};
let sketch = kind.profile_sketch();
if response.secondary_clicked() && !selected {
select_only(app, TimelineItem::Feature(*id));
}