* Viewport: keep the model scene in blocks and upload only what changed Every change rebuilt the whole model scene (every body's triangles and edges) and uploaded all of it again, so a design an agent builds one body at a time paid for the whole model on every command. The scene is now built in blocks of a few bodies, cut where a body's own key says, and each block's bytes and GPU buffers are kept while the block comes back unchanged. Body boxes (fit, picking, ground shadow) are kept per mesh. Rendering is unchanged: the bicycle from bench-agent opens to a byte-identical screenshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Browser: find an occurrence's placed bodies by name, not by search per row Each body row of a placed component searched the placement's bodies for its own, every frame: quadratic in the component's bodies (720 for the bench-agent bicycle's hardware). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9b0f028f13
commit
39bc87a8cc
5 files changed
+278
-60
No files matched your search
@@ -1374,9 +1374,15 @@ fn folder_rows_at(app: &mut SolveApp, ui: &mut egui::Ui, comp: u64, path: Option
|
||||
let prefix = path.map(occurrence_key).unwrap_or_else(|| format!("c{comp}"));
|
||||
if let Some(path) = path {
|
||||
let Ok(placement) = app.session.occurrence_placement(path) else { return };
|
||||
// Each row's placed body by its definition's name (a search per row was quadratic in a
|
||||
// component's bodies, every frame).
|
||||
let mut placed: std::collections::HashMap<&str, &_> = std::collections::HashMap::with_capacity(placement.bodies.len());
|
||||
for b in &placement.bodies {
|
||||
placed.entry(b.source.as_str()).or_insert(b);
|
||||
}
|
||||
for e in &mut all {
|
||||
if folder == "bodies" {
|
||||
let Some(body) = placement.bodies.iter().find(|b| b.source == e.key) else { continue };
|
||||
let Some(body) = placed.get(e.key.as_str()) else { continue };
|
||||
e.sel = Some(Sel::Body { name: body.world.clone() });
|
||||
e.target = Some(Target::Body { name: body.world.clone() });
|
||||
e.bodies = vec![body.world.clone()];
|
||||
|
||||
@@ -51,6 +51,49 @@ pub struct GpuScene {
|
||||
pub under: Vec<u8>,
|
||||
/// Opaque bodies drawn from their plans on the GPU, like `tris`.
|
||||
pub planned: Vec<PlannedBody>,
|
||||
/// The model's bodies, a few at a time, drawn before the loose geometry above (`tris`,
|
||||
/// `trans`, `lines` and `hidden` of each block, in order, then the scene's own). A block
|
||||
/// that comes back unchanged keeps its buffers: a design changing one body at a time (an
|
||||
/// agent adding bodies over MCP) uploads only the blocks that changed, not the whole model.
|
||||
pub blocks: Vec<SceneBlock>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl GpuScene {
|
||||
/// The blocks' geometry moved into the scene's own lists, in drawing order (for tests that
|
||||
/// count what the scene holds).
|
||||
pub fn merged(mut self) -> GpuScene {
|
||||
let mut out = GpuScene { glass: std::mem::take(&mut self.glass), ..Default::default() };
|
||||
for block in std::mem::take(&mut self.blocks) {
|
||||
out.tris.extend_from_slice(&block.bytes.tris);
|
||||
out.trans.extend_from_slice(&block.bytes.trans);
|
||||
out.lines.extend_from_slice(&block.bytes.lines);
|
||||
out.hidden.extend_from_slice(&block.bytes.hidden);
|
||||
}
|
||||
out.tris.append(&mut self.tris);
|
||||
out.trans.append(&mut self.trans);
|
||||
out.lines.append(&mut self.lines);
|
||||
out.hidden.append(&mut self.hidden);
|
||||
GpuScene { tris: out.tris, trans: out.trans, glass: out.glass, lines: out.lines, hidden: out.hidden, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// A run of the model's bodies in the scene: their triangles and edges, and a key that names
|
||||
/// exactly this content (the viewport keeps the bytes while the key comes back).
|
||||
#[derive(Clone)]
|
||||
pub struct SceneBlock {
|
||||
pub key: u64,
|
||||
pub bytes: Arc<BlockBytes>,
|
||||
}
|
||||
|
||||
/// A block's geometry, laid out as in [`GpuScene`].
|
||||
#[derive(Default)]
|
||||
pub struct BlockBytes {
|
||||
pub tris: Vec<u8>,
|
||||
pub trans: Vec<u8>,
|
||||
pub glass: Vec<u8>,
|
||||
pub lines: Vec<u8>,
|
||||
pub hidden: Vec<u8>,
|
||||
}
|
||||
|
||||
impl GpuScene {
|
||||
@@ -194,8 +237,28 @@ struct PlannedDraw {
|
||||
colors: wgpu::Buffer,
|
||||
}
|
||||
|
||||
/// A scene block on the GPU, with the bytes it was uploaded from.
|
||||
struct BlockGpu {
|
||||
bytes: Arc<BlockBytes>,
|
||||
tris: Option<Batch>,
|
||||
trans: Option<Batch>,
|
||||
lines: Option<Batch>,
|
||||
hidden: Option<Batch>,
|
||||
}
|
||||
|
||||
/// The blocks' buffers, then the scene's own, as one batch (the blocks' buffers are shared,
|
||||
/// not copied).
|
||||
fn joined<'a>(blocks: impl Iterator<Item = &'a Option<Batch>>, own: Option<Batch>) -> Option<Batch> {
|
||||
let mut parts: Vec<(wgpu::Buffer, u32)> = blocks.flatten().flat_map(|b| b.parts.iter().cloned()).collect();
|
||||
let chunk = own.as_ref().map(|b| b.chunk).unwrap_or(0);
|
||||
parts.extend(own.into_iter().flat_map(|b| b.parts));
|
||||
(!parts.is_empty()).then_some(Batch { parts, chunk })
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Batches {
|
||||
/// The scene blocks on the GPU, by key.
|
||||
blocks: std::collections::HashMap<u64, BlockGpu>,
|
||||
key: Option<u64>,
|
||||
tris: Option<Batch>,
|
||||
trans: Option<Batch>,
|
||||
@@ -227,13 +290,33 @@ impl Batches {
|
||||
Some((tess, encoder)) => self.planned(device, tess, encoder, &sc.planned),
|
||||
None => Vec::new(),
|
||||
};
|
||||
self.tris = upload(device, "sc_tris", &sc.tris, TRI_SIZE);
|
||||
self.trans = upload(device, "sc_trans", &sc.trans, TRI_SIZE);
|
||||
let mut kept = std::mem::take(&mut self.blocks);
|
||||
let mut blocks: Vec<&BlockGpu> = Vec::with_capacity(sc.blocks.len());
|
||||
for block in &sc.blocks {
|
||||
let gpu = match kept.remove(&block.key).filter(|g| Arc::ptr_eq(&g.bytes, &block.bytes)) {
|
||||
Some(g) => g,
|
||||
None => BlockGpu {
|
||||
bytes: block.bytes.clone(),
|
||||
tris: upload(device, "sc_tris", &block.bytes.tris, TRI_SIZE),
|
||||
trans: upload(device, "sc_trans", &block.bytes.trans, TRI_SIZE),
|
||||
lines: upload(device, "sc_lines", &block.bytes.lines, LINE_SIZE),
|
||||
hidden: upload(device, "sc_hidden", &block.bytes.hidden, LINE_SIZE),
|
||||
},
|
||||
};
|
||||
self.blocks.insert(block.key, gpu);
|
||||
}
|
||||
for block in &sc.blocks {
|
||||
if let Some(g) = self.blocks.get(&block.key) {
|
||||
blocks.push(g);
|
||||
}
|
||||
}
|
||||
self.tris = joined(blocks.iter().map(|b| &b.tris), upload(device, "sc_tris", &sc.tris, TRI_SIZE));
|
||||
self.trans = joined(blocks.iter().map(|b| &b.trans), upload(device, "sc_trans", &sc.trans, TRI_SIZE));
|
||||
self.glass = upload_with(device, "sc_glass", &sc.glass, TRI_SIZE, wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST);
|
||||
self.glass_cpu = sc.glass;
|
||||
self.glass_sorted_for = None;
|
||||
self.lines = upload(device, "sc_lines", &sc.lines, LINE_SIZE);
|
||||
self.hidden = upload(device, "sc_hidden", &sc.hidden, LINE_SIZE);
|
||||
self.lines = joined(blocks.iter().map(|b| &b.lines), upload(device, "sc_lines", &sc.lines, LINE_SIZE));
|
||||
self.hidden = joined(blocks.iter().map(|b| &b.hidden), upload(device, "sc_hidden", &sc.hidden, LINE_SIZE));
|
||||
self.overlays = upload(device, "sc_overlay", &sc.overlay, LINE_SIZE);
|
||||
self.ghost = upload(device, "sc_ghost", &sc.ghost, TRI_SIZE);
|
||||
self.xray = upload(device, "sc_xray", &sc.xray, TRI_SIZE);
|
||||
|
||||
+167
-54
@@ -38,6 +38,10 @@ pub struct ViewportState {
|
||||
/// Per-body boxes of one world state, held (not just its address, which a later state can
|
||||
/// reuse once the old one is freed, so stale boxes would come back).
|
||||
pick_bounds: std::sync::Mutex<Option<BodyBounds>>,
|
||||
/// The model scene's blocks of bodies by key, from the last scene built.
|
||||
pub(crate) blocks: std::sync::Mutex<std::collections::HashMap<u64, CachedBlock>>,
|
||||
/// Each body mesh's box (the mesh held, so its address stays its own), from the last state.
|
||||
mesh_bounds: std::sync::Mutex<std::collections::HashMap<usize, (std::sync::Arc<solidcraft_engine::geom::Mesh>, solidcraft_engine::geom::Aabb3)>>,
|
||||
/// The last click in the view was the middle button's (for the middle double-click).
|
||||
middle_clicked: bool,
|
||||
/// The last pick (its key and hits): reused while nothing it depends on changed.
|
||||
@@ -350,6 +354,110 @@ fn shown_mesh(
|
||||
.unwrap_or_else(|| b.mesh())
|
||||
}
|
||||
|
||||
/// A model scene block kept by the viewport: its bytes and the meshes they were built from
|
||||
/// (held, so no other mesh can take their addresses while the block is kept).
|
||||
pub(crate) struct CachedBlock {
|
||||
bytes: std::sync::Arc<crate::gpu::BlockBytes>,
|
||||
meshes: Vec<std::sync::Arc<solidcraft_engine::geom::Mesh>>,
|
||||
}
|
||||
|
||||
/// One body as the model scene draws it.
|
||||
struct BodyDraw {
|
||||
mesh: std::sync::Arc<solidcraft_engine::geom::Mesh>,
|
||||
col: [u8; 4],
|
||||
faded: bool,
|
||||
opacity: f64,
|
||||
face_looks: std::collections::HashMap<usize, solidcraft_engine::doc::appearance::Look>,
|
||||
/// Its faces are drawn from its plan on the GPU (only its edges are built here).
|
||||
on_gpu: bool,
|
||||
}
|
||||
|
||||
impl BodyDraw {
|
||||
/// Everything [`BodyDraw::build`] reads.
|
||||
fn key(&self, shaded: bool, edges: bool, hidden_edges: bool, edge_col: [u8; 4], hidden_col: [u8; 4]) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(std::sync::Arc::as_ptr(&self.mesh) as usize).hash(&mut h);
|
||||
(self.col, self.faded, self.opacity.to_bits(), self.on_gpu, shaded, edges, hidden_edges, edge_col, hidden_col).hash(&mut h);
|
||||
let mut looks: Vec<_> = self.face_looks.iter().map(|(f, l)| (*f, l.color, l.opacity.to_bits())).collect();
|
||||
looks.sort_unstable();
|
||||
looks.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn build(&self, sc: &mut crate::gpu::BlockBytes, shaded: bool, edges: bool, hidden_edges: bool, edge_col: [u8; 4], hidden_col: [u8; 4]) {
|
||||
let m = &self.mesh;
|
||||
let mut out = GpuScene::default();
|
||||
for (ti, t) in m.triangles.iter().enumerate().filter(|_| shaded && !self.on_gpu) {
|
||||
let (mut col, a) = match m.tri_face.get(ti).and_then(|f| self.face_looks.get(&(*f as usize))) {
|
||||
Some(l) => {
|
||||
let c = Color32::from_rgb(l.color[0], l.color[1], l.color[2]);
|
||||
(if self.faded { c4(c.gamma_multiply(0.35)) } else { c4(c) }, l.opacity)
|
||||
}
|
||||
None => (self.col, self.opacity),
|
||||
};
|
||||
let glass = !self.faded && a < 0.999;
|
||||
if glass {
|
||||
col[3] = (a.clamp(0.05, 1.0) * 255.0).round() as u8;
|
||||
}
|
||||
for k in t {
|
||||
let i = *k as usize;
|
||||
if let (Some(p), Some(n)) = (m.positions.get(i), m.normals.get(i)) {
|
||||
if self.faded {
|
||||
out.trans_tri(p.to_f32(), n.to_f32(), col);
|
||||
} else if glass {
|
||||
out.glass_tri(p.to_f32(), n.to_f32(), col);
|
||||
} else {
|
||||
out.tri(p.to_f32(), n.to_f32(), col);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (ei, e) in m.edges.iter().enumerate().filter(|_| edges) {
|
||||
if !m.edge_pickable(ei) || (shaded && !m.shaded_edge_visible(ei)) {
|
||||
continue;
|
||||
}
|
||||
for w in e.windows(2) {
|
||||
out.line(w[0].to_f32(), w[1].to_f32(), edge_col, 1.3, false);
|
||||
if hidden_edges {
|
||||
out.hidden_line(w[0].to_f32(), w[1].to_f32(), hidden_col, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
sc.tris.extend_from_slice(&out.tris);
|
||||
sc.trans.extend_from_slice(&out.trans);
|
||||
sc.glass.extend_from_slice(&out.glass);
|
||||
sc.lines.extend_from_slice(&out.lines);
|
||||
sc.hidden.extend_from_slice(&out.hidden);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a block of bodies to the scene: the bytes built for the same bodies last time when
|
||||
/// there were any, else built now.
|
||||
fn push_block(
|
||||
sc: &mut GpuScene,
|
||||
kept: &mut std::collections::HashMap<u64, CachedBlock>,
|
||||
made: &mut std::collections::HashMap<u64, CachedBlock>,
|
||||
key: u64,
|
||||
draws: Vec<BodyDraw>,
|
||||
build: impl Fn(&BodyDraw, &mut crate::gpu::BlockBytes),
|
||||
) {
|
||||
let same = |c: &CachedBlock| c.meshes.len() == draws.len() && c.meshes.iter().zip(&draws).all(|(m, d)| std::sync::Arc::ptr_eq(m, &d.mesh));
|
||||
let block = match kept.remove(&key).or_else(|| made.remove(&key)).filter(same) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
let mut bytes = crate::gpu::BlockBytes::default();
|
||||
for d in &draws {
|
||||
build(d, &mut bytes);
|
||||
}
|
||||
CachedBlock { bytes: std::sync::Arc::new(bytes), meshes: draws.iter().map(|d| d.mesh.clone()).collect() }
|
||||
}
|
||||
};
|
||||
// See-through triangles are sorted with the rest of the glass, every view.
|
||||
sc.glass.extend_from_slice(&block.bytes.glass);
|
||||
sc.blocks.push(crate::gpu::SceneBlock { key, bytes: block.bytes.clone() });
|
||||
made.insert(key, block);
|
||||
}
|
||||
|
||||
fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let tk = Tokens::get();
|
||||
let mut sc = GpuScene::default();
|
||||
@@ -357,6 +465,20 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
// Bodies where their occurrences place them.
|
||||
let st = s.world_state();
|
||||
let active_bodies = s.active_occurrence_path.as_ref().and_then(|p| s.occurrence_bodies(p, true).ok());
|
||||
let (shaded, edges) = match app.ui.visual_style {
|
||||
1 => (true, false),
|
||||
2 => (false, true),
|
||||
_ => (true, true),
|
||||
};
|
||||
let edge_col = if shaded { c4(tk.body_edge) } else { c4(tk.text) };
|
||||
// Shaded with hidden edges: edges behind the model show faintly.
|
||||
let hidden_edges = app.ui.visual_style == 3;
|
||||
let hidden_col = c4(tk.text.gamma_multiply(0.35));
|
||||
// The blocks the last scene built, reused when their bodies come back as they were.
|
||||
let mut kept = app.viewport.blocks.lock().map(|mut b| std::mem::take(&mut *b)).unwrap_or_default();
|
||||
let mut made: std::collections::HashMap<u64, CachedBlock> = std::collections::HashMap::new();
|
||||
let mut run: Vec<BodyDraw> = Vec::new();
|
||||
let mut run_key = std::collections::hash_map::DefaultHasher::new();
|
||||
for b in &st.bodies {
|
||||
if !app.session.body_visible(&b.name) || app.preview.replaced.contains(&b.name) {
|
||||
continue;
|
||||
@@ -364,16 +486,6 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let faded = body_faded(app, b, &active_bodies);
|
||||
let base = body_color(&s.doc, b).unwrap_or(tk.body);
|
||||
let col = if faded { c4(base.gamma_multiply(0.35)) } else { c4(base) };
|
||||
let m = b.mesh();
|
||||
let (shaded, edges) = match app.ui.visual_style {
|
||||
1 => (true, false),
|
||||
2 => (false, true),
|
||||
_ => (true, true),
|
||||
};
|
||||
let edge_col = if shaded { c4(tk.body_edge) } else { c4(tk.text) };
|
||||
// Shaded with hidden edges: edges behind the model show faintly.
|
||||
let hidden_edges = app.ui.visual_style == 3;
|
||||
let hidden_col = c4(tk.text.gamma_multiply(0.35));
|
||||
// Appearances: the body's opacity, and faces with looks of their own.
|
||||
let opacity = s.doc.model_body_opacity(b);
|
||||
let face_looks: std::collections::HashMap<usize, solidcraft_engine::doc::appearance::Look> =
|
||||
@@ -390,43 +502,24 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
.collect();
|
||||
sc.planned.push(crate::gpu::PlannedBody { plan, face_colors });
|
||||
}
|
||||
for (ti, t) in m.triangles.iter().enumerate().filter(|_| shaded && !on_gpu) {
|
||||
let (mut col, a) = match m.tri_face.get(ti).and_then(|f| face_looks.get(&(*f as usize))) {
|
||||
Some(l) => {
|
||||
let c = Color32::from_rgb(l.color[0], l.color[1], l.color[2]);
|
||||
(if faded { c4(c.gamma_multiply(0.35)) } else { c4(c) }, l.opacity)
|
||||
}
|
||||
None => (col, opacity),
|
||||
};
|
||||
let glass = !faded && a < 0.999;
|
||||
if glass {
|
||||
col[3] = (a.clamp(0.05, 1.0) * 255.0).round() as u8;
|
||||
}
|
||||
for k in t {
|
||||
let i = *k as usize;
|
||||
if let (Some(p), Some(n)) = (m.positions.get(i), m.normals.get(i)) {
|
||||
if faded {
|
||||
sc.trans_tri(p.to_f32(), n.to_f32(), col);
|
||||
} else if glass {
|
||||
sc.glass_tri(p.to_f32(), n.to_f32(), col);
|
||||
} else {
|
||||
sc.tri(p.to_f32(), n.to_f32(), col);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (ei, e) in m.edges.iter().enumerate().filter(|_| edges) {
|
||||
if !m.edge_pickable(ei) || (shaded && !m.shaded_edge_visible(ei)) {
|
||||
continue;
|
||||
}
|
||||
for w in e.windows(2) {
|
||||
sc.line(w[0].to_f32(), w[1].to_f32(), edge_col, 1.3, false);
|
||||
if hidden_edges {
|
||||
sc.hidden_line(w[0].to_f32(), w[1].to_f32(), hidden_col, 1.0);
|
||||
}
|
||||
}
|
||||
let body = BodyDraw { mesh: b.mesh(), col, faded, opacity, face_looks, on_gpu };
|
||||
let key = body.key(shaded, edges, hidden_edges, edge_col, hidden_col);
|
||||
key.hash(&mut run_key);
|
||||
run.push(body);
|
||||
// Blocks end where a body's own key says, so a body added or removed changes only its
|
||||
// own block, not every block after it.
|
||||
if key.is_multiple_of(16) || run.len() >= 64 {
|
||||
let draws = std::mem::take(&mut run);
|
||||
let block_key = std::mem::replace(&mut run_key, std::collections::hash_map::DefaultHasher::new()).finish();
|
||||
push_block(&mut sc, &mut kept, &mut made, block_key, draws, |d, out| d.build(out, shaded, edges, hidden_edges, edge_col, hidden_col));
|
||||
}
|
||||
}
|
||||
if !run.is_empty() {
|
||||
push_block(&mut sc, &mut kept, &mut made, run_key.finish(), run, |d, out| d.build(out, shaded, edges, hidden_edges, edge_col, hidden_col));
|
||||
}
|
||||
if let Ok(mut b) = app.viewport.blocks.lock() {
|
||||
*b = made;
|
||||
}
|
||||
if app.ui.ground_shadow && app.ui.visual_style != 2 && app.cam.pitch > 0.0 {
|
||||
ground_shadow(&mut sc, app, &st);
|
||||
}
|
||||
@@ -511,12 +604,12 @@ fn build_sketch_scene(app: &SolveApp, proj: &Proj) -> GpuScene {
|
||||
sc
|
||||
}
|
||||
|
||||
fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &solidcraft_engine::doc::ModelState) {
|
||||
fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &std::sync::Arc<solidcraft_engine::doc::ModelState>) {
|
||||
// The bodies' box in the camera's Z-up frame (the ground is its XY plane).
|
||||
let mut b = solidcraft_engine::geom::Aabb3::EMPTY;
|
||||
for x in st.bodies.iter().filter(|x| app.session.body_visible(&x.name)) {
|
||||
let bb = x.mesh().bounds();
|
||||
if bb.is_empty() {
|
||||
let boxes = body_bounds(app, st);
|
||||
for (x, (_, bb)) in st.bodies.iter().zip(boxes.iter()) {
|
||||
if bb.is_empty() || !app.session.body_visible(&x.name) {
|
||||
continue;
|
||||
}
|
||||
for i in 0..8 {
|
||||
@@ -705,7 +798,27 @@ fn body_bounds(
|
||||
{
|
||||
return b.clone();
|
||||
}
|
||||
let b = std::sync::Arc::new(st.bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect::<Vec<_>>());
|
||||
// Boxes of meshes the last state had too are kept (each body's box walks its mesh).
|
||||
let mut known = app.viewport.mesh_bounds.lock().map(|mut k| std::mem::take(&mut *k)).unwrap_or_default();
|
||||
let mut now = std::collections::HashMap::with_capacity(st.bodies.len());
|
||||
let b = std::sync::Arc::new(
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let mesh = b.mesh();
|
||||
let ptr = std::sync::Arc::as_ptr(&mesh) as usize;
|
||||
let bounds = match known.remove(&ptr).or_else(|| now.remove(&ptr)) {
|
||||
Some((_, bounds)) => bounds,
|
||||
None => mesh.bounds(),
|
||||
};
|
||||
now.insert(ptr, (mesh, bounds));
|
||||
(b.name.clone(), bounds)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
if let Ok(mut k) = app.viewport.mesh_bounds.lock() {
|
||||
*k = now;
|
||||
}
|
||||
*g = Some((st.clone(), b.clone()));
|
||||
return b;
|
||||
}
|
||||
@@ -3344,7 +3457,7 @@ mod tests {
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
|
||||
let mut render = |tess: bool| {
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess });
|
||||
let scene = build_scene(&app);
|
||||
let scene = build_scene(&app).merged();
|
||||
let proj = projection(&app, rect);
|
||||
let e = proj.cam.eye().to_f32();
|
||||
let eye = [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }];
|
||||
@@ -3392,7 +3505,7 @@ mod tests {
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess });
|
||||
const SIZE: u32 = 256;
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
|
||||
let scene = build_scene(&app);
|
||||
let scene = build_scene(&app).merged();
|
||||
assert!(scene.planned.is_empty(), "no body waits for the GPU evaluation");
|
||||
assert!(!scene.tris.is_empty(), "the bodies are meshed on the CPU");
|
||||
let proj = projection(&app, rect);
|
||||
@@ -3978,11 +4091,11 @@ mod tests {
|
||||
assert_eq!(mesh.smooth_edges.iter().filter(|f| **f).count(), 512);
|
||||
let smooth = mesh.smooth_edges.iter().position(|f| *f).unwrap();
|
||||
let expected = mesh.edges.iter().enumerate().filter(|(i, _)| mesh.shaded_edge_visible(*i)).map(|(_, e)| e.len() - 1).sum::<usize>();
|
||||
let shaded = build_scene(&app);
|
||||
let shaded = build_scene(&app).merged();
|
||||
assert_eq!(shaded.lines.len() / crate::gpu::LINE_SIZE, expected);
|
||||
assert!(!shaded.tris.is_empty());
|
||||
app.ui.visual_style = 2;
|
||||
let wire = build_scene(&app);
|
||||
let wire = build_scene(&app).merged();
|
||||
let expected_wire =
|
||||
mesh.edges.iter().enumerate().filter(|(i, _)| !mesh.seams.get(*i).copied().unwrap_or(false)).map(|(_, e)| e.len() - 1).sum::<usize>();
|
||||
assert_eq!(wire.lines.len() / crate::gpu::LINE_SIZE, expected_wire);
|
||||
|
||||
@@ -31,7 +31,7 @@ fn placed_step_body_uses_imported_opacity_until_a_design_look_overrides_it() {
|
||||
assert!((appearance.opacity - 0.4).abs() < 1e-6);
|
||||
assert_eq!(appearance.color, [51, 153, 204]);
|
||||
crate::dialogs::cancel(&mut app);
|
||||
let scene = build_scene(&app);
|
||||
let scene = build_scene(&app).merged();
|
||||
assert!(!scene.glass.is_empty());
|
||||
assert!(
|
||||
scene.tris.is_empty(),
|
||||
|
||||
@@ -71,6 +71,22 @@ once per command), but slowly. `solidcraft-cli eval` output for the design corpu
|
||||
the regression scripts, the generated bicycle and a 3,300-feature agent-built design is
|
||||
unchanged apart from the timings.
|
||||
|
||||
The viewport rebuilt its whole model scene after every change and uploaded all of it again
|
||||
(every body's triangles and edges, 60 MB for the bicycle where bodies are built on the CPU). It
|
||||
now builds the scene in blocks of a few bodies, cut where a body's own key says so that adding
|
||||
or removing a body changes only its block, and keeps each block's bytes and GPU buffers while
|
||||
the block comes back unchanged. The bodies' boxes (fit, picking, the ground shadow) are kept per
|
||||
mesh the same way. Built command by command over the control channel (`engine.execute`, then
|
||||
`ui.inspect`) in the desktop app under Xvfb with software GL:
|
||||
|
||||
| desktop, bicycle built over the control channel | main | with #1148 and this |
|
||||
|---|---:|---:|
|
||||
| UI work per change at ~1,000 bodies (`frame_ms`) | 1,374 ms | 18 ms |
|
||||
| model scene build at ~1,000 bodies (`build_ms`) | 41 ms | 1.4 ms |
|
||||
| whole build | 1,409 s | 210 s |
|
||||
|
||||
Most of what is left there is software rendering of 770k triangles per frame.
|
||||
|
||||
## STEP import: shells and item solids without copies (2026-10-13)
|
||||
|
||||
A file that lists one product definition many times (a bolt used 70 times, a panel in several
|
||||
|
||||
Reference in new issue
Block a user