perf(scene): make geometry edits incremental
Avoid full document, cache, and GPU rebuilds for entity deltas so add, delete, history, selection, and live polyline edits stay responsive on large drawings.
This commit is contained in:
parent
73b8dbe9d4
commit
05b044f367
32 changed files with 1929 additions and 509 deletions
|
|
@ -351,9 +351,9 @@ impl OpenCADStudio {
|
||||||
self.update_cont_anchor(&entity);
|
self.update_cont_anchor(&entity);
|
||||||
}
|
}
|
||||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||||
// A plain drawable added on an existing layer touches only the
|
// Ordinary drawables, viewports and raster images use targeted
|
||||||
// one new entity; a block/image/dimension/viewport add also
|
// entity/object deltas. Block sentinels and novel layers retain
|
||||||
// mutates layers/objects/blocks → keep the full snapshot.
|
// the structure snapshot fallback.
|
||||||
let delta_safe = self.delta_add_safe(i, &entity);
|
let delta_safe = self.delta_add_safe(i, &entity);
|
||||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||||
self.commit_entity(entity);
|
self.commit_entity(entity);
|
||||||
|
|
@ -401,7 +401,7 @@ impl OpenCADStudio {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
||||||
let pending = self.begin_undo(i, label, 1, false);
|
let pending = self.begin_undo(i, label, 1, true);
|
||||||
let handle = self.commit_entity_handle(
|
let handle = self.commit_entity_handle(
|
||||||
acadrust::EntityType::Viewport(viewport),
|
acadrust::EntityType::Viewport(viewport),
|
||||||
);
|
);
|
||||||
|
|
@ -475,7 +475,7 @@ impl OpenCADStudio {
|
||||||
let created_boundary = boundary.is_some();
|
let created_boundary = boundary.is_some();
|
||||||
let touched = 2;
|
let touched = 2;
|
||||||
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
||||||
let pending = self.begin_undo(i, label, touched, false);
|
let pending = self.begin_undo(i, label, touched, true);
|
||||||
let clip_handle = match boundary {
|
let clip_handle = match boundary {
|
||||||
Some(mut boundary) => {
|
Some(mut boundary) => {
|
||||||
// A non-rectangular viewport owns a helper boundary
|
// A non-rectangular viewport owns a helper boundary
|
||||||
|
|
@ -1126,6 +1126,16 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CmdResult::FinalizeLiveEntity(handle) => {
|
||||||
|
// The live geometry already matches the command's committed
|
||||||
|
// vertices. Close the deferred history image and UI state
|
||||||
|
// without publishing a redundant Modified delta.
|
||||||
|
self.finish_live_entity_history(i, handle);
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
}
|
||||||
CmdResult::RemoveLiveEntity(handle) => {
|
CmdResult::RemoveLiveEntity(handle) => {
|
||||||
// The command backed off below a valid entity (PLINE Undo at
|
// The command backed off below a valid entity (PLINE Undo at
|
||||||
// one remaining vertex): take the live entity out of the
|
// one remaining vertex): take the live entity out of the
|
||||||
|
|
@ -1289,9 +1299,14 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// Color / linetype / lineweight are baked into the cached
|
// Color / linetype / lineweight are baked into the cached
|
||||||
// wires at tessellation time; bump the geometry epoch so the
|
// wires at tessellation time; re-tessellate only the matched
|
||||||
// matched objects repaint instead of holding their old look.
|
// objects instead of rebuilding a large drawing.
|
||||||
self.tabs[i].scene.bump_geometry();
|
let changes: Vec<_> = dest
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&format!("Properties matched to {} object(s).", dest.len()));
|
.push_info(&format!("Properties matched to {} object(s).", dest.len()));
|
||||||
|
|
@ -1661,9 +1676,10 @@ impl OpenCADStudio {
|
||||||
if changed {
|
if changed {
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// Repaint immediately (wide-band fill included).
|
// Repaint immediately (wide-band fill included).
|
||||||
self.tabs[i].scene.mark_entity_dirty(handle);
|
|
||||||
self.tabs[i].scene.refresh_fill_model(handle);
|
self.tabs[i].scene.refresh_fill_model(handle);
|
||||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||||
self.command_line.push_output("PEDIT: applied.");
|
self.command_line.push_output("PEDIT: applied.");
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2093,16 +2109,12 @@ impl OpenCADStudio {
|
||||||
// moved entities so the viewport reflects the stretch right away
|
// moved entities so the viewport reflects the stretch right away
|
||||||
// instead of only on the next unrelated redraw. See #95.
|
// instead of only on the next unrelated redraw. See #95.
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
if structural {
|
|
||||||
self.tabs[i].scene.bump_geometry();
|
|
||||||
} else {
|
|
||||||
let changes: Vec<_> = changed_handles
|
let changes: Vec<_> = changed_handles
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
.collect();
|
.collect();
|
||||||
self.tabs[i].scene.bump_entities(&changes);
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
|
|
@ -2928,9 +2940,15 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The wires were tessellated before the filters existed; force a rebuild
|
// The wires were tessellated before the filters existed; refresh only
|
||||||
// so the clip is applied to the freshly-pasted, now-filtered inserts.
|
// the freshly-pasted entities whose clip object graph was attached.
|
||||||
self.tabs[i].scene.bump_geometry();
|
let changes: Vec<_> = by_index
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|handle| !handle.is_null())
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recreate the captured xdictionary subtrees in this document (fresh
|
/// Recreate the captured xdictionary subtrees in this document (fresh
|
||||||
|
|
@ -3098,9 +3116,11 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||||
};
|
};
|
||||||
|
|
||||||
let effective_spacing = if spacing <= 0.0 { 10.0 } else { spacing };
|
let effective_spacing = if spacing <= 0.0 { 10.0 } else { spacing };
|
||||||
|
let mut changes = Vec::new();
|
||||||
for (idx, &hv) in other_vals.iter().enumerate() {
|
for (idx, &hv) in other_vals.iter().enumerate() {
|
||||||
let h = acadrust::Handle::from(hv);
|
let h = acadrust::Handle::from(hv);
|
||||||
let target = base_coord + effective_spacing * (idx + 1) as f64;
|
let target = base_coord + effective_spacing * (idx + 1) as f64;
|
||||||
|
let mut changed = false;
|
||||||
if let Some(acadrust::EntityType::Dimension(dim)) = scene.document.get_entity_mut(h) {
|
if let Some(acadrust::EntityType::Dimension(dim)) = scene.document.get_entity_mut(h) {
|
||||||
// Slide this dim's definition point along perp so its perpendicular
|
// Slide this dim's definition point along perp so its perpendicular
|
||||||
// coordinate equals `target`; update both the struct field (render)
|
// coordinate equals `target`; update both the struct field (render)
|
||||||
|
|
@ -3115,19 +3135,26 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||||
Dimension::Linear(d) => {
|
Dimension::Linear(d) => {
|
||||||
slide(&mut d.definition_point);
|
slide(&mut d.definition_point);
|
||||||
d.base.definition_point = d.definition_point;
|
d.base.definition_point = d.definition_point;
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
Dimension::Aligned(d) => {
|
Dimension::Aligned(d) => {
|
||||||
slide(&mut d.definition_point);
|
slide(&mut d.definition_point);
|
||||||
d.base.definition_point = d.definition_point;
|
d.base.definition_point = d.definition_point;
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The dimension line moved, so its baked *D block is stale — drop it so
|
if changed {
|
||||||
// the next save re-bakes it (no-op for non-dimensions). (#181)
|
// The dimension line moved, so its baked *D block is stale — drop it
|
||||||
|
// so the next save re-bakes it. (#181)
|
||||||
scene.invalidate_dim_block_recorded(h);
|
scene.invalidate_dim_block_recorded(h);
|
||||||
|
changes.push((h, crate::scene::ChangeKind::Modified));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changes.is_empty() {
|
||||||
|
scene.bump_entities(&changes);
|
||||||
}
|
}
|
||||||
scene.bump_geometry();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── MLEADERALIGN helper ───────────────────────────────────────────────────────
|
// ── MLEADERALIGN helper ───────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -370,6 +370,7 @@ impl OpenCADStudio {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
self.push_undo_snapshot(i, "ATTSYNC");
|
self.push_undo_snapshot(i, "ATTSYNC");
|
||||||
let mut synced = 0usize;
|
let mut synced = 0usize;
|
||||||
|
let mut changes = Vec::new();
|
||||||
for e in self.tabs[i].scene.document.entities_mut() {
|
for e in self.tabs[i].scene.document.entities_mut() {
|
||||||
if let acadrust::EntityType::Insert(ins) = e {
|
if let acadrust::EntityType::Insert(ins) = e {
|
||||||
if ins.block_name.eq_ignore_ascii_case(&block) {
|
if ins.block_name.eq_ignore_ascii_case(&block) {
|
||||||
|
|
@ -390,10 +391,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
synced += 1;
|
synced += 1;
|
||||||
|
changes.push((ins.common.handle, crate::scene::ChangeKind::Modified));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"ATTSYNC: synchronised {synced} insert(s) of \"{block}\" against {} attribute definition(s).",
|
"ATTSYNC: synchronised {synced} insert(s) of \"{block}\" against {} attribute definition(s).",
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.bump_geometry();
|
let changes: Vec<_> = handles
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("JUSTIFYTEXT: updated {n} text object(s)."));
|
.push_output(&format!("JUSTIFYTEXT: updated {n} text object(s)."));
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +257,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.bump_geometry();
|
let changes: Vec<_> = handles
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("TCASE: updated {n} text object(s)."));
|
.push_output(&format!("TCASE: updated {n} text object(s)."));
|
||||||
}
|
}
|
||||||
|
|
@ -397,7 +405,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.bump_geometry();
|
let changes: Vec<_> = handles
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"TEXTFIT: fitted {n} text object(s) to width {target}."
|
"TEXTFIT: fitted {n} text object(s) to width {target}."
|
||||||
));
|
));
|
||||||
|
|
@ -459,7 +471,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.bump_geometry();
|
let changes: Vec<_> = texts
|
||||||
|
.iter()
|
||||||
|
.map(|(handle, _, _)| (*handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"TCOUNT: numbered {} text object(s) from {start}.",
|
"TCOUNT: numbered {} text object(s) from {start}.",
|
||||||
texts.len()
|
texts.len()
|
||||||
|
|
|
||||||
|
|
@ -686,7 +686,11 @@ impl OpenCADStudio {
|
||||||
n += 1;
|
n += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
let changes: Vec<_> = handles
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"OBJECTSCALE: marked {n} object(s) annotative (they scale with the annotation scale)."
|
"OBJECTSCALE: marked {n} object(s) annotative (they scale with the annotation scale)."
|
||||||
|
|
@ -776,6 +780,7 @@ impl OpenCADStudio {
|
||||||
let v = v.min(100);
|
let v = v.min(100);
|
||||||
self.push_undo_snapshot(i, "ADJUST");
|
self.push_undo_snapshot(i, "ADJUST");
|
||||||
let mut changed = 0usize;
|
let mut changed = 0usize;
|
||||||
|
let mut changed_handles = Vec::new();
|
||||||
for h in &handles {
|
for h in &handles {
|
||||||
if let Some(acadrust::EntityType::RasterImage(img)) = self.tabs[i]
|
if let Some(acadrust::EntityType::RasterImage(img)) = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -787,14 +792,17 @@ impl OpenCADStudio {
|
||||||
"BRIGHTNESS" => {
|
"BRIGHTNESS" => {
|
||||||
img.brightness = v;
|
img.brightness = v;
|
||||||
changed += 1;
|
changed += 1;
|
||||||
|
changed_handles.push(*h);
|
||||||
}
|
}
|
||||||
"CONTRAST" => {
|
"CONTRAST" => {
|
||||||
img.contrast = v;
|
img.contrast = v;
|
||||||
changed += 1;
|
changed += 1;
|
||||||
|
changed_handles.push(*h);
|
||||||
}
|
}
|
||||||
"FADE" => {
|
"FADE" => {
|
||||||
img.fade = v;
|
img.fade = v;
|
||||||
changed += 1;
|
changed += 1;
|
||||||
|
changed_handles.push(*h);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -802,7 +810,14 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
if changed > 0 {
|
if changed > 0 {
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.bump_geometry();
|
for &handle in &changed_handles {
|
||||||
|
self.tabs[i].scene.reseed_derived_caches(handle);
|
||||||
|
}
|
||||||
|
let changes: Vec<_> = changed_handles
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("ADJUST: {action} = {v} on {changed} image(s)."));
|
.push_output(&format!("ADJUST: {action} = {v} on {changed} image(s)."));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -860,7 +875,7 @@ impl OpenCADStudio {
|
||||||
let hdr = &mut self.tabs[i].scene.document.header;
|
let hdr = &mut self.tabs[i].scene.document.header;
|
||||||
hdr.current_annotation_scale = arg.clone();
|
hdr.current_annotation_scale = arg.clone();
|
||||||
hdr.annotation_scale_value = 1.0 / v as f64;
|
hdr.annotation_scale_value = 1.0 / v as f64;
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_annotation_dependencies();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Annotation scale: {arg}"));
|
.push_output(&format!("Annotation scale: {arg}"));
|
||||||
|
|
@ -1014,7 +1029,6 @@ impl OpenCADStudio {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.add_entity_clone(acadrust::EntityType::Table(table));
|
.add_entity_clone(acadrust::EntityType::Table(table));
|
||||||
self.tabs[i].scene.bump_geometry();
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"DATALINK: imported {nrows}×{ncols} cells into a table at the origin."
|
"DATALINK: imported {nrows}×{ncols} cells into a table at the origin."
|
||||||
|
|
@ -1061,7 +1075,6 @@ impl OpenCADStudio {
|
||||||
.scene
|
.scene
|
||||||
.add_entity_clone(acadrust::EntityType::Point(p));
|
.add_entity_clone(acadrust::EntityType::Point(p));
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"LANDXMLIMPORT: imported {} survey point(s). Use ZOOM EXTENTS to view.",
|
"LANDXMLIMPORT: imported {} survey point(s). Use ZOOM EXTENTS to view.",
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,9 @@ impl OpenCADStudio {
|
||||||
self.grip_hover = None;
|
self.grip_hover = None;
|
||||||
self.grip_popup = None;
|
self.grip_popup = None;
|
||||||
self.visibility_popup = None;
|
self.visibility_popup = None;
|
||||||
self.tabs[i].scene.bump_geometry();
|
// Entering BEDIT changes which block is assembled, not the
|
||||||
|
// cached geometry of that block's entities.
|
||||||
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
// Frame the camera on the block's own geometry (block-local, near
|
// Frame the camera on the block's own geometry (block-local, near
|
||||||
// origin) — fit_all() goes through current_layout_block_handle so
|
// origin) — fit_all() goes through current_layout_block_handle so
|
||||||
// it already scopes to the edited block. Without this the view
|
// it already scopes to the edited block. Without this the view
|
||||||
|
|
|
||||||
|
|
@ -129,9 +129,14 @@ impl OpenCADStudio {
|
||||||
self.push_undo_snapshot(i, "LAYER COLOR");
|
self.push_undo_snapshot(i, "LAYER COLOR");
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// By-layer colour is baked into every wire on
|
// By-layer colour is baked into every wire on
|
||||||
// this layer — re-tessellate so they repaint
|
// this layer. The dependency index maps block
|
||||||
// immediately (issue #231 class).
|
// children back to their top-level INSERTs, so
|
||||||
self.tabs[i].scene.bump_geometry();
|
// unrelated entities stay warm.
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.invalidate_layer_dependencies(std::slice::from_ref(
|
||||||
|
&layer_name,
|
||||||
|
));
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"LAYER: '{}' color set to ACI {}.",
|
"LAYER: '{}' color set to ACI {}.",
|
||||||
layer_name, idx
|
layer_name, idx
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,10 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.document.layers.remove(&src);
|
self.tabs[i].scene.document.layers.remove(&src);
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_dependency_index();
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.invalidate_layer_dependencies(std::slice::from_ref(&dst));
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
|
|
|
||||||
|
|
@ -725,7 +725,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.bump_geometry();
|
let changes: Vec<_> = handles
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"SETBYLAYER: reset {changed} entity/entities to ByLayer."
|
"SETBYLAYER: reset {changed} entity/entities to ByLayer."
|
||||||
));
|
));
|
||||||
|
|
@ -1593,7 +1597,9 @@ impl OpenCADStudio {
|
||||||
// TEXTFILL reset the glyph atlas; re-tessellate so text picks
|
// TEXTFILL reset the glyph atlas; re-tessellate so text picks
|
||||||
// up the re-baked filled / hollow tiles.
|
// up the re-baked filled / hollow tiles.
|
||||||
if name == "TEXTFILL" {
|
if name == "TEXTFILL" {
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.invalidate_text_geometry_dependencies();
|
||||||
}
|
}
|
||||||
// ORTHOMODE / OSMODE set the header directly; mirror them into
|
// ORTHOMODE / OSMODE set the header directly; mirror them into
|
||||||
// the live Ortho / running OSNAP so the constraint + status
|
// the live Ortho / running OSNAP so the constraint + status
|
||||||
|
|
@ -1902,7 +1908,7 @@ impl OpenCADStudio {
|
||||||
self.push_undo_snapshot(i, "PDMODE");
|
self.push_undo_snapshot(i, "PDMODE");
|
||||||
self.tabs[i].scene.document.header.point_display_mode = v;
|
self.tabs[i].scene.document.header.point_display_mode = v;
|
||||||
// Point glyphs are built at tessellation time — rebuild them.
|
// Point glyphs are built at tessellation time — rebuild them.
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_point_dependencies();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(&format!("PDMODE set to {v}"));
|
self.command_line.push_output(&format!("PDMODE set to {v}"));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2046,7 +2052,7 @@ impl OpenCADStudio {
|
||||||
} else if let Ok(v) = val_str.parse::<f64>() {
|
} else if let Ok(v) = val_str.parse::<f64>() {
|
||||||
self.push_undo_snapshot(i, "PDSIZE");
|
self.push_undo_snapshot(i, "PDSIZE");
|
||||||
self.tabs[i].scene.document.header.point_display_size = v;
|
self.tabs[i].scene.document.header.point_display_size = v;
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_point_dependencies();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("PDSIZE set to {v:.4}"));
|
.push_output(&format!("PDSIZE set to {v:.4}"));
|
||||||
|
|
|
||||||
|
|
@ -956,7 +956,9 @@ impl OpenCADStudio {
|
||||||
// render-side `sort_cache` rebuilds per geometry epoch.
|
// render-side `sort_cache` rebuilds per geometry epoch.
|
||||||
// Bump it so the new draw order shows immediately
|
// Bump it so the new draw order shows immediately
|
||||||
// instead of waiting for an unrelated geometry change.
|
// instead of waiting for an unrelated geometry change.
|
||||||
self.tabs[i].scene.bump_geometry();
|
// Draw order changes submission order only; all
|
||||||
|
// per-entity tessellation remains valid.
|
||||||
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
} else {
|
} else {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
|
|
|
||||||
|
|
@ -623,52 +623,30 @@ impl OpenCADStudio {
|
||||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy is delta-safe only when no target is a Dimension and no complete
|
/// Dimension copies still need a full snapshot because they clone a fresh
|
||||||
/// group is copied: dimensions clone fresh anonymous `*D` block records,
|
/// anonymous `*D` block record. Complete group copies record their exact
|
||||||
/// and complete group copies add Group objects / dictionary entries. Both
|
/// Group/dictionary object entries and remain delta-safe.
|
||||||
/// are non-entity state a pure-entity delta can't restore.
|
|
||||||
pub(super) fn delta_copy_safe(&self, i: usize, handles: &[Handle]) -> bool {
|
pub(super) fn delta_copy_safe(&self, i: usize, handles: &[Handle]) -> bool {
|
||||||
use acadrust::objects::ObjectType;
|
|
||||||
let doc = &self.tabs[i].scene.document;
|
let doc = &self.tabs[i].scene.document;
|
||||||
let handle_set: HashSet<Handle> = handles.iter().copied().collect();
|
!handles
|
||||||
let copies_dimension = handles
|
|
||||||
.iter()
|
.iter()
|
||||||
.any(|h| matches!(doc.get_entity(*h), Some(EntityType::Dimension(_))));
|
.any(|h| matches!(doc.get_entity(*h), Some(EntityType::Dimension(_))))
|
||||||
let copies_complete_group = doc.objects.values().any(|o| match o {
|
|
||||||
ObjectType::Group(g) => {
|
|
||||||
!g.entities.is_empty() && g.entities.iter().all(|h| handle_set.contains(h))
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
!copies_dimension && !copies_complete_group
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Erase is delta-safe only when no target belongs to a group: erasing a
|
/// Erase records any affected Group/object dictionary entries alongside the
|
||||||
/// grouped entity rewrites the group's membership in `document.objects`.
|
/// entity before-images, so it never needs a whole-structure snapshot.
|
||||||
pub(super) fn delta_erase_safe(&self, i: usize, handles: &[Handle]) -> bool {
|
pub(super) fn delta_erase_safe(&self, _i: usize, _handles: &[Handle]) -> bool {
|
||||||
use acadrust::objects::ObjectType;
|
true
|
||||||
let doc = &self.tabs[i].scene.document;
|
|
||||||
let handle_set: HashSet<Handle> = handles.iter().copied().collect();
|
|
||||||
!doc.objects.values().any(|o| match o {
|
|
||||||
ObjectType::Group(g) => g.entities.iter().any(|h| handle_set.contains(h)),
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add is delta-safe only for a plain drawable on an already-existing layer:
|
/// Add is delta-safe on an already-existing layer unless it creates block
|
||||||
/// a block / image add also creates block records, image definitions or
|
/// records. RasterImage definitions are captured as exact object-map deltas;
|
||||||
/// layers (non-entity state). INSERT only references an existing block and
|
/// Viewport commits explicitly record their added entity handle.
|
||||||
/// is entity-delta safe.
|
|
||||||
pub(super) fn delta_add_safe(&self, i: usize, entity: &EntityType) -> bool {
|
pub(super) fn delta_add_safe(&self, i: usize, entity: &EntityType) -> bool {
|
||||||
if matches!(
|
if matches!(
|
||||||
entity,
|
entity,
|
||||||
EntityType::Block(_)
|
EntityType::Block(_)
|
||||||
| EntityType::BlockEnd(_)
|
| EntityType::BlockEnd(_)
|
||||||
| EntityType::RasterImage(_)
|
|
||||||
// A viewport commit routes through add_entity_to_layout +
|
|
||||||
// bump_geometry_no_blocks, bypassing the recorded Scene::add_entity
|
|
||||||
// — nothing would be captured, so keep the full snapshot.
|
|
||||||
| EntityType::Viewport(_)
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -706,14 +684,25 @@ impl OpenCADStudio {
|
||||||
pending.label
|
pending.label
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> = rec
|
let (entity_before, object_before) = rec.into_recorded_images();
|
||||||
.into_before_images()
|
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> = entity_before
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(h, before)| {
|
.map(|(h, before)| {
|
||||||
let after = self.tabs[i].scene.document.get_entity_arc(h);
|
let after = self.tabs[i].scene.document.get_entity_arc(h);
|
||||||
(h, before, after)
|
(h, before, after)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let objects: Vec<ObjectEntryDelta> = object_before
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(handle, before)| {
|
||||||
|
let after = self.tabs[i].scene.document.objects.get(&handle).cloned();
|
||||||
|
(before != after).then_some(ObjectEntryDelta {
|
||||||
|
handle,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||||
let dirty_after = self.tabs[i].dirty;
|
let dirty_after = self.tabs[i].dirty;
|
||||||
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
|
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
|
||||||
|
|
@ -736,6 +725,9 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if structure.is_none() && !objects.is_empty() {
|
||||||
|
structure = Some(StructureSnapshot::Objects(objects));
|
||||||
|
}
|
||||||
let delta = DeltaSnapshot {
|
let delta = DeltaSnapshot {
|
||||||
entities,
|
entities,
|
||||||
current_layout_before: pending.current_layout,
|
current_layout_before: pending.current_layout,
|
||||||
|
|
@ -849,7 +841,6 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].scene.document.objects.remove(&entry.handle);
|
self.tabs[i].scene.document.objects.remove(&entry.handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.invalidate_dependency_index();
|
|
||||||
}
|
}
|
||||||
StructureSnapshot::Styles {
|
StructureSnapshot::Styles {
|
||||||
before,
|
before,
|
||||||
|
|
@ -909,13 +900,21 @@ impl OpenCADStudio {
|
||||||
(&snapshot.after, &snapshot.selected_after)
|
(&snapshot.after, &snapshot.selected_after)
|
||||||
};
|
};
|
||||||
let scene = &mut self.tabs[i].scene;
|
let scene = &mut self.tabs[i].scene;
|
||||||
|
let mut affected = scene.object_isolation.hidden.clone();
|
||||||
|
affected.extend(state.hidden.iter().copied());
|
||||||
scene.object_isolation = state.clone();
|
scene.object_isolation = state.clone();
|
||||||
scene.selected = selected
|
scene.selected = selected
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|handle| scene.document.get_entity(*handle).is_some())
|
.filter(|handle| scene.document.get_entity(*handle).is_some())
|
||||||
.collect();
|
.collect();
|
||||||
scene.bump_geometry_no_blocks();
|
let changes: Vec<_> = affected
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
if !changes.is_empty() {
|
||||||
|
scene.bump_entities(&changes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform the one expensive cache/UI synchronization required after a
|
/// Perform the one expensive cache/UI synchronization required after a
|
||||||
|
|
@ -925,7 +924,7 @@ impl OpenCADStudio {
|
||||||
&mut self,
|
&mut self,
|
||||||
i: usize,
|
i: usize,
|
||||||
had_full: bool,
|
had_full: bool,
|
||||||
structure_changed: bool,
|
layer_panel_changed: bool,
|
||||||
changes: &[(Handle, crate::scene::ChangeKind)],
|
changes: &[(Handle, crate::scene::ChangeKind)],
|
||||||
) {
|
) {
|
||||||
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
||||||
|
|
@ -970,7 +969,7 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].active_grip = None;
|
self.tabs[i].active_grip = None;
|
||||||
if structure_changed {
|
if layer_panel_changed {
|
||||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||||
let vp_info = self.tabs[i].scene.viewport_list();
|
let vp_info = self.tabs[i].scene.viewport_list();
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
|
|
@ -1001,7 +1000,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
let mut last_label = String::new();
|
let mut last_label = String::new();
|
||||||
let mut had_full = false;
|
let mut had_full = false;
|
||||||
let mut structure_changed = false;
|
let mut layer_panel_changed = false;
|
||||||
let mut changes = Vec::new();
|
let mut changes = Vec::new();
|
||||||
for _ in 0..steps {
|
for _ in 0..steps {
|
||||||
let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else {
|
let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else {
|
||||||
|
|
@ -1013,7 +1012,19 @@ impl OpenCADStudio {
|
||||||
// Symmetric: undo applies the before side, then the same
|
// Symmetric: undo applies the before side, then the same
|
||||||
// delta rides to the redo stack (it still holds the after
|
// delta rides to the redo stack (it still holds the after
|
||||||
// side) — no current-state capture needed.
|
// side) — no current-state capture needed.
|
||||||
structure_changed |= d.structure.is_some();
|
layer_panel_changed |= d.structure.as_ref().is_some_and(|structure| {
|
||||||
|
matches!(
|
||||||
|
structure,
|
||||||
|
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
|
||||||
|
)
|
||||||
|
}) || d.entities.iter().any(|(_, before, after)| {
|
||||||
|
before
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||||
|
|| after
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||||
|
});
|
||||||
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
||||||
changes.extend(self.apply_delta_state(i, &mut d, true));
|
changes.extend(self.apply_delta_state(i, &mut d, true));
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
|
|
@ -1030,7 +1041,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.finish_history_apply(i, had_full, structure_changed, &changes);
|
self.finish_history_apply(i, had_full, layer_panel_changed, &changes);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Undo: {last_label}"));
|
.push_output(&format!("Undo: {last_label}"));
|
||||||
}
|
}
|
||||||
|
|
@ -1047,7 +1058,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
let mut last_label = String::new();
|
let mut last_label = String::new();
|
||||||
let mut had_full = false;
|
let mut had_full = false;
|
||||||
let mut structure_changed = false;
|
let mut layer_panel_changed = false;
|
||||||
let mut changes = Vec::new();
|
let mut changes = Vec::new();
|
||||||
for _ in 0..steps {
|
for _ in 0..steps {
|
||||||
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
|
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
|
||||||
|
|
@ -1056,7 +1067,19 @@ impl OpenCADStudio {
|
||||||
last_label = snapshot.label().to_string();
|
last_label = snapshot.label().to_string();
|
||||||
match snapshot {
|
match snapshot {
|
||||||
HistorySnapshot::Delta(mut d) => {
|
HistorySnapshot::Delta(mut d) => {
|
||||||
structure_changed |= d.structure.is_some();
|
layer_panel_changed |= d.structure.as_ref().is_some_and(|structure| {
|
||||||
|
matches!(
|
||||||
|
structure,
|
||||||
|
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
|
||||||
|
)
|
||||||
|
}) || d.entities.iter().any(|(_, before, after)| {
|
||||||
|
before
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||||
|
|| after
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||||
|
});
|
||||||
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
||||||
changes.extend(self.apply_delta_state(i, &mut d, false));
|
changes.extend(self.apply_delta_state(i, &mut d, false));
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
|
|
@ -1073,7 +1096,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.finish_history_apply(i, had_full, structure_changed, &changes);
|
self.finish_history_apply(i, had_full, layer_panel_changed, &changes);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Redo: {last_label}"));
|
.push_output(&format!("Redo: {last_label}"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1089,7 +1089,9 @@ impl super::OpenCADStudio {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
} else {
|
} else {
|
||||||
// Align new MText to the active UCS (text runs along the UCS X axis).
|
// Align new MText to the active UCS (text runs along the UCS X axis).
|
||||||
|
|
@ -1140,7 +1142,9 @@ impl super::OpenCADStudio {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
} else {
|
} else {
|
||||||
mt.rotation = self.tabs[i].ucs_rotation_angle();
|
mt.rotation = self.tabs[i].ucs_rotation_angle();
|
||||||
|
|
|
||||||
|
|
@ -1965,15 +1965,14 @@ impl OpenCADStudio {
|
||||||
Ok(new_handle) => {
|
Ok(new_handle) => {
|
||||||
if self.tabs[i].scene.is_recording_undo() {
|
if self.tabs[i].scene.is_recording_undo() {
|
||||||
self.tabs[i].scene.record_undo_before(new_handle, None);
|
self.tabs[i].scene.record_undo_before(new_handle, None);
|
||||||
self.tabs[i].scene.poison_undo_recording();
|
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.auto_fit_viewport(new_handle);
|
self.tabs[i].scene.auto_fit_viewport(new_handle);
|
||||||
// Adding a viewport straight onto the document layout
|
// Adding a viewport straight onto the document layout
|
||||||
// bypasses Scene::add_entity, which is what normally
|
// bypasses Scene::add_entity; publish the exact new handle
|
||||||
// invalidates the wire-tessellation cache. Without this the
|
// so only its border is tessellated.
|
||||||
// new viewport's border isn't tessellated until the next
|
self.tabs[i]
|
||||||
// zoom/pan forces a rebuild.
|
.scene
|
||||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
.bump_entities(&[(new_handle, crate::scene::ChangeKind::Added)]);
|
||||||
Some(new_handle)
|
Some(new_handle)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -581,10 +581,7 @@ impl OpenCADStudio {
|
||||||
pub(super) fn style_stage_commit(&mut self) {
|
pub(super) fn style_stage_commit(&mut self) {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let Some(stage) = self.style_stage.take() else {
|
let Some(stage) = self.style_stage.take() else {
|
||||||
// No active transaction — still refresh the drawing so a bare Apply
|
// No staged edits: a bare Apply has no geometry work to publish.
|
||||||
// is harmless.
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.tabs[i].scene.bump_geometry();
|
|
||||||
self.sync_ribbon_styles();
|
self.sync_ribbon_styles();
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -820,7 +817,7 @@ impl OpenCADStudio {
|
||||||
if !resolves {
|
if !resolves {
|
||||||
self.tabs[i].scene.document.header.current_annotation_scale = snap.current.clone();
|
self.tabs[i].scene.document.header.current_annotation_scale = snap.current.clone();
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_annotation_dependencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Begin a staging transaction for a freshly-opened scale manager.
|
/// Begin a staging transaction for a freshly-opened scale manager.
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,9 @@ impl super::OpenCADStudio {
|
||||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(h) {
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(h) {
|
||||||
write_text_field(entity, ed.field, ed.value.clone());
|
write_text_field(entity, ed.field, ed.value.clone());
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
} else {
|
} else {
|
||||||
let mut t = Text::with_value(
|
let mut t = Text::with_value(
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.invalidate_dim_block_recorded(pending.handle);
|
.invalidate_dim_block_recorded(pending.handle);
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_entities(&[(
|
||||||
|
pending.handle,
|
||||||
|
crate::scene::ChangeKind::Modified,
|
||||||
|
)]);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.refresh_selected_grips();
|
self.refresh_selected_grips();
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
|
|
@ -534,7 +537,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
crate::scene::model::object::GripMenuAction::RemoveLeader,
|
crate::scene::model::object::GripMenuAction::RemoveLeader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||||
self.refresh_selected_grips();
|
self.refresh_selected_grips();
|
||||||
}
|
}
|
||||||
// Cancel an in-progress grip drag: restore the edited
|
// Cancel an in-progress grip drag: restore the edited
|
||||||
|
|
@ -649,7 +654,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.sync_with_viewports(&doc_layers, vp_info);
|
.sync_with_viewports(&doc_layers, vp_info);
|
||||||
self.tabs[i].scene.bump_geometry();
|
// Per-viewport layer visibility changes the resident
|
||||||
|
// assembly, not any entity's tessellated geometry.
|
||||||
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1022,7 +1029,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.invalidate_dim_block_recorded(popup.handle);
|
.invalidate_dim_block_recorded(popup.handle);
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(popup.handle, crate::scene::ChangeKind::Modified)]);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.refresh_selected_grips();
|
self.refresh_selected_grips();
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
|
|
@ -1534,7 +1543,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
.get(&value)
|
.get(&value)
|
||||||
.map(|br| br.name.clone());
|
.map(|br| br.name.clone());
|
||||||
if let Some(canon) = canon {
|
if let Some(canon) = canon {
|
||||||
let mut changed = false;
|
let mut changes = Vec::new();
|
||||||
for &handle in &handles {
|
for &handle in &handles {
|
||||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1544,14 +1553,15 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
{
|
{
|
||||||
if ins.block_name != canon {
|
if ins.block_name != canon {
|
||||||
ins.block_name = canon.clone();
|
ins.block_name = canon.clone();
|
||||||
changed = true;
|
changes.push((
|
||||||
|
handle,
|
||||||
|
crate::scene::ChangeKind::Modified,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changed {
|
if !changes.is_empty() {
|
||||||
// The picked block may not be in the defn cache
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
// yet — rebuild block definitions too.
|
|
||||||
self.tabs[i].scene.bump_geometry();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if field == "plot_style" {
|
} else if field == "plot_style" {
|
||||||
|
|
@ -1804,7 +1814,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
if protected {
|
if protected {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut changed = false;
|
let mut changes = Vec::new();
|
||||||
for &handle in handles {
|
for &handle in handles {
|
||||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1814,12 +1824,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
{
|
{
|
||||||
if ins.block_name != canon {
|
if ins.block_name != canon {
|
||||||
ins.block_name = canon.clone();
|
ins.block_name = canon.clone();
|
||||||
changed = true;
|
changes.push((handle, crate::scene::ChangeKind::Modified));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changed {
|
if !changes.is_empty() {
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1922,9 +1922,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// The paper sheet fill is cached; bump geometry so the new
|
// The paper sheet fill is cached by epoch, while document
|
||||||
// sheet size re-tessellates and shows immediately.
|
// entity tessellation is unaffected by the paper size.
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
self.command_line.push_info(&format!(
|
self.command_line.push_info(&format!(
|
||||||
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
|
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
|
||||||
center={center} rot={rotation}°"
|
center={center} rot={rotation}°"
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,22 @@ fn is_modal_blocked_key_msg(msg: &Message) -> bool {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn perf_message_label(msg: &Message) -> &'static str {
|
||||||
|
match msg {
|
||||||
|
Message::ViewportLeftPress | Message::PanePress(_) => "pointer-down",
|
||||||
|
Message::ViewportLeftRelease | Message::PaneRelease(_) => "pointer-up",
|
||||||
|
Message::ViewportMove(_) | Message::PaneMove(_, _) => "pointer-move",
|
||||||
|
Message::CommandFinalize => "command-finalize",
|
||||||
|
Message::CommandEscape => "command-escape",
|
||||||
|
Message::Undo | Message::UndoMany(_) => "undo",
|
||||||
|
Message::Redo | Message::RedoMany(_) => "redo",
|
||||||
|
Message::DeleteSelected => "delete",
|
||||||
|
Message::HoverDwellTick => "hover-dwell",
|
||||||
|
Message::InteractionIndexReady { .. } => "interaction-index-ready",
|
||||||
|
_ => "other",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
||||||
|
|
||||||
fn format_size(bytes: u64) -> String {
|
fn format_size(bytes: u64) -> String {
|
||||||
|
|
@ -139,6 +155,8 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
||||||
|
let perf_started = crate::perf::enabled().then(Instant::now);
|
||||||
|
let perf_label = perf_message_label(&msg);
|
||||||
// A modal dialog must capture the keyboard the same way it already
|
// A modal dialog must capture the keyboard the same way it already
|
||||||
// captures the mouse. Otherwise keystrokes from the global key
|
// captures the mouse. Otherwise keystrokes from the global key
|
||||||
// subscription leak past the modal into the command line and fire as
|
// subscription leak past the modal into the command line and fire as
|
||||||
|
|
@ -185,6 +203,15 @@ impl OpenCADStudio {
|
||||||
self.snapper.clear_tracking();
|
self.snapper.clear_tracking();
|
||||||
self.otrack_active = None;
|
self.otrack_active = None;
|
||||||
}
|
}
|
||||||
|
if let Some(started) = perf_started {
|
||||||
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if elapsed_ms >= 5.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] update {:>7.1}ms message={perf_label}",
|
||||||
|
elapsed_ms,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
task
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +250,7 @@ impl OpenCADStudio {
|
||||||
crate::scene::text::web_font::insert(script, Some(bytes));
|
crate::scene::text::web_font::insert(script, Some(bytes));
|
||||||
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
||||||
for tab in self.tabs.iter_mut() {
|
for tab in self.tabs.iter_mut() {
|
||||||
tab.scene.bump_geometry();
|
tab.scene.invalidate_text_geometry_dependencies();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -965,7 +992,9 @@ impl OpenCADStudio {
|
||||||
&self.tabs[idx].scene.document.header.code_page,
|
&self.tabs[idx].scene.document.header.code_page,
|
||||||
) {
|
) {
|
||||||
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
||||||
self.tabs[idx].scene.bump_geometry();
|
self.tabs[idx]
|
||||||
|
.scene
|
||||||
|
.invalidate_text_geometry_dependencies();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -2103,7 +2132,7 @@ impl OpenCADStudio {
|
||||||
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
||||||
tab.scene.annotation_scale = scale;
|
tab.scene.annotation_scale = scale;
|
||||||
util::sync_annotation_scale_header(&mut tab.scene);
|
util::sync_annotation_scale_header(&mut tab.scene);
|
||||||
tab.scene.bump_geometry();
|
tab.scene.invalidate_annotation_dependencies();
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -2341,7 +2370,7 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].scene.document.header.annotation_scale_value = p / d;
|
self.tabs[i].scene.document.header.annotation_scale_value = p / d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_annotation_dependencies();
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ impl OpenCADStudio {
|
||||||
let next = if self.point_size_relative { -mag } else { mag };
|
let next = if self.point_size_relative { -mag } else { mag };
|
||||||
self.push_undo_snapshot(i, "PDSIZE");
|
self.push_undo_snapshot(i, "PDSIZE");
|
||||||
self.tabs[i].scene.document.header.point_display_size = next;
|
self.tabs[i].scene.document.header.point_display_size = next;
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_point_dependencies();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,7 +110,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
self.push_undo_snapshot(i, "PDMODE");
|
self.push_undo_snapshot(i, "PDMODE");
|
||||||
self.tabs[i].scene.document.header.point_display_mode = next;
|
self.tabs[i].scene.document.header.point_display_mode = next;
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_point_dependencies();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].wireframe = wf;
|
self.tabs[i].wireframe = wf;
|
||||||
self.ribbon.set_wireframe(wf);
|
self.ribbon.set_wireframe(wf);
|
||||||
self.tabs[i].visual_style = label.into();
|
self.tabs[i].visual_style = label.into();
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Project a pane-local cursor onto the active drawing plane and return a
|
/// Project a pane-local cursor onto the active drawing plane and return a
|
||||||
|
|
@ -405,7 +405,7 @@ impl OpenCADStudio {
|
||||||
// viewport, the picker drives that viewport entity's own
|
// viewport, the picker drives that viewport entity's own
|
||||||
// render mode; the model-layout tab style is untouched.
|
// render mode; the model-layout tab style is untouched.
|
||||||
if self.tabs[i].scene.set_active_viewport_render_mode(mode) {
|
if self.tabs[i].scene.set_active_viewport_render_mode(mode) {
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Viewport visual style: {label}"));
|
.push_output(&format!("Viewport visual style: {label}"));
|
||||||
return Task::none();
|
return Task::none();
|
||||||
|
|
@ -423,7 +423,7 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].visual_style = label.into();
|
self.tabs[i].visual_style = label.into();
|
||||||
// Re-upload face3d fills on the next frame — the render
|
// Re-upload face3d fills on the next frame — the render
|
||||||
// pipeline keys its upload cache off `geometry_epoch`.
|
// pipeline keys its upload cache off `geometry_epoch`.
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Visual style: {label}"));
|
.push_output(&format!("Visual style: {label}"));
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
|
||||||
|
|
@ -1116,7 +1116,7 @@ impl OpenCADStudio {
|
||||||
s.last_tess_wires.get(),
|
s.last_tess_wires.get(),
|
||||||
s.geometry_epoch,
|
s.geometry_epoch,
|
||||||
);
|
);
|
||||||
let trace = crate::perf::snapshot_text();
|
let trace = crate::perf::snapshot_tail_text(80);
|
||||||
let trace = if trace.is_empty() {
|
let trace = if trace.is_empty() {
|
||||||
"No samples yet".to_string()
|
"No samples yet".to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1020,6 +1020,10 @@ pub enum CmdResult {
|
||||||
entity: EntityType,
|
entity: EntityType,
|
||||||
finish: bool,
|
finish: bool,
|
||||||
},
|
},
|
||||||
|
/// End a command-owned live entity without replacing its already-current
|
||||||
|
/// document geometry. PLINE uses this for Enter/Escape after the latest
|
||||||
|
/// vertex was published, avoiding one redundant geometry epoch/GPU patch.
|
||||||
|
FinalizeLiveEntity(Handle),
|
||||||
/// Remove the live entity from the document but keep the command running —
|
/// Remove the live entity from the document but keep the command running —
|
||||||
/// PLINE's Undo popping back below the two vertices an entity needs.
|
/// PLINE's Undo popping back below the two vertices an entity needs.
|
||||||
RemoveLiveEntity(Handle),
|
RemoveLiveEntity(Handle),
|
||||||
|
|
|
||||||
|
|
@ -345,12 +345,21 @@ impl CadCommand for PlineCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
// The committed vertices already live in the document; finalise it.
|
// Every landed vertex is already published into the live document
|
||||||
self.sync_live(false, true)
|
// entity. Finishing must only close its history/command state; replacing
|
||||||
|
// the identical polyline again would advance geometry_epoch and make
|
||||||
|
// large drawings do another cache/GPU patch for no visual change.
|
||||||
|
match self.live_handle {
|
||||||
|
Some(handle) => CmdResult::FinalizeLiveEntity(handle),
|
||||||
|
None => CmdResult::Cancel,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_escape(&mut self) -> CmdResult {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
self.sync_live(false, true)
|
match self.live_handle {
|
||||||
|
Some(handle) => CmdResult::FinalizeLiveEntity(handle),
|
||||||
|
None => CmdResult::Cancel,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
|
|
@ -483,4 +492,23 @@ mod tests {
|
||||||
_ => panic!("later segments should update the same live polyline"),
|
_ => panic!("later segments should update the same live polyline"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finishing_live_polyline_does_not_publish_duplicate_update() {
|
||||||
|
let handle = Handle::new(42);
|
||||||
|
|
||||||
|
let mut enter = PlineCommand::new();
|
||||||
|
enter.live_handle = Some(handle);
|
||||||
|
assert!(matches!(
|
||||||
|
enter.on_enter(),
|
||||||
|
CmdResult::FinalizeLiveEntity(h) if h == handle
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut escape = PlineCommand::new();
|
||||||
|
escape.live_handle = Some(handle);
|
||||||
|
assert!(matches!(
|
||||||
|
escape.on_escape(),
|
||||||
|
CmdResult::FinalizeLiveEntity(h) if h == handle
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
src/perf.rs
34
src/perf.rs
|
|
@ -10,6 +10,8 @@ const MAX_LINES: usize = 500;
|
||||||
static UI_ENABLED: AtomicBool = AtomicBool::new(false);
|
static UI_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
static ENV_ENABLED: OnceLock<bool> = OnceLock::new();
|
static ENV_ENABLED: OnceLock<bool> = OnceLock::new();
|
||||||
static LINES: OnceLock<Mutex<VecDeque<String>>> = OnceLock::new();
|
static LINES: OnceLock<Mutex<VecDeque<String>>> = OnceLock::new();
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
static STDERR_TX: OnceLock<std::sync::mpsc::SyncSender<String>> = OnceLock::new();
|
||||||
|
|
||||||
fn lines() -> &'static Mutex<VecDeque<String>> {
|
fn lines() -> &'static Mutex<VecDeque<String>> {
|
||||||
LINES.get_or_init(|| Mutex::new(VecDeque::with_capacity(MAX_LINES)))
|
LINES.get_or_init(|| Mutex::new(VecDeque::with_capacity(MAX_LINES)))
|
||||||
|
|
@ -32,6 +34,24 @@ pub fn record(args: fmt::Arguments<'_>) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let line = args.to_string();
|
let line = args.to_string();
|
||||||
|
// Terminal writes can block the UI/render thread when its pipe is slow.
|
||||||
|
// PERF is diagnostic and must not create the hitch it is measuring.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let tx = STDERR_TX.get_or_init(|| {
|
||||||
|
let (tx, rx) = std::sync::mpsc::sync_channel::<String>(1024);
|
||||||
|
let _ = std::thread::Builder::new()
|
||||||
|
.name("ocs-perf-log".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
while let Ok(line) = rx.recv() {
|
||||||
|
eprintln!("{line}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
let _ = tx.try_send(line.clone());
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
eprintln!("{line}");
|
eprintln!("{line}");
|
||||||
let mut entries = lines().lock().unwrap_or_else(|e| e.into_inner());
|
let mut entries = lines().lock().unwrap_or_else(|e| e.into_inner());
|
||||||
if entries.len() == MAX_LINES {
|
if entries.len() == MAX_LINES {
|
||||||
|
|
@ -50,6 +70,20 @@ pub fn snapshot_text() -> String {
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A short tail for the live HUD. The full 500-line history remains available
|
||||||
|
/// to Copy, while rendering a large monolithic text widget on every mouse event
|
||||||
|
/// is avoided.
|
||||||
|
pub fn snapshot_tail_text(max_lines: usize) -> String {
|
||||||
|
let entries = lines().lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let skip = entries.len().saturating_sub(max_lines);
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.skip(skip)
|
||||||
|
.map(String::as_str)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear() {
|
pub fn clear() {
|
||||||
lines().lock().unwrap_or_else(|e| e.into_inner()).clear();
|
lines().lock().unwrap_or_else(|e| e.into_inner()).clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,13 +112,12 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_entity(&mut self, mut entity: EntityType) -> Handle {
|
pub fn add_entity(&mut self, mut entity: EntityType) -> Handle {
|
||||||
// Only Insert / Block entities can introduce or reference a block
|
// Only block sentinels mutate a block definition and require rebuilding
|
||||||
// definition that the block cache must (re)build. Adding a plain
|
// the block cache. A top-level INSERT merely references an existing
|
||||||
// top-level entity (line, arc, text, …) leaves every block defn intact,
|
// definition, so adding it can patch just that new render handle.
|
||||||
// so it keeps the cache and skips the all-blocks re-tessellation.
|
|
||||||
let affects_blocks = matches!(
|
let affects_blocks = matches!(
|
||||||
&entity,
|
&entity,
|
||||||
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
|
EntityType::Block(_) | EntityType::BlockEnd(_)
|
||||||
);
|
);
|
||||||
// INSERT invalidates rendered block instances, but it does not mutate
|
// INSERT invalidates rendered block instances, but it does not mutate
|
||||||
// the referenced block definition. Only block sentinels require a
|
// the referenced block definition. Only block sentinels require a
|
||||||
|
|
@ -161,6 +160,9 @@ impl Scene {
|
||||||
if img.definition_handle.is_none() {
|
if img.definition_handle.is_none() {
|
||||||
use acadrust::objects::{ImageDefinition, ObjectType};
|
use acadrust::objects::{ImageDefinition, ObjectType};
|
||||||
let def_handle = Handle::new(self.document.next_handle());
|
let def_handle = Handle::new(self.document.next_handle());
|
||||||
|
if self.is_recording_undo() {
|
||||||
|
self.record_undo_object_before(def_handle, None);
|
||||||
|
}
|
||||||
let mut img_def = ImageDefinition::with_dimensions(
|
let mut img_def = ImageDefinition::with_dimensions(
|
||||||
&img.file_path,
|
&img.file_path,
|
||||||
img.size.x as u32,
|
img.size.x as u32,
|
||||||
|
|
@ -184,7 +186,6 @@ impl Scene {
|
||||||
// definition mutates non-entity state a pure-entity delta can't undo.
|
// definition mutates non-entity state a pure-entity delta can't undo.
|
||||||
let creates_layer =
|
let creates_layer =
|
||||||
self.is_recording_undo() && !layer.trim().is_empty() && !self.document.layers.contains(&layer);
|
self.is_recording_undo() && !layer.trim().is_empty() && !self.document.layers.contains(&layer);
|
||||||
let is_image = matches!(&entity, EntityType::RasterImage(_));
|
|
||||||
self.ensure_layer(&layer);
|
self.ensure_layer(&layer);
|
||||||
|
|
||||||
// Route to the correct block based on current editing mode:
|
// Route to the correct block based on current editing mode:
|
||||||
|
|
@ -238,11 +239,12 @@ impl Scene {
|
||||||
}
|
}
|
||||||
// Delta-undo: the new handle's before-image is "nothing" (it did not
|
// Delta-undo: the new handle's before-image is "nothing" (it did not
|
||||||
// exist). Poison the recording if this add also mutated non-entity
|
// exist). Poison the recording if this add also mutated non-entity
|
||||||
// state (a new layer / block / image definition) so the app knows a
|
// state (a new layer / block) so the app knows a pure-entity delta
|
||||||
// pure-entity delta would be incomplete.
|
// would be incomplete. Raster image definitions are captured as
|
||||||
|
// targeted object before-images above.
|
||||||
if self.is_recording_undo() {
|
if self.is_recording_undo() {
|
||||||
self.record_undo_before(handle, None);
|
self.record_undo_before(handle, None);
|
||||||
if creates_layer || mutates_block_structure || is_image {
|
if creates_layer || mutates_block_structure {
|
||||||
self.poison_undo_recording();
|
self.poison_undo_recording();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -323,14 +325,15 @@ impl Scene {
|
||||||
// The caller edited a snapshot copy; keep the live entity in its block.
|
// The caller edited a snapshot copy; keep the live entity in its block.
|
||||||
entity.common_mut().owner_handle = existing.common().owner_handle;
|
entity.common_mut().owner_handle = existing.common().owner_handle;
|
||||||
|
|
||||||
// Replacing (or becoming) a block entity forces a full block-cache
|
// Replacing (or becoming) a block sentinel forces a full block-cache
|
||||||
// rebuild; a plain entity only needs its own wires re-tessellated.
|
// rebuild. INSERT edits (including retargeting to another existing
|
||||||
|
// definition) only change that top-level render handle.
|
||||||
let affects_blocks = matches!(
|
let affects_blocks = matches!(
|
||||||
existing,
|
existing,
|
||||||
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
|
EntityType::Block(_) | EntityType::BlockEnd(_)
|
||||||
) || matches!(
|
) || matches!(
|
||||||
&entity,
|
&entity,
|
||||||
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
|
EntityType::Block(_) | EntityType::BlockEnd(_)
|
||||||
);
|
);
|
||||||
|
|
||||||
// A plugin edit may retarget the entity to a novel layer; register it
|
// A plugin edit may retarget the entity to a novel layer; register it
|
||||||
|
|
@ -481,6 +484,34 @@ impl Scene {
|
||||||
if handles.is_empty() {
|
if handles.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let mesh_entities: Vec<(Handle, std::sync::Arc<EntityType>)> = handles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&handle| {
|
||||||
|
let entity = self.document.get_entity_arc(handle)?;
|
||||||
|
matches!(
|
||||||
|
entity.as_ref(),
|
||||||
|
EntityType::Solid3D(_)
|
||||||
|
| EntityType::Region(_)
|
||||||
|
| EntityType::Body(_)
|
||||||
|
| EntityType::Surface(_)
|
||||||
|
| EntityType::Mesh(_)
|
||||||
|
| EntityType::PolygonMesh(_)
|
||||||
|
| EntityType::PolyfaceMesh(_)
|
||||||
|
)
|
||||||
|
.then_some((handle, entity))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for handle in handles {
|
||||||
|
self.meshes.remove(handle);
|
||||||
|
self.block_meshes.remove(handle);
|
||||||
|
self.solid_models.remove(handle);
|
||||||
|
}
|
||||||
|
// The overwhelmingly common Undo/Redo target is 2-D geometry. Return
|
||||||
|
// before scanning every Layout object just to discover there is no mesh
|
||||||
|
// to rebuild.
|
||||||
|
if mesh_entities.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let layout_blocks: std::collections::HashSet<Handle> = self
|
let layout_blocks: std::collections::HashSet<Handle> = self
|
||||||
.document
|
.document
|
||||||
.objects
|
.objects
|
||||||
|
|
@ -492,32 +523,15 @@ impl Scene {
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let entries: Vec<(Handle, std::sync::Arc<EntityType>, [f32; 4], bool)> = handles
|
let entries: Vec<(Handle, std::sync::Arc<EntityType>, [f32; 4], bool)> =
|
||||||
.iter()
|
mesh_entities
|
||||||
.filter_map(|&handle| {
|
.into_iter()
|
||||||
let entity = self.document.get_entity_arc(handle)?;
|
.map(|(handle, entity)| {
|
||||||
if !matches!(
|
|
||||||
entity.as_ref(),
|
|
||||||
EntityType::Solid3D(_)
|
|
||||||
| EntityType::Region(_)
|
|
||||||
| EntityType::Body(_)
|
|
||||||
| EntityType::Surface(_)
|
|
||||||
| EntityType::Mesh(_)
|
|
||||||
| EntityType::PolygonMesh(_)
|
|
||||||
| EntityType::PolyfaceMesh(_)
|
|
||||||
) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let color = self.render_style(entity.as_ref()).0;
|
let color = self.render_style(entity.as_ref()).0;
|
||||||
let top_level = layout_blocks.contains(&entity.common().owner_handle);
|
let top_level = layout_blocks.contains(&entity.common().owner_handle);
|
||||||
Some((handle, entity, color, top_level))
|
(handle, entity, color, top_level)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
for handle in handles {
|
|
||||||
self.meshes.remove(handle);
|
|
||||||
self.block_meshes.remove(handle);
|
|
||||||
self.solid_models.remove(handle);
|
|
||||||
}
|
|
||||||
let facet_res = self.document.header.facet_resolution;
|
let facet_res = self.document.header.facet_resolution;
|
||||||
let isolines = self.document.header.isolines.max(0) as usize;
|
let isolines = self.document.header.isolines.max(0) as usize;
|
||||||
use crate::par::prelude::*;
|
use crate::par::prelude::*;
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ impl Scene {
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let group_dict_handle = self.document.header.acad_group_dict_handle;
|
let group_dict_handle = self.document.header.acad_group_dict_handle;
|
||||||
let mut copied = 0;
|
let mut copied = 0;
|
||||||
|
let mut dictionary_recorded = false;
|
||||||
for source in sources {
|
for source in sources {
|
||||||
let entities: Vec<Handle> = source
|
let entities: Vec<Handle> = source
|
||||||
.entities
|
.entities
|
||||||
|
|
@ -105,6 +106,14 @@ impl Scene {
|
||||||
group.name = name.clone();
|
group.name = name.clone();
|
||||||
group.entities = entities;
|
group.entities = entities;
|
||||||
let gh = group.handle;
|
let gh = group.handle;
|
||||||
|
if self.is_recording_undo() {
|
||||||
|
if !dictionary_recorded {
|
||||||
|
let before = self.document.objects.get(&group_dict_handle).cloned();
|
||||||
|
self.record_undo_object_before(group_dict_handle, before);
|
||||||
|
dictionary_recorded = true;
|
||||||
|
}
|
||||||
|
self.record_undo_object_before(gh, None);
|
||||||
|
}
|
||||||
self.document.objects.insert(gh, ObjectType::Group(group));
|
self.document.objects.insert(gh, ObjectType::Group(group));
|
||||||
if let Some(ObjectType::Dictionary(dict)) =
|
if let Some(ObjectType::Dictionary(dict)) =
|
||||||
self.document.objects.get_mut(&group_dict_handle)
|
self.document.objects.get_mut(&group_dict_handle)
|
||||||
|
|
|
||||||
1171
src/scene/mod.rs
1171
src/scene/mod.rs
File diff suppressed because it is too large
Load diff
|
|
@ -410,12 +410,9 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let copied_groups = self.copy_complete_groups(&handle_map);
|
// Complete group copies record their new Group objects and dictionary
|
||||||
if copied_groups > 0 && self.is_recording_undo() {
|
// entry as targeted object deltas inside copy_complete_groups.
|
||||||
// Group copies add Group objects / dictionary entries, which a pure
|
self.copy_complete_groups(&handle_map);
|
||||||
// entity delta cannot restore.
|
|
||||||
self.poison_undo_recording();
|
|
||||||
}
|
|
||||||
// The copies are new handles (natural memo misses, tessellated fresh)
|
// The copies are new handles (natural memo misses, tessellated fresh)
|
||||||
// and reference only already-cached blocks — no block defn changes.
|
// and reference only already-cached blocks — no block defn changes.
|
||||||
// Report them as additions so derived caches patch in exactly the copies.
|
// Report them as additions so derived caches patch in exactly the copies.
|
||||||
|
|
|
||||||
|
|
@ -196,22 +196,55 @@ impl Scene {
|
||||||
Arc<Vec<HatchModel>>,
|
Arc<Vec<HatchModel>>,
|
||||||
Arc<Vec<ImageModel>>,
|
Arc<Vec<ImageModel>>,
|
||||||
) {
|
) {
|
||||||
let selected = self.selected_set_sig();
|
let selected = self.selected_hatch_sig();
|
||||||
{
|
let reuse = {
|
||||||
let cache = self.paper_sheet_render_cache.borrow();
|
let cache = self.paper_sheet_render_cache.borrow();
|
||||||
if let Some(cache) = cache.get(&self.current_layout) {
|
if let Some(cache) = cache.get(&self.current_layout) {
|
||||||
if cache.epoch == self.geometry_epoch
|
if cache.layout == self.current_layout
|
||||||
&& cache.layout == self.current_layout
|
|
||||||
&& cache.selected == selected
|
&& cache.selected == selected
|
||||||
&& cache.paper_bg == self.paper_bg_color
|
&& cache.paper_bg == self.paper_bg_color
|
||||||
|
&& self.category_cache_valid(
|
||||||
|
cache.epoch,
|
||||||
|
super::CACHE_CATEGORY_HATCH,
|
||||||
|
|handle| self.hatches.contains_key(&handle),
|
||||||
|
)
|
||||||
|
&& self.category_cache_valid(
|
||||||
|
cache.epoch,
|
||||||
|
super::CACHE_CATEGORY_WIPEOUT,
|
||||||
|
|handle| {
|
||||||
|
matches!(
|
||||||
|
self.document.get_entity(handle),
|
||||||
|
Some(EntityType::Wipeout(_))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
&& self.category_cache_valid(
|
||||||
|
cache.epoch,
|
||||||
|
super::CACHE_CATEGORY_IMAGE,
|
||||||
|
|handle| self.images.contains_key(&handle),
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return (
|
Some((
|
||||||
Arc::clone(&cache.hatches),
|
Arc::clone(&cache.hatches),
|
||||||
Arc::clone(&cache.wipeouts),
|
Arc::clone(&cache.wipeouts),
|
||||||
Arc::clone(&cache.images),
|
Arc::clone(&cache.images),
|
||||||
);
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if let Some(models) = reuse {
|
||||||
|
if let Some(cache) = self
|
||||||
|
.paper_sheet_render_cache
|
||||||
|
.borrow_mut()
|
||||||
|
.get_mut(&self.current_layout)
|
||||||
|
{
|
||||||
|
cache.epoch = self.geometry_epoch;
|
||||||
|
}
|
||||||
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hatches = Vec::new();
|
let mut hatches = Vec::new();
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,13 @@ pub struct Pipeline {
|
||||||
/// ranges from the resident arena.
|
/// ranges from the resident arena.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) wire_cull_key: (u64, u64, u32, u32),
|
pub(crate) wire_cull_key: (u64, u64, u32, u32),
|
||||||
|
/// View/source keys for CPU visibility passes. A plain entity edit changes
|
||||||
|
/// the scene render signature, but it must not rescan every unchanged hatch,
|
||||||
|
/// wipeout, or mesh AABB when the camera and corresponding source stayed
|
||||||
|
/// identical.
|
||||||
|
pub(crate) hatch_lod_key: (usize, u64, u32, u32, bool),
|
||||||
|
pub(crate) wipeout_lod_key: (usize, u64, u32, u32, bool),
|
||||||
|
pub(crate) mesh_lod_key: (usize, u64, u32, u32),
|
||||||
/// This content viewport's non-rectangular clip boundary as a triangle-fan
|
/// This content viewport's non-rectangular clip boundary as a triangle-fan
|
||||||
/// vertex buffer in the render target's normalized device coords (`None` =
|
/// vertex buffer in the render target's normalized device coords (`None` =
|
||||||
/// rectangular / unclipped, where the viewport's own render rectangle does
|
/// rectangular / unclipped, where the viewport's own render rectangle does
|
||||||
|
|
@ -292,7 +299,7 @@ pub struct Pipeline {
|
||||||
pub cached_mesh_source: Option<std::sync::Arc<Vec<MeshLodSet>>>,
|
pub cached_mesh_source: Option<std::sync::Arc<Vec<MeshLodSet>>>,
|
||||||
pub cached_face3d_source: Option<std::sync::Arc<Vec<WireModel>>>,
|
pub cached_face3d_source: Option<std::sync::Arc<Vec<WireModel>>>,
|
||||||
pub cached_face3d_depth_source:
|
pub cached_face3d_depth_source:
|
||||||
Option<std::sync::Arc<rustc_hash::FxHashMap<u64, [f32; 2]>>>,
|
Option<std::sync::Weak<rustc_hash::FxHashMap<u64, [f32; 2]>>>,
|
||||||
pub cached_fill_mode: bool,
|
pub cached_fill_mode: bool,
|
||||||
/// Last `(geometry_epoch, camera_generation)` value for which GPU buffers
|
/// Last `(geometry_epoch, camera_generation)` value for which GPU buffers
|
||||||
/// were uploaded. We re-upload when either side changes — pan/zoom bumps
|
/// were uploaded. We re-upload when either side changes — pan/zoom bumps
|
||||||
|
|
@ -1812,6 +1819,9 @@ impl Pipeline {
|
||||||
wire_arena_id: u64::MAX,
|
wire_arena_id: u64::MAX,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
wire_cull_key: (u64::MAX, u64::MAX, 0, 0),
|
wire_cull_key: (u64::MAX, u64::MAX, 0, 0),
|
||||||
|
hatch_lod_key: (usize::MAX, u64::MAX, 0, 0, false),
|
||||||
|
wipeout_lod_key: (usize::MAX, u64::MAX, 0, 0, false),
|
||||||
|
mesh_lod_key: (usize::MAX, u64::MAX, 0, 0),
|
||||||
clip_boundary: None,
|
clip_boundary: None,
|
||||||
gpu_selected_wires: vec![],
|
gpu_selected_wires: vec![],
|
||||||
gpu_preview_wires: vec![],
|
gpu_preview_wires: vec![],
|
||||||
|
|
@ -1928,36 +1938,64 @@ impl Pipeline {
|
||||||
hover: Option<acadrust::Handle>,
|
hover: Option<acadrust::Handle>,
|
||||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
) {
|
) {
|
||||||
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
let hover = hover.filter(|h| !selected.contains(h));
|
let hover = hover.filter(|h| !selected.contains(h));
|
||||||
if selected.is_empty() && hover.is_none() {
|
if selected.is_empty() && hover.is_none() {
|
||||||
self.gpu_selected_wires = vec![];
|
self.gpu_selected_wires = vec![];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Gather the highlighted entities' wires via the prebuilt index —
|
// Gather borrowed highlighted wires via the prebuilt index —
|
||||||
// O(highlighted), no per-wire string parse. Selection recolours blue,
|
// O(highlighted), no per-wire string parse or deep geometry clone.
|
||||||
// hover orange. Drawn on top (depth-compare Always) over the base pass.
|
let mut selected_wires: Vec<&WireModel> = Vec::new();
|
||||||
let mut out: Vec<WireModel> = Vec::new();
|
let mut hover_wires: Vec<&WireModel> = Vec::new();
|
||||||
let push = |handle_val: u64, color: [f32; 4], wires: &[WireModel], out: &mut Vec<WireModel>| {
|
for h in selected {
|
||||||
if let Some(idxs) = self.wire_handle_index.get(&handle_val) {
|
if let Some(idxs) = self.wire_handle_index.get(&h.value()) {
|
||||||
let mut slots = idxs.clone();
|
let mut slots = idxs.clone();
|
||||||
slots.sort_unstable();
|
slots.sort_unstable();
|
||||||
for &i in &slots {
|
for &i in &slots {
|
||||||
if let Some(w) = wires.get(i as usize) {
|
if let Some(w) = wires.get(i as usize) {
|
||||||
let mut c = w.clone();
|
selected_wires.push(w);
|
||||||
c.color = color;
|
|
||||||
out.push(c);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
for h in selected {
|
|
||||||
push(h.value(), WireModel::SELECTED, wires, &mut out);
|
|
||||||
}
|
}
|
||||||
if let Some(h) = hover {
|
if let Some(h) = hover {
|
||||||
push(h.value(), WireModel::HOVER, wires, &mut out);
|
if let Some(idxs) = self.wire_handle_index.get(&h.value()) {
|
||||||
|
let mut slots = idxs.clone();
|
||||||
|
slots.sort_unstable();
|
||||||
|
for &i in &slots {
|
||||||
|
if let Some(w) = wires.get(i as usize) {
|
||||||
|
hover_wires.push(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut gpu = WireGpu::from_highlight_refs(
|
||||||
|
device,
|
||||||
|
&selected_wires,
|
||||||
|
WireModel::SELECTED,
|
||||||
|
depth_map,
|
||||||
|
self.wire_const_bgl.as_ref(),
|
||||||
|
);
|
||||||
|
gpu.extend(WireGpu::from_highlight_refs(
|
||||||
|
device,
|
||||||
|
&hover_wires,
|
||||||
|
WireModel::HOVER,
|
||||||
|
depth_map,
|
||||||
|
self.wire_const_bgl.as_ref(),
|
||||||
|
));
|
||||||
|
self.gpu_selected_wires = gpu;
|
||||||
|
if let Some(started) = perf_started {
|
||||||
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if elapsed_ms >= 1.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] wire-highlight {:>7.1}ms handles={} wires={}",
|
||||||
|
elapsed_ms,
|
||||||
|
selected.len() + usize::from(hover.is_some()),
|
||||||
|
selected_wires.len() + hover_wires.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.gpu_selected_wires =
|
|
||||||
WireGpu::from_run(device, &out, depth_map, false, self.wire_const_bgl.as_ref());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the text-highlight overlay: the glyph quads of just the selected /
|
/// Build the text-highlight overlay: the glyph quads of just the selected /
|
||||||
|
|
@ -1971,6 +2009,7 @@ impl Pipeline {
|
||||||
selected: &rustc_hash::FxHashSet<acadrust::Handle>,
|
selected: &rustc_hash::FxHashSet<acadrust::Handle>,
|
||||||
hover: Option<acadrust::Handle>,
|
hover: Option<acadrust::Handle>,
|
||||||
) {
|
) {
|
||||||
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
let hover = hover.filter(|h| !selected.contains(h));
|
let hover = hover.filter(|h| !selected.contains(h));
|
||||||
if selected.is_empty() && hover.is_none() {
|
if selected.is_empty() && hover.is_none() {
|
||||||
self.text_highlight_vbuf = None;
|
self.text_highlight_vbuf = None;
|
||||||
|
|
@ -2001,6 +2040,16 @@ impl Pipeline {
|
||||||
}
|
}
|
||||||
self.text_highlight_vcount = out.len() as u32;
|
self.text_highlight_vcount = out.len() as u32;
|
||||||
self.text_highlight_vbuf = text_gpu::upload_vertices(device, &out);
|
self.text_highlight_vbuf = text_gpu::upload_vertices(device, &out);
|
||||||
|
if let Some(started) = perf_started {
|
||||||
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if elapsed_ms >= 1.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] text-highlight {:>7.1}ms vertices={}",
|
||||||
|
elapsed_ms,
|
||||||
|
out.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload the live overlay (command preview / interim / grip-drag) wires.
|
/// Upload the live overlay (command preview / interim / grip-drag) wires.
|
||||||
|
|
@ -2297,6 +2346,7 @@ impl Pipeline {
|
||||||
clip_w: u32,
|
clip_w: u32,
|
||||||
clip_h: u32,
|
clip_h: u32,
|
||||||
) {
|
) {
|
||||||
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
let Some(batch) = &mut self.gpu_hatch else {
|
let Some(batch) = &mut self.gpu_hatch else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -2306,6 +2356,16 @@ impl Pipeline {
|
||||||
batch.visibility[i] = if skip { 0 } else { 1 };
|
batch.visibility[i] = if skip { 0 } else { 1 };
|
||||||
}
|
}
|
||||||
batch.upload_visibility(queue);
|
batch.upload_visibility(queue);
|
||||||
|
if let Some(started) = perf_started {
|
||||||
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if elapsed_ms >= 1.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] hatch-lod {:>7.1}ms instances={}",
|
||||||
|
elapsed_ms,
|
||||||
|
batch.instance_aabbs.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-frame wipeout frustum-skip flag (Phase 2.3). Mirrors
|
/// Per-frame wipeout frustum-skip flag (Phase 2.3). Mirrors
|
||||||
|
|
@ -2331,6 +2391,7 @@ impl Pipeline {
|
||||||
wireframe_only: bool,
|
wireframe_only: bool,
|
||||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
) {
|
) {
|
||||||
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
// Edge buffer is always built from `face3d_wires`, so 3DFACE
|
// Edge buffer is always built from `face3d_wires`, so 3DFACE
|
||||||
// outlines stay on the screen regardless of mode.
|
// outlines stay on the screen regardless of mode.
|
||||||
self.gpu_face3d_edges =
|
self.gpu_face3d_edges =
|
||||||
|
|
@ -2359,6 +2420,17 @@ impl Pipeline {
|
||||||
depth_map,
|
depth_map,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if let Some(started) = perf_started {
|
||||||
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if elapsed_ms >= 1.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] face3d-upload {:>7.1}ms face-wires={} all-wires={}",
|
||||||
|
elapsed_ms,
|
||||||
|
face3d_wires.len(),
|
||||||
|
all_wires.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the batched mesh buffers (a few large buffers for the whole solid
|
/// Build the batched mesh buffers (a few large buffers for the whole solid
|
||||||
|
|
@ -2773,6 +2845,7 @@ impl Pipeline {
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
hatches: &[HatchModel],
|
hatches: &[HatchModel],
|
||||||
) {
|
) {
|
||||||
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
if let Some(bgl1) = &self.hatch_compat_bgl1 {
|
if let Some(bgl1) = &self.hatch_compat_bgl1 {
|
||||||
self.gpu_hatches_compat = hatches
|
self.gpu_hatches_compat = hatches
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -2792,6 +2865,16 @@ impl Pipeline {
|
||||||
let renderable: Vec<HatchModel> =
|
let renderable: Vec<HatchModel> =
|
||||||
hatches.iter().filter(|h| h.boundary.len() >= 3).cloned().collect();
|
hatches.iter().filter(|h| h.boundary.len() >= 3).cloned().collect();
|
||||||
self.gpu_hatch = hatch_gpu::HatchGpu::build(device, bgl1, &renderable);
|
self.gpu_hatch = hatch_gpu::HatchGpu::build(device, bgl1, &renderable);
|
||||||
|
if let Some(started) = perf_started {
|
||||||
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if elapsed_ms >= 1.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] hatch-upload {:>7.1}ms models={}",
|
||||||
|
elapsed_ms,
|
||||||
|
renderable.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) {
|
pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) {
|
||||||
|
|
@ -2823,6 +2906,7 @@ impl Pipeline {
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
verts: &[text_gpu::TextVertex],
|
verts: &[text_gpu::TextVertex],
|
||||||
) {
|
) {
|
||||||
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
||||||
if self.text_atlas_gpu.is_none() || atlas.is_dirty() {
|
if self.text_atlas_gpu.is_none() || atlas.is_dirty() {
|
||||||
self.text_atlas_gpu = Some(text_gpu::TextAtlasGpu::upload(
|
self.text_atlas_gpu = Some(text_gpu::TextAtlasGpu::upload(
|
||||||
|
|
@ -2836,6 +2920,16 @@ impl Pipeline {
|
||||||
}
|
}
|
||||||
self.text_vbuf = text_gpu::upload_vertices(device, verts);
|
self.text_vbuf = text_gpu::upload_vertices(device, verts);
|
||||||
self.text_vcount = verts.len() as u32;
|
self.text_vcount = verts.len() as u32;
|
||||||
|
if let Some(started) = perf_started {
|
||||||
|
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if elapsed_ms >= 1.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] text-upload {:>7.1}ms vertices={}",
|
||||||
|
elapsed_ms,
|
||||||
|
verts.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_uniforms(&self, queue: &wgpu::Queue, uniforms: &Uniforms) {
|
pub fn upload_uniforms(&self, queue: &wgpu::Queue, uniforms: &Uniforms) {
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,15 @@
|
||||||
// * Modify in place — a move / rotate / scale / colour change keeps the
|
// * Modify in place — a move / rotate / scale / colour change keeps the
|
||||||
// entity's segment count, so its slab is overwritten where it sits.
|
// entity's segment count, so its slab is overwritten where it sits.
|
||||||
// * Add — bump-allocate a fresh slab at the tail.
|
// * Add — bump-allocate a fresh slab at the tail.
|
||||||
// * A Modify whose segment count changed — reject the patch before touching
|
// * A tail Modify whose segment count changed — resize its terminal slab in
|
||||||
// GPU state, so the caller can rebuild this wire arena cleanly.
|
// place. A non-tail shape change rejects the patch before touching GPU state
|
||||||
|
// so the caller can rebuild this wire arena cleanly.
|
||||||
// * Erase — tombstone the slab (blank instances that render nothing).
|
// * Erase — tombstone the slab (blank instances that render nothing).
|
||||||
//
|
//
|
||||||
// Two correctness points make add/remove safe:
|
// Two correctness points make add/remove safe:
|
||||||
// * draw_depth_map normalises each entity's draw-order z-bias by the block's
|
// * draw_depth_map uses stable sparse labels, so Add/Remove changes only the
|
||||||
// entity count, so ANY add/remove re-scales EVERY entity's bias. We keep a
|
// named entity's depth. Existing slabs retain their WireConst values and no
|
||||||
// CPU mirror of the WireConst array and, on a structural change, refresh every
|
// whole-const-buffer upload is needed.
|
||||||
// slab's draw_depth and re-upload the (small, ~1 MB) const buffer — the huge
|
|
||||||
// instance buffer is untouched.
|
|
||||||
// * A tail-appended entity draws last, which only mis-orders alpha-blended /
|
// * A tail-appended entity draws last, which only mis-orders alpha-blended /
|
||||||
// coincident wires. So when the set contains ANY transparent wire we bail to a
|
// coincident wires. So when the set contains ANY transparent wire we bail to a
|
||||||
// full rebuild instead of appending. Opaque overlap resolves by the z-bias, so
|
// full rebuild instead of appending. Opaque overlap resolves by the z-bias, so
|
||||||
|
|
@ -80,6 +79,23 @@ struct PreparedPatchRun {
|
||||||
order_sensitive: bool,
|
order_sensitive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn can_resize_terminal_slab(
|
||||||
|
slab: &Slab,
|
||||||
|
inst_tail: u32,
|
||||||
|
const_tail: u32,
|
||||||
|
inst_cap: u32,
|
||||||
|
const_cap: u32,
|
||||||
|
new_inst_len: u32,
|
||||||
|
new_const_len: u32,
|
||||||
|
change_count: usize,
|
||||||
|
) -> bool {
|
||||||
|
change_count == 1
|
||||||
|
&& slab.inst_off + slab.inst_len == inst_tail
|
||||||
|
&& slab.const_off + slab.const_len == const_tail
|
||||||
|
&& slab.inst_off + new_inst_len <= inst_cap
|
||||||
|
&& slab.const_off + new_const_len <= const_cap
|
||||||
|
}
|
||||||
|
|
||||||
pub struct WireArena {
|
pub struct WireArena {
|
||||||
inst_buf: wgpu::Buffer,
|
inst_buf: wgpu::Buffer,
|
||||||
inst_cap: u32,
|
inst_cap: u32,
|
||||||
|
|
@ -88,8 +104,7 @@ pub struct WireArena {
|
||||||
const_bind_group: std::sync::Arc<wgpu::BindGroup>,
|
const_bind_group: std::sync::Arc<wgpu::BindGroup>,
|
||||||
const_cap: u32,
|
const_cap: u32,
|
||||||
const_tail: u32,
|
const_tail: u32,
|
||||||
/// CPU mirror of the const buffer so a structural edit can refresh every
|
/// CPU mirror of the const buffer, used for in-place tail resize/patches.
|
||||||
/// slab's draw_depth (denominator change) without re-emitting geometry.
|
|
||||||
consts_cpu: Vec<WireConst>,
|
consts_cpu: Vec<WireConst>,
|
||||||
slabs: FxHashMap<Handle, Slab>,
|
slabs: FxHashMap<Handle, Slab>,
|
||||||
/// Temporarily hidden Modified slabs. Grip drag blanks these but keeps their
|
/// Temporarily hidden Modified slabs. Grip drag blanks these but keeps their
|
||||||
|
|
@ -166,7 +181,7 @@ pub(crate) fn patch_handle_index(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if edit.new_len != 0 {
|
if edit.visible && edit.new_len != 0 {
|
||||||
index.insert(
|
index.insert(
|
||||||
edit.handle.value(),
|
edit.handle.value(),
|
||||||
(edit.start..edit.start + edit.new_len)
|
(edit.start..edit.start + edit.new_len)
|
||||||
|
|
@ -504,17 +519,14 @@ impl WireArena {
|
||||||
new_handles_are_suffix: bool,
|
new_handles_are_suffix: bool,
|
||||||
depth_map: &FxHashMap<u64, [f32; 2]>,
|
depth_map: &FxHashMap<u64, [f32; 2]>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let depth_structural = changes
|
|
||||||
.iter()
|
|
||||||
.any(|(_, kind)| !matches!(kind, ChangeKind::Modified));
|
|
||||||
|
|
||||||
// Prepare and validate every visible run before mutating either GPU
|
// Prepare and validate every visible run before mutating either GPU
|
||||||
// buffer. Growing/shrinking a Modified slab used to tombstone the old
|
// buffer. Growing/shrinking a Modified slab used to tombstone the old
|
||||||
// range and append a new one. Repeated live-polyline updates could then
|
// range and append a new one. Repeated live-polyline updates could then
|
||||||
// leave the arena half-patched when a later guard requested a rebuild,
|
// leave the arena half-patched when a later guard requested a rebuild,
|
||||||
// producing alternating old/new submissions. A shape change now takes
|
// producing alternating old/new submissions. The terminal slab can
|
||||||
// the clean full-arena fallback, while the scene itself still
|
// grow/shrink without relocation (the common live-polyline case);
|
||||||
// re-tessellates only the named entity.
|
// other shape changes take the clean full-arena fallback while the
|
||||||
|
// scene itself still re-tessellates only the named entity.
|
||||||
let mut prepared: FxHashMap<Handle, PreparedPatchRun> = FxHashMap::default();
|
let mut prepared: FxHashMap<Handle, PreparedPatchRun> = FxHashMap::default();
|
||||||
for &(h, kind) in changes {
|
for &(h, kind) in changes {
|
||||||
let run = runs.get(&h).map(Vec::as_slice).unwrap_or(&[]);
|
let run = runs.get(&h).map(Vec::as_slice).unwrap_or(&[]);
|
||||||
|
|
@ -538,17 +550,26 @@ impl WireArena {
|
||||||
let inst_len = insts.len() as u32;
|
let inst_len = insts.len() as u32;
|
||||||
let const_len = csts.len() as u32;
|
let const_len = csts.len() as u32;
|
||||||
|
|
||||||
if matches!(kind, ChangeKind::Modified)
|
if matches!(kind, ChangeKind::Modified) {
|
||||||
&& self
|
let known = self.slabs.get(&h).or_else(|| self.vacant.get(&h));
|
||||||
.slabs
|
let shape_changed = known
|
||||||
.get(&h)
|
.is_some_and(|slab| slab.inst_len != inst_len || slab.const_len != const_len);
|
||||||
.or_else(|| self.vacant.get(&h))
|
let can_resize_tail = self.slabs.get(&h).is_some_and(|slab| {
|
||||||
.is_some_and(|slab| {
|
can_resize_terminal_slab(
|
||||||
slab.inst_len != inst_len || slab.const_len != const_len
|
slab,
|
||||||
})
|
self.inst_tail,
|
||||||
{
|
self.const_tail,
|
||||||
|
self.inst_cap,
|
||||||
|
self.const_cap,
|
||||||
|
inst_len,
|
||||||
|
const_len,
|
||||||
|
changes.len(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if shape_changed && !can_resize_tail {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prepared.insert(
|
prepared.insert(
|
||||||
h,
|
h,
|
||||||
|
|
@ -642,6 +663,50 @@ impl WireArena {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A live entity is normally the newest handle and therefore owns
|
||||||
|
// the terminal slab. Resize that slab at the same offsets instead
|
||||||
|
// of tombstoning it and rebuilding the whole arena as its segment
|
||||||
|
// count grows on every click.
|
||||||
|
let can_resize_tail = self.slabs.get(&h).is_some_and(|slab| {
|
||||||
|
can_resize_terminal_slab(
|
||||||
|
slab,
|
||||||
|
self.inst_tail,
|
||||||
|
self.const_tail,
|
||||||
|
self.inst_cap,
|
||||||
|
self.const_cap,
|
||||||
|
inst_len,
|
||||||
|
const_len,
|
||||||
|
changes.len(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if can_resize_tail {
|
||||||
|
let (inst_off, const_off) = {
|
||||||
|
let slab = self.slabs.get(&h).unwrap();
|
||||||
|
(slab.inst_off, slab.const_off)
|
||||||
|
};
|
||||||
|
for instance in &mut insts {
|
||||||
|
instance.wire_id += const_off;
|
||||||
|
}
|
||||||
|
self.write_insts(queue, inst_off, &insts);
|
||||||
|
self.consts_cpu.truncate(const_off as usize);
|
||||||
|
self.consts_cpu.extend(csts.iter().copied());
|
||||||
|
let csz = std::mem::size_of::<WireConst>() as u64;
|
||||||
|
queue.write_buffer(
|
||||||
|
&self.const_buf,
|
||||||
|
const_off as u64 * csz,
|
||||||
|
bytemuck::cast_slice(&csts),
|
||||||
|
);
|
||||||
|
self.inst_tail = inst_off + inst_len;
|
||||||
|
self.const_tail = const_off + const_len;
|
||||||
|
let slab = self.slabs.get_mut(&h).unwrap();
|
||||||
|
slab.inst_len = inst_len;
|
||||||
|
slab.const_len = const_len;
|
||||||
|
slab.base_depth = base_depth;
|
||||||
|
slab.aabb = aabb;
|
||||||
|
self.order_sensitive |= run_order_sensitive;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Layout changed ⇒ append at the tail. Unsafe to relocate when the set
|
// Layout changed ⇒ append at the tail. Unsafe to relocate when the set
|
||||||
// resolves overlap by submission order: transparency, a wire with no
|
// resolves overlap by submission order: transparency, a wire with no
|
||||||
// draw-order depth, or the mesh-edge arena (all its wires are forced
|
// draw-order depth, or the mesh-edge arena (all its wires are forced
|
||||||
|
|
@ -674,6 +739,16 @@ impl WireArena {
|
||||||
for c in &csts {
|
for c in &csts {
|
||||||
self.consts_cpu.push(*c);
|
self.consts_cpu.push(*c);
|
||||||
}
|
}
|
||||||
|
// Appended instances immediately reference these constant slots.
|
||||||
|
// Keep the GPU buffer in sync without relying on a full-scene upload.
|
||||||
|
if !csts.is_empty() {
|
||||||
|
let const_size = std::mem::size_of::<WireConst>() as u64;
|
||||||
|
queue.write_buffer(
|
||||||
|
&self.const_buf,
|
||||||
|
const_off as u64 * const_size,
|
||||||
|
bytemuck::cast_slice(&csts),
|
||||||
|
);
|
||||||
|
}
|
||||||
self.inst_tail += inst_len;
|
self.inst_tail += inst_len;
|
||||||
self.const_tail += const_len;
|
self.const_tail += const_len;
|
||||||
self.slabs.insert(
|
self.slabs.insert(
|
||||||
|
|
@ -690,30 +765,6 @@ impl WireArena {
|
||||||
self.order_sensitive |= run_order_sensitive;
|
self.order_sensitive |= run_order_sensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
if depth_structural {
|
|
||||||
// The entity count changed ⇒ draw_depth_map re-normalised every
|
|
||||||
// entity's z-bias. Refresh each live slab's draw_depth from the new
|
|
||||||
// depth map and re-upload the whole (small) const buffer; the instance
|
|
||||||
// buffer is untouched.
|
|
||||||
// Preserve block-local depth composition. A slab may contain many
|
|
||||||
// different child offsets around its entity base; shifting every
|
|
||||||
// const by the base delta keeps those offsets intact.
|
|
||||||
for (h, slab) in &mut self.slabs {
|
|
||||||
// Mesh-edge wires keep depth 0 (no draw-order bias); regular wires
|
|
||||||
// take the re-normalised map value.
|
|
||||||
let dd = if self.mesh_edge {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
depth_map.get(&h.value()).map_or(0.0, |d| d[0])
|
|
||||||
};
|
|
||||||
let delta = dd - slab.base_depth;
|
|
||||||
for k in 0..slab.const_len {
|
|
||||||
self.consts_cpu[(slab.const_off + k) as usize].draw_depth += delta;
|
|
||||||
}
|
|
||||||
slab.base_depth = dd;
|
|
||||||
}
|
|
||||||
queue.write_buffer(&self.const_buf, 0, bytemuck::cast_slice(&self.consts_cpu));
|
|
||||||
}
|
|
||||||
if self.tombstoned > self.inst_tail / 2 {
|
if self.tombstoned > self.inst_tail / 2 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -746,6 +797,8 @@ impl WireArena {
|
||||||
clip_w: u32,
|
clip_w: u32,
|
||||||
clip_h: u32,
|
clip_h: u32,
|
||||||
) -> Vec<WireGpu> {
|
) -> Vec<WireGpu> {
|
||||||
|
let perf = crate::perf::enabled();
|
||||||
|
let perf_started = perf.then(iced::time::Instant::now);
|
||||||
if self.inst_tail == 0 {
|
if self.inst_tail == 0 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -799,14 +852,18 @@ impl WireArena {
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if crate::perf::enabled() {
|
if perf {
|
||||||
let submitted: u64 = ranges
|
let submitted: u64 = ranges
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(start, end)| (end - start) as u64)
|
.map(|(start, end)| (end - start) as u64)
|
||||||
.sum();
|
.sum();
|
||||||
if submitted < self.inst_tail as u64 {
|
let elapsed_ms = perf_started
|
||||||
|
.map(|started| started.elapsed().as_secs_f64() * 1000.0)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if submitted < self.inst_tail as u64 || elapsed_ms >= 1.0 {
|
||||||
crate::perf_record!(
|
crate::perf_record!(
|
||||||
"[perf] wire-cull submitted={} resident={} ranges={}",
|
"[perf] wire-cull {:>7.1}ms submitted={} resident={} ranges={}",
|
||||||
|
elapsed_ms,
|
||||||
submitted,
|
submitted,
|
||||||
self.inst_tail,
|
self.inst_tail,
|
||||||
ranges.len(),
|
ranges.len(),
|
||||||
|
|
@ -826,3 +883,57 @@ impl WireArena {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn slab() -> Slab {
|
||||||
|
Slab {
|
||||||
|
inst_off: 100,
|
||||||
|
inst_len: 10,
|
||||||
|
const_off: 20,
|
||||||
|
const_len: 2,
|
||||||
|
aabb: [0.0; 4],
|
||||||
|
base_depth: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terminal_slab_can_grow_without_relocating_the_arena() {
|
||||||
|
assert!(can_resize_terminal_slab(
|
||||||
|
&slab(),
|
||||||
|
110,
|
||||||
|
22,
|
||||||
|
1000,
|
||||||
|
100,
|
||||||
|
18,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_terminal_or_batched_resize_uses_clean_fallback() {
|
||||||
|
assert!(!can_resize_terminal_slab(
|
||||||
|
&slab(),
|
||||||
|
111,
|
||||||
|
22,
|
||||||
|
1000,
|
||||||
|
100,
|
||||||
|
18,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
assert!(!can_resize_terminal_slab(
|
||||||
|
&slab(),
|
||||||
|
110,
|
||||||
|
22,
|
||||||
|
1000,
|
||||||
|
100,
|
||||||
|
18,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -607,6 +607,84 @@ fn build_const_bind_group(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WireGpu {
|
impl WireGpu {
|
||||||
|
/// Build a small selection/hover overlay from borrowed resident wires while
|
||||||
|
/// overriding their colour. Avoids deep-cloning every point/text/fill array
|
||||||
|
/// of a large selected polyline or block before packing the overlay.
|
||||||
|
pub fn from_highlight_refs(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
wires: &[&WireModel],
|
||||||
|
color: [f32; 4],
|
||||||
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
|
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||||
|
) -> Vec<Self> {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
if let Some(const_bgl) = const_bgl {
|
||||||
|
const MAX_INSTANCES: usize =
|
||||||
|
268_435_456 / std::mem::size_of::<WireInstance>();
|
||||||
|
use crate::par::prelude::*;
|
||||||
|
let per: Vec<(Vec<WireInstance>, WireConst)> = wires
|
||||||
|
.par_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, &wire)| {
|
||||||
|
emit_wire_native(wire, idx as u32, color, wire_draw_depth(wire, depth_map))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut instances =
|
||||||
|
Vec::with_capacity(per.iter().map(|(items, _)| items.len()).sum());
|
||||||
|
let mut consts = Vec::with_capacity(per.len());
|
||||||
|
for (mut items, constant) in per {
|
||||||
|
instances.append(&mut items);
|
||||||
|
consts.push(constant);
|
||||||
|
}
|
||||||
|
if instances.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let bind_group = build_const_bind_group(device, const_bgl, &consts);
|
||||||
|
return instances
|
||||||
|
.chunks(MAX_INSTANCES)
|
||||||
|
.map(|chunk| Self {
|
||||||
|
instance_buffer: instance_buffer_mapped(
|
||||||
|
device,
|
||||||
|
"wire.highlight.ibuf",
|
||||||
|
chunk,
|
||||||
|
),
|
||||||
|
first_instance: 0,
|
||||||
|
instance_count: chunk.len() as u32,
|
||||||
|
is_3d_mesh_edge: false,
|
||||||
|
const_bind_group: Some(bind_group.clone()),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = const_bgl;
|
||||||
|
const MAX_PACKED_INSTANCES: usize =
|
||||||
|
268_435_456 / std::mem::size_of::<PackedWireInstance>();
|
||||||
|
let per: Vec<Vec<PackedWireInstance>> = wires
|
||||||
|
.iter()
|
||||||
|
.map(|wire| {
|
||||||
|
emit_wire_packed(wire, color, wire_draw_depth(wire, depth_map))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut instances = Vec::with_capacity(per.iter().map(Vec::len).sum());
|
||||||
|
for mut items in per {
|
||||||
|
instances.append(&mut items);
|
||||||
|
}
|
||||||
|
instances
|
||||||
|
.chunks(MAX_PACKED_INSTANCES)
|
||||||
|
.map(|chunk| Self {
|
||||||
|
instance_buffer: instance_buffer_mapped(
|
||||||
|
device,
|
||||||
|
"wire.highlight.compat.ibuf",
|
||||||
|
chunk,
|
||||||
|
),
|
||||||
|
first_instance: 0,
|
||||||
|
instance_count: chunk.len() as u32,
|
||||||
|
is_3d_mesh_edge: false,
|
||||||
|
const_bind_group: None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Native-only equivalent of [`from_run`] for an already partitioned set
|
/// Native-only equivalent of [`from_run`] for an already partitioned set
|
||||||
/// of borrowed wires. Used when one arena partition exceeds the 256 MB
|
/// of borrowed wires. Used when one arena partition exceeds the 256 MB
|
||||||
/// buffer limit: the compatible partition stays patchable while only the
|
/// buffer limit: the compatible partition stays patchable while only the
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,7 @@ impl Scene {
|
||||||
// ── Erase ─────────────────────────────────────────────────────────────
|
// ── Erase ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn erase_entities(&mut self, handles: &[Handle]) {
|
pub fn erase_entities(&mut self, handles: &[Handle]) {
|
||||||
let handle_set: HashSet<Handle> = handles.iter().copied().collect();
|
let mut handle_set: HashSet<Handle> = HashSet::default();
|
||||||
let mut erased: Vec<(Handle, ChangeKind)> = Vec::new();
|
let mut erased: Vec<(Handle, ChangeKind)> = Vec::new();
|
||||||
for &h in handles {
|
for &h in handles {
|
||||||
// Objects on a locked layer can't be erased.
|
// Objects on a locked layer can't be erased.
|
||||||
|
|
@ -343,6 +343,7 @@ impl Scene {
|
||||||
let before = self.document.get_entity_arc(h);
|
let before = self.document.get_entity_arc(h);
|
||||||
self.record_undo_before(h, before);
|
self.record_undo_before(h, before);
|
||||||
}
|
}
|
||||||
|
self.remember_removed_cache_categories(h);
|
||||||
self.document.remove_entity_arc(h);
|
self.document.remove_entity_arc(h);
|
||||||
self.selected.remove(&h);
|
self.selected.remove(&h);
|
||||||
self.hatches.remove(&h);
|
self.hatches.remove(&h);
|
||||||
|
|
@ -350,22 +351,53 @@ impl Scene {
|
||||||
self.meshes.remove(&h);
|
self.meshes.remove(&h);
|
||||||
self.block_meshes.remove(&h);
|
self.block_meshes.remove(&h);
|
||||||
self.solid_models.remove(&h);
|
self.solid_models.remove(&h);
|
||||||
|
handle_set.insert(h);
|
||||||
erased.push((h, ChangeKind::Removed));
|
erased.push((h, ChangeKind::Removed));
|
||||||
}
|
}
|
||||||
|
// Capture exactly the group objects that this erase will rewrite, plus
|
||||||
|
// the group dictionary when an emptied group will be removed. Do this
|
||||||
|
// before taking mutable object-map borrows below.
|
||||||
|
if self.is_recording_undo() && !handle_set.is_empty() {
|
||||||
|
let affected_groups: Vec<(Handle, ObjectType)> = self
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(&handle, object)| match object {
|
||||||
|
ObjectType::Group(group)
|
||||||
|
if group.entities.iter().any(|h| handle_set.contains(h)) =>
|
||||||
|
{
|
||||||
|
Some((handle, object.clone()))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !affected_groups.is_empty() {
|
||||||
|
let removes_group = affected_groups.iter().any(|(_, object)| {
|
||||||
|
matches!(
|
||||||
|
object,
|
||||||
|
ObjectType::Group(group)
|
||||||
|
if group.entities.iter().all(|h| handle_set.contains(h))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if removes_group {
|
||||||
|
let dictionary = self.document.header.acad_group_dict_handle;
|
||||||
|
let dictionary_before = self.document.objects.get(&dictionary).cloned();
|
||||||
|
self.record_undo_object_before(dictionary, dictionary_before);
|
||||||
|
}
|
||||||
|
for (handle, object) in affected_groups {
|
||||||
|
self.record_undo_object_before(handle, Some(object));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Remove erased handles from all groups; delete groups that become empty.
|
// Remove erased handles from all groups; delete groups that become empty.
|
||||||
let group_dict_handle = self.document.header.acad_group_dict_handle;
|
let group_dict_handle = self.document.header.acad_group_dict_handle;
|
||||||
let mut groups_changed = false;
|
|
||||||
let to_remove: Vec<Handle> = self
|
let to_remove: Vec<Handle> = self
|
||||||
.document
|
.document
|
||||||
.objects
|
.objects
|
||||||
.values_mut()
|
.values_mut()
|
||||||
.filter_map(|obj| match obj {
|
.filter_map(|obj| match obj {
|
||||||
ObjectType::Group(g) => {
|
ObjectType::Group(g) => {
|
||||||
let before = g.entities.len();
|
|
||||||
g.entities.retain(|h| !handle_set.contains(h));
|
g.entities.retain(|h| !handle_set.contains(h));
|
||||||
if g.entities.len() != before {
|
|
||||||
groups_changed = true;
|
|
||||||
}
|
|
||||||
if g.entities.is_empty() {
|
if g.entities.is_empty() {
|
||||||
Some(g.handle)
|
Some(g.handle)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -375,11 +407,6 @@ impl Scene {
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// Delta-undo: editing a group (or deleting an emptied one) mutates
|
|
||||||
// document.objects — non-entity state a pure-entity delta can't restore.
|
|
||||||
if groups_changed {
|
|
||||||
self.poison_undo_recording();
|
|
||||||
}
|
|
||||||
for gh in &to_remove {
|
for gh in &to_remove {
|
||||||
if let Some(ObjectType::Dictionary(dict)) =
|
if let Some(ObjectType::Dictionary(dict)) =
|
||||||
self.document.objects.get_mut(&group_dict_handle)
|
self.document.objects.get_mut(&group_dict_handle)
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ pub struct ViewportData {
|
||||||
/// by the wire / face3d pipelines as a clip-z bias. WireModels carry no
|
/// by the wire / face3d pipelines as a clip-z bias. WireModels carry no
|
||||||
/// depth field (84 construction sites); the bias is looked up by handle
|
/// depth field (84 construction sites); the bias is looked up by handle
|
||||||
/// at GPU-upload time from this map instead.
|
/// at GPU-upload time from this map instead.
|
||||||
pub(in crate::scene) draw_depths: Arc<rustc_hash::FxHashMap<u64, [f32; 2]>>,
|
pub(in crate::scene) draw_depths:
|
||||||
|
std::sync::Weak<rustc_hash::FxHashMap<u64, [f32; 2]>>,
|
||||||
pub(in crate::scene) hatches: Arc<Vec<HatchModel>>,
|
pub(in crate::scene) hatches: Arc<Vec<HatchModel>>,
|
||||||
/// Wipeout fills — rendered in a separate pass AFTER wires.
|
/// Wipeout fills — rendered in a separate pass AFTER wires.
|
||||||
pub(in crate::scene) wipeout_hatches: Arc<Vec<HatchModel>>,
|
pub(in crate::scene) wipeout_hatches: Arc<Vec<HatchModel>>,
|
||||||
|
|
@ -277,6 +278,9 @@ impl shader::Primitive for Primitive {
|
||||||
{
|
{
|
||||||
inner.wire_cull_key = (u64::MAX, u64::MAX, 0, 0);
|
inner.wire_cull_key = (u64::MAX, u64::MAX, 0, 0);
|
||||||
}
|
}
|
||||||
|
inner.hatch_lod_key = (usize::MAX, u64::MAX, 0, 0, false);
|
||||||
|
inner.wipeout_lod_key = (usize::MAX, u64::MAX, 0, 0, false);
|
||||||
|
inner.mesh_lod_key = (usize::MAX, u64::MAX, 0, 0);
|
||||||
inner.render_sig = u64::MAX;
|
inner.render_sig = u64::MAX;
|
||||||
}
|
}
|
||||||
// The MSAA / depth / resolve textures are always sized to the
|
// The MSAA / depth / resolve textures are always sized to the
|
||||||
|
|
@ -337,6 +341,9 @@ impl shader::Primitive for Primitive {
|
||||||
let Some(vp_wires) = vp.wires.upgrade() else {
|
let Some(vp_wires) = vp.wires.upgrade() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let Some(draw_depths) = vp.draw_depths.upgrade() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
// Third component is the *selected-set* signature (not
|
// Third component is the *selected-set* signature (not
|
||||||
// selection_generation, which also bumps on hover) so a rollover
|
// selection_generation, which also bumps on hover) so a rollover
|
||||||
// doesn't re-upload the static hatch / face3d buffers.
|
// doesn't re-upload the static hatch / face3d buffers.
|
||||||
|
|
@ -411,7 +418,8 @@ impl shader::Primitive for Primitive {
|
||||||
|| inner
|
|| inner
|
||||||
.cached_face3d_depth_source
|
.cached_face3d_depth_source
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(true, |source| !Arc::ptr_eq(source, &vp.draw_depths))
|
.and_then(std::sync::Weak::upgrade)
|
||||||
|
.map_or(true, |source| !Arc::ptr_eq(&source, &draw_depths))
|
||||||
|| (inner.cached_face3d_key.0 != vp.wire_content_id
|
|| (inner.cached_face3d_key.0 != vp.wire_content_id
|
||||||
&& !face_pass_unchanged);
|
&& !face_pass_unchanged);
|
||||||
if face3d_changed || face3d_fill_active != inner.cached_face3d_key.1 {
|
if face3d_changed || face3d_fill_active != inner.cached_face3d_key.1 {
|
||||||
|
|
@ -420,10 +428,10 @@ impl shader::Primitive for Primitive {
|
||||||
&vp.face3d_wires[..],
|
&vp.face3d_wires[..],
|
||||||
&vp_wires[..],
|
&vp_wires[..],
|
||||||
!face3d_fill_active,
|
!face3d_fill_active,
|
||||||
&vp.draw_depths,
|
&draw_depths,
|
||||||
);
|
);
|
||||||
inner.cached_face3d_source = Some(Arc::clone(&vp.face3d_wires));
|
inner.cached_face3d_source = Some(Arc::clone(&vp.face3d_wires));
|
||||||
inner.cached_face3d_depth_source = Some(Arc::clone(&vp.draw_depths));
|
inner.cached_face3d_depth_source = Some(Arc::downgrade(&draw_depths));
|
||||||
}
|
}
|
||||||
inner.cached_face3d_key = (vp.wire_content_id, face3d_fill_active);
|
inner.cached_face3d_key = (vp.wire_content_id, face3d_fill_active);
|
||||||
// Wire buffers are world-space, so a camera move alone doesn't
|
// Wire buffers are world-space, so a camera move alone doesn't
|
||||||
|
|
@ -527,7 +535,7 @@ impl shader::Primitive for Primitive {
|
||||||
&patch.changes,
|
&patch.changes,
|
||||||
®ular_changed,
|
®ular_changed,
|
||||||
patch.new_handles_are_suffix,
|
patch.new_handles_are_suffix,
|
||||||
&vp.draw_depths,
|
&draw_depths,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
inner.wire_arena_fallback_kind == Some(false)
|
inner.wire_arena_fallback_kind == Some(false)
|
||||||
|
|
@ -541,7 +549,7 @@ impl shader::Primitive for Primitive {
|
||||||
&patch.changes,
|
&patch.changes,
|
||||||
&mesh_changed,
|
&mesh_changed,
|
||||||
patch.new_handles_are_suffix,
|
patch.new_handles_are_suffix,
|
||||||
&vp.draw_depths,
|
&draw_depths,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
inner.wire_arena_fallback_kind == Some(true)
|
inner.wire_arena_fallback_kind == Some(true)
|
||||||
|
|
@ -571,7 +579,7 @@ impl shader::Primitive for Primitive {
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
®ular,
|
®ular,
|
||||||
&vp.draw_depths,
|
&draw_depths,
|
||||||
bgl,
|
bgl,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
@ -580,7 +588,7 @@ impl shader::Primitive for Primitive {
|
||||||
crate::scene::pipeline::WireGpu::from_run_refs(
|
crate::scene::pipeline::WireGpu::from_run_refs(
|
||||||
device,
|
device,
|
||||||
®ular,
|
®ular,
|
||||||
&vp.draw_depths,
|
&draw_depths,
|
||||||
false,
|
false,
|
||||||
bgl,
|
bgl,
|
||||||
),
|
),
|
||||||
|
|
@ -607,7 +615,7 @@ impl shader::Primitive for Primitive {
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
&mesh,
|
&mesh,
|
||||||
&vp.draw_depths,
|
&draw_depths,
|
||||||
bgl,
|
bgl,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
@ -616,7 +624,7 @@ impl shader::Primitive for Primitive {
|
||||||
crate::scene::pipeline::WireGpu::from_run_refs(
|
crate::scene::pipeline::WireGpu::from_run_refs(
|
||||||
device,
|
device,
|
||||||
&mesh,
|
&mesh,
|
||||||
&vp.draw_depths,
|
&draw_depths,
|
||||||
true,
|
true,
|
||||||
bgl,
|
bgl,
|
||||||
),
|
),
|
||||||
|
|
@ -700,7 +708,7 @@ impl shader::Primitive for Primitive {
|
||||||
Some(entry) => entry,
|
Some(entry) => entry,
|
||||||
None => {
|
None => {
|
||||||
let entry =
|
let entry =
|
||||||
inner.build_wire_buffers(device, &vp_wires[..], &vp.draw_depths);
|
inner.build_wire_buffers(device, &vp_wires[..], &draw_depths);
|
||||||
pipeline
|
pipeline
|
||||||
.wire_buffer_cache
|
.wire_buffer_cache
|
||||||
.insert(vp.wire_content_id, entry.clone());
|
.insert(vp.wire_content_id, entry.clone());
|
||||||
|
|
@ -745,13 +753,26 @@ impl shader::Primitive for Primitive {
|
||||||
// so this refreshes without re-tessellating or re-uploading the main
|
// so this refreshes without re-tessellating or re-uploading the main
|
||||||
// wire buffers.
|
// wire buffers.
|
||||||
let sel_key = (vp.wire_content_id, vp.selection_generation);
|
let sel_key = (vp.wire_content_id, vp.selection_generation);
|
||||||
if sel_key != inner.cached_selection {
|
let highlighted_geometry_unchanged =
|
||||||
|
vp.selected_handles.is_empty() && vp.hover_handle.is_none()
|
||||||
|
|| vp.wire_patch.as_ref().is_some_and(|(previous, patch)| {
|
||||||
|
*previous == inner.cached_selection.0
|
||||||
|
&& patch.changes.iter().all(|(handle, _)| {
|
||||||
|
!vp.selected_handles.contains(handle)
|
||||||
|
&& vp.hover_handle != Some(*handle)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let selection_changed = inner.cached_selection.1 != vp.selection_generation;
|
||||||
|
let highlighted_geometry_changed = inner.cached_selection.0
|
||||||
|
!= vp.wire_content_id
|
||||||
|
&& !highlighted_geometry_unchanged;
|
||||||
|
if selection_changed || highlighted_geometry_changed {
|
||||||
inner.upload_selected_wires(
|
inner.upload_selected_wires(
|
||||||
device,
|
device,
|
||||||
&vp_wires[..],
|
&vp_wires[..],
|
||||||
&vp.selected_handles,
|
&vp.selected_handles,
|
||||||
vp.hover_handle,
|
vp.hover_handle,
|
||||||
&vp.draw_depths,
|
&draw_depths,
|
||||||
);
|
);
|
||||||
// Text highlight rides the same selection key: a pick / rollover
|
// Text highlight rides the same selection key: a pick / rollover
|
||||||
// recolours the selected / hovered glyphs without touching the
|
// recolours the selected / hovered glyphs without touching the
|
||||||
|
|
@ -762,8 +783,11 @@ impl shader::Primitive for Primitive {
|
||||||
&vp.selected_handles,
|
&vp.selected_handles,
|
||||||
vp.hover_handle,
|
vp.hover_handle,
|
||||||
);
|
);
|
||||||
inner.cached_selection = sel_key;
|
|
||||||
}
|
}
|
||||||
|
// Advance the content id even when an unrelated entity patch kept
|
||||||
|
// the existing overlay valid, so future deltas compare to the
|
||||||
|
// arena generation actually on screen.
|
||||||
|
inner.cached_selection = sel_key;
|
||||||
// Batched solid meshes stay resident while unrelated entity
|
// Batched solid meshes stay resident while unrelated entity
|
||||||
// categories change.
|
// categories change.
|
||||||
if inner
|
if inner
|
||||||
|
|
@ -800,7 +824,7 @@ impl shader::Primitive for Primitive {
|
||||||
// Live overlay (command preview / interim / grip drag) — small and
|
// Live overlay (command preview / interim / grip drag) — small and
|
||||||
// refreshed every frame it's present, so a drag never re-uploads
|
// refreshed every frame it's present, so a drag never re-uploads
|
||||||
// the resident base wire buffer.
|
// the resident base wire buffer.
|
||||||
inner.upload_preview_wires(device, &vp.preview_wires[..], &vp.draw_depths);
|
inner.upload_preview_wires(device, &vp.preview_wires[..], &draw_depths);
|
||||||
inner.upload_preview_text(device, queue, &vp.preview_text_verts[..]);
|
inner.upload_preview_text(device, queue, &vp.preview_text_verts[..]);
|
||||||
// Cull / scissor / LOD project AABBs relative-to-eye (matching the
|
// Cull / scissor / LOD project AABBs relative-to-eye (matching the
|
||||||
// GPU's RTE path) so the math stays precise at UTM-scale coords.
|
// GPU's RTE path) so the math stays precise at UTM-scale coords.
|
||||||
|
|
@ -819,8 +843,35 @@ impl shader::Primitive for Primitive {
|
||||||
inner.upload_silhouettes(device, &[], vp.view_dir);
|
inner.upload_silhouettes(device, &[], vp.view_dir);
|
||||||
}
|
}
|
||||||
inner.upload_clip_boundary(device, &vp.clip_boundary_ndc);
|
inner.upload_clip_boundary(device, &vp.clip_boundary_ndc);
|
||||||
|
let hatch_lod_key = (
|
||||||
|
Arc::as_ptr(&vp.hatches) as usize,
|
||||||
|
vp.camera_generation,
|
||||||
|
clip_size.width,
|
||||||
|
clip_size.height,
|
||||||
|
fill_mode,
|
||||||
|
);
|
||||||
|
if inner.hatch_lod_key != hatch_lod_key {
|
||||||
inner.compute_hatch_lod(queue, view_rot, eye, clip_size.width, clip_size.height);
|
inner.compute_hatch_lod(queue, view_rot, eye, clip_size.width, clip_size.height);
|
||||||
|
inner.hatch_lod_key = hatch_lod_key;
|
||||||
|
}
|
||||||
|
let wipeout_lod_key = (
|
||||||
|
Arc::as_ptr(&vp.wipeout_hatches) as usize,
|
||||||
|
vp.camera_generation,
|
||||||
|
clip_size.width,
|
||||||
|
clip_size.height,
|
||||||
|
fill_mode,
|
||||||
|
);
|
||||||
|
if inner.wipeout_lod_key != wipeout_lod_key {
|
||||||
inner.compute_wipeout_lod(view_rot, eye, clip_size.width, clip_size.height);
|
inner.compute_wipeout_lod(view_rot, eye, clip_size.width, clip_size.height);
|
||||||
|
inner.wipeout_lod_key = wipeout_lod_key;
|
||||||
|
}
|
||||||
|
let mesh_lod_key = (
|
||||||
|
Arc::as_ptr(&vp.meshes) as usize,
|
||||||
|
vp.camera_generation,
|
||||||
|
clip_size.width,
|
||||||
|
clip_size.height,
|
||||||
|
);
|
||||||
|
if inner.mesh_lod_key != mesh_lod_key {
|
||||||
inner.compute_mesh_lod(
|
inner.compute_mesh_lod(
|
||||||
queue,
|
queue,
|
||||||
view_rot,
|
view_rot,
|
||||||
|
|
@ -828,6 +879,8 @@ impl shader::Primitive for Primitive {
|
||||||
clip_size.width,
|
clip_size.width,
|
||||||
clip_size.height,
|
clip_size.height,
|
||||||
);
|
);
|
||||||
|
inner.mesh_lod_key = mesh_lod_key;
|
||||||
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
let cull_key = (
|
let cull_key = (
|
||||||
|
|
@ -881,6 +934,7 @@ impl shader::Primitive for Primitive {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let prepare_ms = nav_prepare_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if let Some(sample) = self.nav_perf {
|
if let Some(sample) = self.nav_perf {
|
||||||
crate::perf::record(format_args!(
|
crate::perf::record(format_args!(
|
||||||
"[perf] nav-prepare op={} space={} mode={} input={:.2}ms build={:.2}ms prepare={:.2}ms elapsed={:.2}ms viewports={}",
|
"[perf] nav-prepare op={} space={} mode={} input={:.2}ms build={:.2}ms prepare={:.2}ms elapsed={:.2}ms viewports={}",
|
||||||
|
|
@ -889,10 +943,16 @@ impl shader::Primitive for Primitive {
|
||||||
sample.mode,
|
sample.mode,
|
||||||
sample.input_ms,
|
sample.input_ms,
|
||||||
sample.build_ms,
|
sample.build_ms,
|
||||||
nav_prepare_started.elapsed().as_secs_f64() * 1000.0,
|
prepare_ms,
|
||||||
sample.started.elapsed().as_secs_f64() * 1000.0,
|
sample.started.elapsed().as_secs_f64() * 1000.0,
|
||||||
self.viewports.len(),
|
self.viewports.len(),
|
||||||
));
|
));
|
||||||
|
} else if crate::perf::enabled() && prepare_ms >= 5.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] frame-prepare {:>7.1}ms viewports={}",
|
||||||
|
prepare_ms,
|
||||||
|
self.viewports.len(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -970,16 +1030,23 @@ impl shader::Primitive for Primitive {
|
||||||
inner.viewcube.render(encoder, target, vp_clip);
|
inner.viewcube.render(encoder, target, vp_clip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let render_ms = nav_render_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if let Some(sample) = self.nav_perf {
|
if let Some(sample) = self.nav_perf {
|
||||||
crate::perf::record(format_args!(
|
crate::perf::record(format_args!(
|
||||||
"[perf] nav-render op={} space={} mode={} encode={:.2}ms elapsed={:.2}ms viewports={}",
|
"[perf] nav-render op={} space={} mode={} encode={:.2}ms elapsed={:.2}ms viewports={}",
|
||||||
sample.op.label(),
|
sample.op.label(),
|
||||||
sample.space,
|
sample.space,
|
||||||
sample.mode,
|
sample.mode,
|
||||||
nav_render_started.elapsed().as_secs_f64() * 1000.0,
|
render_ms,
|
||||||
sample.started.elapsed().as_secs_f64() * 1000.0,
|
sample.started.elapsed().as_secs_f64() * 1000.0,
|
||||||
self.viewports.len(),
|
self.viewports.len(),
|
||||||
));
|
));
|
||||||
|
} else if crate::perf::enabled() && render_ms >= 5.0 {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] frame-encode {:>7.1}ms viewports={}",
|
||||||
|
render_ms,
|
||||||
|
self.viewports.len(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2062,11 +2129,12 @@ impl Scene {
|
||||||
} else {
|
} else {
|
||||||
0x3000_0000_0000_0000 | inst.handle.value()
|
0x3000_0000_0000_0000 | inst.handle.value()
|
||||||
};
|
};
|
||||||
|
let draw_depths = self.draw_depth_map();
|
||||||
let text_verts = self.gather_text_verts(
|
let text_verts = self.gather_text_verts(
|
||||||
&all_wires,
|
&all_wires,
|
||||||
wire_content_id,
|
wire_content_id,
|
||||||
text_source_key,
|
text_source_key,
|
||||||
&self.draw_depth_map(),
|
&draw_depths,
|
||||||
);
|
);
|
||||||
// Grip-drag / command-preview glyphs, excluded from the epoch-cached base
|
// Grip-drag / command-preview glyphs, excluded from the epoch-cached base
|
||||||
// gather above. Two sources, both tiny (one operation's worth) and walked
|
// gather above. Two sources, both tiny (one operation's worth) and walked
|
||||||
|
|
@ -2117,7 +2185,7 @@ impl Scene {
|
||||||
face3d_wires,
|
face3d_wires,
|
||||||
text_verts,
|
text_verts,
|
||||||
preview_text_verts,
|
preview_text_verts,
|
||||||
draw_depths: self.draw_depth_map(),
|
draw_depths: Arc::downgrade(&draw_depths),
|
||||||
hatches,
|
hatches,
|
||||||
wipeout_hatches,
|
wipeout_hatches,
|
||||||
images,
|
images,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue