perf(scene): frustum-cull wires using camera view AABB
Block defns and top-level entities now know their world-space XY
AABB, and `tessellate_entity` / `expand_insert` early-out for anything
that doesn't intersect the camera's view rect. Tested commit-only on
time_open (correct subset emitted with default camera); GUI run
pending user verification.
Cache plumbing:
- `LocalWire` / `BlockDefn` carry `aabb_local` (post-build recursive
union over nested defns).
- `Scene::view_world_aabb()` derives a 25%-margined world rect from
the active camera + last-seen render aspect.
- `wire_cache` / `paper_sheet_cache` keys widen to
`(geometry_epoch, camera_generation)` so pan/zoom invalidates the
cached wire list — and pipeline `cached_epoch` widens to match
so the GPU re-uploads on view change.
- `XrefStatus::Unloaded` ack already shipped — orthogonal to this.
Also adds `examples/xref_states.rs` to dump per-block_record xref
metadata for diagnosing the is_loaded / unloaded distinction.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e88a946176
commit
78cbb5570e
5 changed files with 339 additions and 24 deletions
23
examples/xref_states.rs
Normal file
23
examples/xref_states.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Dump every xref block_record's raw is_loaded bit before any resolve.
|
||||||
|
// cargo run --release --example xref_states -- <dwg>
|
||||||
|
use acadrust::io::dwg::DwgReader;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let path = std::env::args().nth(1).unwrap();
|
||||||
|
let doc = DwgReader::from_file(&path)?.read()?;
|
||||||
|
|
||||||
|
for br in doc.block_records.iter() {
|
||||||
|
if br.flags.is_xref || br.flags.is_xref_overlay {
|
||||||
|
println!(
|
||||||
|
"name={:<30} is_xref={} overlay={} is_loaded={:?} xref_path={:?} entity_handles={}",
|
||||||
|
br.name,
|
||||||
|
br.flags.is_xref,
|
||||||
|
br.flags.is_xref_overlay,
|
||||||
|
br.is_loaded,
|
||||||
|
br.xref_path,
|
||||||
|
br.entity_handles.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -42,6 +42,11 @@ pub struct LocalWire {
|
||||||
pub color_is_byblock: bool,
|
pub color_is_byblock: bool,
|
||||||
pub lt_is_byblock: bool,
|
pub lt_is_byblock: bool,
|
||||||
pub lw_is_byblock: bool,
|
pub lw_is_byblock: bool,
|
||||||
|
/// XY bounding box of this wire in block-local coordinates.
|
||||||
|
/// `[min_x, min_y, max_x, max_y]`. Used for view-frustum culling at
|
||||||
|
/// expand-time: transform corners by the Insert transform → world AABB
|
||||||
|
/// → test against the camera's world-space view rect.
|
||||||
|
pub aabb_local: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -69,6 +74,10 @@ pub enum LocalSub {
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct BlockDefn {
|
pub struct BlockDefn {
|
||||||
pub subs: Vec<LocalSub>,
|
pub subs: Vec<LocalSub>,
|
||||||
|
/// Union of every sub's local AABB (including nested-INSERT contributions
|
||||||
|
/// resolved at expand time via their own defn's `aabb_local`). XY only —
|
||||||
|
/// the wire renderer is 2D-dominant.
|
||||||
|
pub aabb_local: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
|
|
@ -97,8 +106,54 @@ impl BlockCache {
|
||||||
let defn = build_defn(doc, name, anno_scale, bg_color);
|
let defn = build_defn(doc, name, anno_scale, bg_color);
|
||||||
cache.defns.insert(name.clone(), Arc::new(defn));
|
cache.defns.insert(name.clone(), Arc::new(defn));
|
||||||
}
|
}
|
||||||
|
cache.compute_block_aabbs(&referenced);
|
||||||
cache
|
cache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute and store the `aabb_local` for every cached defn. Direct wires
|
||||||
|
/// contribute their own aabb_local; nested INSERT references look up the
|
||||||
|
/// nested defn (already cached) and transform its aabb_local by the
|
||||||
|
/// nested Insert's transform before unioning.
|
||||||
|
///
|
||||||
|
/// Run as a post-pass so it doesn't matter which order build_defn was
|
||||||
|
/// called in. Cycle guard: a self-referential block keeps an empty AABB
|
||||||
|
/// (will fail every frustum test → not emitted, which is correct).
|
||||||
|
fn compute_block_aabbs(&mut self, names: &[String]) {
|
||||||
|
// Snapshot defn pointers up front — we mutate the map below.
|
||||||
|
let names: Vec<String> = names.to_vec();
|
||||||
|
for name in &names {
|
||||||
|
let mut visited: Vec<String> = Vec::new();
|
||||||
|
let aabb = self.defn_aabb_recursive(name, &mut visited);
|
||||||
|
if let Some(defn_arc) = self.defns.get_mut(name) {
|
||||||
|
let mut defn = (**defn_arc).clone();
|
||||||
|
defn.aabb_local = aabb;
|
||||||
|
*defn_arc = Arc::new(defn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn defn_aabb_recursive(&self, block_name: &str, visited: &mut Vec<String>) -> [f32; 4] {
|
||||||
|
if visited.iter().any(|n| n == block_name) {
|
||||||
|
return [0.0, 0.0, 0.0, 0.0];
|
||||||
|
}
|
||||||
|
let Some(defn) = self.defns.get(block_name) else {
|
||||||
|
return [0.0, 0.0, 0.0, 0.0];
|
||||||
|
};
|
||||||
|
visited.push(block_name.to_string());
|
||||||
|
let mut acc = [0.0_f32, 0.0, 0.0, 0.0];
|
||||||
|
for sub in &defn.subs {
|
||||||
|
let aabb = match sub {
|
||||||
|
LocalSub::Wire(lw) => lw.aabb_local,
|
||||||
|
LocalSub::Nested(nref) => {
|
||||||
|
let nested_local = self.defn_aabb_recursive(&nref.block_name, visited);
|
||||||
|
transform_aabb_xy(nested_local, &nref.xform)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
acc = aabb_union(acc, aabb);
|
||||||
|
}
|
||||||
|
visited.pop();
|
||||||
|
acc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walk all entities + all block_record contents collecting every distinct
|
/// Walk all entities + all block_record contents collecting every distinct
|
||||||
|
|
@ -163,7 +218,10 @@ fn build_defn(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BlockDefn { subs }
|
BlockDefn {
|
||||||
|
subs,
|
||||||
|
aabb_local: [0.0; 4],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_nested_ref(
|
fn build_nested_ref(
|
||||||
|
|
@ -220,6 +278,10 @@ fn tessellate_sub_local(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let aabb_local = aabb_from_points_iter(
|
||||||
|
wire.points.iter().copied().chain(wire.fill_tris.iter().copied()),
|
||||||
|
);
|
||||||
|
|
||||||
Some(LocalWire {
|
Some(LocalWire {
|
||||||
points: wire.points,
|
points: wire.points,
|
||||||
key_vertices: wire.key_vertices,
|
key_vertices: wire.key_vertices,
|
||||||
|
|
@ -235,9 +297,89 @@ fn tessellate_sub_local(
|
||||||
color_is_byblock,
|
color_is_byblock,
|
||||||
lt_is_byblock,
|
lt_is_byblock,
|
||||||
lw_is_byblock,
|
lw_is_byblock,
|
||||||
|
aabb_local,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn aabb_from_points_iter<I: IntoIterator<Item = [f32; 3]>>(pts: I) -> [f32; 4] {
|
||||||
|
let mut min_x = f32::INFINITY;
|
||||||
|
let mut min_y = f32::INFINITY;
|
||||||
|
let mut max_x = f32::NEG_INFINITY;
|
||||||
|
let mut max_y = f32::NEG_INFINITY;
|
||||||
|
for p in pts {
|
||||||
|
if !p[0].is_finite() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if p[0] < min_x {
|
||||||
|
min_x = p[0];
|
||||||
|
}
|
||||||
|
if p[1] < min_y {
|
||||||
|
min_y = p[1];
|
||||||
|
}
|
||||||
|
if p[0] > max_x {
|
||||||
|
max_x = p[0];
|
||||||
|
}
|
||||||
|
if p[1] > max_y {
|
||||||
|
max_y = p[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min_x.is_infinite() {
|
||||||
|
[0.0, 0.0, 0.0, 0.0]
|
||||||
|
} else {
|
||||||
|
[min_x, min_y, max_x, max_y]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform a local-space XY AABB by `t` and return the world-space XY AABB
|
||||||
|
/// of the transformed corners. For non-rotated transforms the corners stay
|
||||||
|
/// axis-aligned; for arbitrary OCS we still get a correct (looser) AABB by
|
||||||
|
/// taking the min/max of the four transformed corner points.
|
||||||
|
fn transform_aabb_xy(local: [f32; 4], t: &Transform) -> [f32; 4] {
|
||||||
|
let [x0, y0, x1, y1] = local;
|
||||||
|
let corners = [
|
||||||
|
Vector3::new(x0 as f64, y0 as f64, 0.0),
|
||||||
|
Vector3::new(x1 as f64, y0 as f64, 0.0),
|
||||||
|
Vector3::new(x1 as f64, y1 as f64, 0.0),
|
||||||
|
Vector3::new(x0 as f64, y1 as f64, 0.0),
|
||||||
|
];
|
||||||
|
let mut min_x = f64::INFINITY;
|
||||||
|
let mut min_y = f64::INFINITY;
|
||||||
|
let mut max_x = f64::NEG_INFINITY;
|
||||||
|
let mut max_y = f64::NEG_INFINITY;
|
||||||
|
for c in corners {
|
||||||
|
let v = t.apply(c);
|
||||||
|
if v.x < min_x {
|
||||||
|
min_x = v.x;
|
||||||
|
}
|
||||||
|
if v.y < min_y {
|
||||||
|
min_y = v.y;
|
||||||
|
}
|
||||||
|
if v.x > max_x {
|
||||||
|
max_x = v.x;
|
||||||
|
}
|
||||||
|
if v.y > max_y {
|
||||||
|
max_y = v.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[min_x as f32, min_y as f32, max_x as f32, max_y as f32]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aabb_union(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||||
|
// [0,0,0,0] is the "empty AABB" sentinel produced by aabb_from_points_iter
|
||||||
|
// when a wire has no finite points — treat it as if the other side wins.
|
||||||
|
if a == [0.0, 0.0, 0.0, 0.0] {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
if b == [0.0, 0.0, 0.0, 0.0] {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
[a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aabb_disjoint_xy(a: [f32; 4], b: [f32; 4]) -> bool {
|
||||||
|
a[2] < b[0] || a[0] > b[2] || a[3] < b[1] || a[1] > b[3]
|
||||||
|
}
|
||||||
|
|
||||||
// ── Use-time expansion ───────────────────────────────────────────────────────
|
// ── Use-time expansion ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Expand one top-level INSERT into world-space WireModels via the cache.
|
/// Expand one top-level INSERT into world-space WireModels via the cache.
|
||||||
|
|
@ -255,12 +397,32 @@ pub fn expand_insert(
|
||||||
selected: bool,
|
selected: bool,
|
||||||
world_offset: [f64; 3],
|
world_offset: [f64; 3],
|
||||||
pslt_factor: f32,
|
pslt_factor: f32,
|
||||||
|
// World-space XY view AABB (with world_offset already subtracted, so the
|
||||||
|
// comparison is in the same f32 space as emitted wires). `None` disables
|
||||||
|
// frustum culling — every cached sub is emitted.
|
||||||
|
view_aabb: Option<[f32; 4]>,
|
||||||
) -> Option<Vec<WireModel>> {
|
) -> Option<Vec<WireModel>> {
|
||||||
let defn = cache.defn(&ins.block_name)?;
|
let defn = cache.defn(&ins.block_name)?;
|
||||||
let xform = ins.get_transform();
|
let xform = ins.get_transform();
|
||||||
let name = ins_handle.value().to_string();
|
let name = ins_handle.value().to_string();
|
||||||
let mut batches = Batches::default();
|
let mut batches = Batches::default();
|
||||||
let mut visited: Vec<String> = Vec::with_capacity(8);
|
let mut visited: Vec<String> = Vec::with_capacity(8);
|
||||||
|
let [ox, oy, _] = world_offset;
|
||||||
|
|
||||||
|
// Whole-Insert cull: if the Insert's world AABB doesn't intersect the
|
||||||
|
// view, bail without doing any per-sub work.
|
||||||
|
if let Some(view) = view_aabb {
|
||||||
|
let insert_world = transform_aabb_xy(defn.aabb_local, &xform);
|
||||||
|
let insert_local = [
|
||||||
|
(insert_world[0] - ox as f32),
|
||||||
|
(insert_world[1] - oy as f32),
|
||||||
|
(insert_world[2] - ox as f32),
|
||||||
|
(insert_world[3] - oy as f32),
|
||||||
|
];
|
||||||
|
if aabb_disjoint_xy(insert_local, view) {
|
||||||
|
return Some(vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for offset in &array_offsets(ins) {
|
for offset in &array_offsets(ins) {
|
||||||
let base_xform = if offset == &[0.0; 3] {
|
let base_xform = if offset == &[0.0; 3] {
|
||||||
|
|
@ -280,6 +442,7 @@ pub fn expand_insert(
|
||||||
selected,
|
selected,
|
||||||
world_offset,
|
world_offset,
|
||||||
pslt_factor,
|
pslt_factor,
|
||||||
|
view_aabb,
|
||||||
};
|
};
|
||||||
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0);
|
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -295,6 +458,8 @@ struct ExpandCtx<'a> {
|
||||||
selected: bool,
|
selected: bool,
|
||||||
world_offset: [f64; 3],
|
world_offset: [f64; 3],
|
||||||
pslt_factor: f32,
|
pslt_factor: f32,
|
||||||
|
// World-space XY view AABB (post world_offset). `None` = no culling.
|
||||||
|
view_aabb: Option<[f32; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Style fingerprint used to group local wires into a single GPU buffer.
|
/// Style fingerprint used to group local wires into a single GPU buffer.
|
||||||
|
|
@ -439,7 +604,22 @@ fn expand_defn(
|
||||||
}
|
}
|
||||||
for sub in &defn.subs {
|
for sub in &defn.subs {
|
||||||
match sub {
|
match sub {
|
||||||
LocalSub::Wire(lw) => emit_wire(lw, accum_xform, ctx, out),
|
LocalSub::Wire(lw) => {
|
||||||
|
if let Some(view) = ctx.view_aabb {
|
||||||
|
let world = transform_aabb_xy(lw.aabb_local, accum_xform);
|
||||||
|
let [ox, oy, _] = ctx.world_offset;
|
||||||
|
let local = [
|
||||||
|
world[0] - ox as f32,
|
||||||
|
world[1] - oy as f32,
|
||||||
|
world[2] - ox as f32,
|
||||||
|
world[3] - oy as f32,
|
||||||
|
];
|
||||||
|
if aabb_disjoint_xy(local, view) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit_wire(lw, accum_xform, ctx, out);
|
||||||
|
}
|
||||||
LocalSub::Nested(nref) => {
|
LocalSub::Nested(nref) => {
|
||||||
if visited.iter().any(|n| n == &nref.block_name) {
|
if visited.iter().any(|n| n == &nref.block_name) {
|
||||||
// Cycle — skip.
|
// Cycle — skip.
|
||||||
|
|
@ -448,6 +628,22 @@ fn expand_defn(
|
||||||
let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else {
|
let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
// Nested-INSERT cull: union AABB of the nested defn, transformed
|
||||||
|
// by composed xform, vs view rect.
|
||||||
|
if let Some(view) = ctx.view_aabb {
|
||||||
|
let composed = nref.xform.then(accum_xform);
|
||||||
|
let world = transform_aabb_xy(nested_defn.aabb_local, &composed);
|
||||||
|
let [ox, oy, _] = ctx.world_offset;
|
||||||
|
let local = [
|
||||||
|
world[0] - ox as f32,
|
||||||
|
world[1] - oy as f32,
|
||||||
|
world[2] - ox as f32,
|
||||||
|
world[3] - oy as f32,
|
||||||
|
];
|
||||||
|
if aabb_disjoint_xy(local, view) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Resolve ByBlock for this nested ref against the outer ctx.
|
// Resolve ByBlock for this nested ref against the outer ctx.
|
||||||
let nested_color = if nref.color_is_byblock {
|
let nested_color = if nref.color_is_byblock {
|
||||||
ctx.ins_color
|
ctx.ins_color
|
||||||
|
|
@ -473,6 +669,7 @@ fn expand_defn(
|
||||||
selected: ctx.selected,
|
selected: ctx.selected,
|
||||||
world_offset: ctx.world_offset,
|
world_offset: ctx.world_offset,
|
||||||
pslt_factor: ctx.pslt_factor,
|
pslt_factor: ctx.pslt_factor,
|
||||||
|
view_aabb: ctx.view_aabb,
|
||||||
};
|
};
|
||||||
visited.push(nref.block_name.clone());
|
visited.push(nref.block_name.clone());
|
||||||
for offset in &nref.instance_offsets {
|
for offset in &nref.instance_offsets {
|
||||||
|
|
|
||||||
100
src/scene/mod.rs
100
src/scene/mod.rs
|
|
@ -220,9 +220,10 @@ pub struct Scene {
|
||||||
/// skip re-uploading unchanged geometry buffers every frame.
|
/// skip re-uploading unchanged geometry buffers every frame.
|
||||||
pub geometry_epoch: u64,
|
pub geometry_epoch: u64,
|
||||||
/// Cached tessellation of all visible entity wires for the current layout.
|
/// Cached tessellation of all visible entity wires for the current layout.
|
||||||
/// Keyed by `geometry_epoch`; invalidated automatically when the epoch changes.
|
/// Keyed by `(geometry_epoch, camera_generation)` so a camera change
|
||||||
|
/// invalidates the cull-dependent wire list as well as a geometry change.
|
||||||
/// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation.
|
/// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation.
|
||||||
wire_cache: RefCell<Option<(u64, Arc<Vec<WireModel>>)>>,
|
wire_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>,
|
||||||
/// Index built from every SortEntitiesTable in the document.
|
/// Index built from every SortEntitiesTable in the document.
|
||||||
/// Maps block_handle → (entity_handle.value() → sort_handle.value()).
|
/// Maps block_handle → (entity_handle.value() → sort_handle.value()).
|
||||||
/// Replaces the O(objects) linear scan inside `wires_for_block()` with an O(1) lookup.
|
/// Replaces the O(objects) linear scan inside `wires_for_block()` with an O(1) lookup.
|
||||||
|
|
@ -240,7 +241,9 @@ pub struct Scene {
|
||||||
viewport_wire_cache: RefCell<HashMap<Handle, (u64, Arc<Vec<WireModel>>)>>,
|
viewport_wire_cache: RefCell<HashMap<Handle, (u64, Arc<Vec<WireModel>>)>>,
|
||||||
/// Cached tessellation of paper-space layout block entities (title block, annotations, etc.).
|
/// Cached tessellation of paper-space layout block entities (title block, annotations, etc.).
|
||||||
/// Separate from `wire_cache` so paper_canvas_wires() doesn't re-tessellate on every frame.
|
/// Separate from `wire_cache` so paper_canvas_wires() doesn't re-tessellate on every frame.
|
||||||
paper_sheet_cache: RefCell<Option<(u64, Arc<Vec<WireModel>>)>>,
|
/// Keyed by `(geometry_epoch, camera_generation)` — paper view changes
|
||||||
|
/// on zoom too, so culled wire output depends on camera.
|
||||||
|
paper_sheet_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>,
|
||||||
/// Per-viewport projected wire cache for the paper canvas (2-D Iced widget).
|
/// Per-viewport projected wire cache for the paper canvas (2-D Iced widget).
|
||||||
/// Stores projected + clipped wires in paper-space coordinates.
|
/// Stores projected + clipped wires in paper-space coordinates.
|
||||||
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
||||||
|
|
@ -287,6 +290,9 @@ pub struct Scene {
|
||||||
/// Lets Insert tessellation transform-copy cached wires instead of
|
/// Lets Insert tessellation transform-copy cached wires instead of
|
||||||
/// clone+explode+re-tessellate per reference.
|
/// clone+explode+re-tessellate per reference.
|
||||||
block_defn_cache: RefCell<Option<(u64, Arc<block_cache::BlockCache>)>>,
|
block_defn_cache: RefCell<Option<(u64, Arc<block_cache::BlockCache>)>>,
|
||||||
|
/// Last viewport aspect ratio captured by the render pipeline. Used by
|
||||||
|
/// `view_world_aabb` to compute the world-space view rect on demand.
|
||||||
|
last_render_aspect: std::cell::Cell<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
|
|
@ -323,6 +329,43 @@ impl Scene {
|
||||||
model_extents_cache: RefCell::new(None),
|
model_extents_cache: RefCell::new(None),
|
||||||
entity_block_map_cache: RefCell::new(None),
|
entity_block_map_cache: RefCell::new(None),
|
||||||
block_defn_cache: RefCell::new(None),
|
block_defn_cache: RefCell::new(None),
|
||||||
|
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the current camera's world-space XY view AABB with
|
||||||
|
/// `world_offset` already subtracted (so the result is in the same f32
|
||||||
|
/// space as emitted wire points). Adds a 25% margin around the
|
||||||
|
/// frustum to absorb pan inertia and avoid clipped-edge popping.
|
||||||
|
pub(super) fn view_world_aabb(&self) -> Option<[f32; 4]> {
|
||||||
|
if self.current_layout != "Model" {
|
||||||
|
// Paper-space viewport composition handles its own culling; the
|
||||||
|
// top-level paper canvas is small enough not to need it.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let cam = self.camera.borrow();
|
||||||
|
let aspect = self.last_render_aspect.get().max(0.01);
|
||||||
|
let h = cam.ortho_size();
|
||||||
|
let w = h * aspect;
|
||||||
|
// Generous margin: in 3D / perspective the projection actually covers
|
||||||
|
// more than the orthographic-equivalent rect, and pan inertia briefly
|
||||||
|
// moves geometry past the visible edge before camera_generation bumps.
|
||||||
|
let margin = 1.25_f32;
|
||||||
|
let cx = (cam.target.x - self.world_offset[0] as f32) as f32;
|
||||||
|
let cy = (cam.target.y - self.world_offset[1] as f32) as f32;
|
||||||
|
Some([
|
||||||
|
cx - w * margin,
|
||||||
|
cy - h * margin,
|
||||||
|
cx + w * margin,
|
||||||
|
cy + h * margin,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by the render pipeline once per frame so `view_world_aabb` knows
|
||||||
|
/// the active widget's aspect ratio.
|
||||||
|
pub fn set_render_aspect(&self, aspect: f32) {
|
||||||
|
if aspect.is_finite() && aspect > 0.0 {
|
||||||
|
self.last_render_aspect.set(aspect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -752,17 +795,18 @@ impl Scene {
|
||||||
/// Shared by both `entity_wires_arc()` and `paper_canvas_wires()` so a single
|
/// Shared by both `entity_wires_arc()` and `paper_canvas_wires()` so a single
|
||||||
/// cache miss triggers only one tessellation pass, not two.
|
/// cache miss triggers only one tessellation pass, not two.
|
||||||
fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
||||||
|
let key = (self.geometry_epoch, self.camera_generation);
|
||||||
{
|
{
|
||||||
let cache = self.paper_sheet_cache.borrow();
|
let cache = self.paper_sheet_cache.borrow();
|
||||||
if let Some((cached_epoch, ref arc)) = *cache {
|
if let Some((cached_key, ref arc)) = *cache {
|
||||||
if cached_epoch == self.geometry_epoch {
|
if cached_key == key {
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let layout_block = self.current_layout_block_handle();
|
let layout_block = self.current_layout_block_handle();
|
||||||
let arc = Arc::new(self.wires_for_block(layout_block));
|
let arc = Arc::new(self.wires_for_block(layout_block));
|
||||||
*self.paper_sheet_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
*self.paper_sheet_cache.borrow_mut() = Some((key, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -770,10 +814,11 @@ impl Scene {
|
||||||
/// Returns a shared `Arc` so `build_primitive()` can skip the clone during
|
/// Returns a shared `Arc` so `build_primitive()` can skip the clone during
|
||||||
/// navigation frames where no preview wires are active.
|
/// navigation frames where no preview wires are active.
|
||||||
pub(super) fn entity_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
pub(super) fn entity_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
||||||
|
let key = (self.geometry_epoch, self.camera_generation);
|
||||||
{
|
{
|
||||||
let cache = self.wire_cache.borrow();
|
let cache = self.wire_cache.borrow();
|
||||||
if let Some((cached_epoch, ref arc)) = *cache {
|
if let Some((cached_key, ref arc)) = *cache {
|
||||||
if cached_epoch == self.geometry_epoch {
|
if cached_key == key {
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -783,14 +828,14 @@ impl Scene {
|
||||||
// no Vec clone needed.
|
// no Vec clone needed.
|
||||||
if self.current_layout == "Model" {
|
if self.current_layout == "Model" {
|
||||||
let arc = self.paper_sheet_wires_arc();
|
let arc = self.paper_sheet_wires_arc();
|
||||||
*self.wire_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
*self.wire_cache.borrow_mut() = Some((key, Arc::clone(&arc)));
|
||||||
return arc;
|
return arc;
|
||||||
}
|
}
|
||||||
// Paper space: extend sheet wires with projected viewport content.
|
// Paper space: extend sheet wires with projected viewport content.
|
||||||
let mut wires = (*self.paper_sheet_wires_arc()).clone();
|
let mut wires = (*self.paper_sheet_wires_arc()).clone();
|
||||||
wires.extend(self.viewport_content_wires(layout_block, None, None));
|
wires.extend(self.viewport_content_wires(layout_block, None, None));
|
||||||
let arc = Arc::new(wires);
|
let arc = Arc::new(wires);
|
||||||
*self.wire_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
*self.wire_cache.borrow_mut() = Some((key, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -953,9 +998,12 @@ impl Scene {
|
||||||
};
|
};
|
||||||
let blk_cache = self.block_cache_arc();
|
let blk_cache = self.block_cache_arc();
|
||||||
let blk_ref: &block_cache::BlockCache = &blk_cache;
|
let blk_ref: &block_cache::BlockCache = &blk_cache;
|
||||||
|
let view_aabb = self.view_world_aabb();
|
||||||
let mut wires: Vec<WireModel> = visible
|
let mut wires: Vec<WireModel> = visible
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.flat_map(|e| tessellate_entity(doc, sel, avp, woff, bg, anno, e, Some(blk_ref)))
|
.flat_map(|e| {
|
||||||
|
tessellate_entity(doc, sel, avp, woff, bg, anno, e, Some(blk_ref), view_aabb)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Apply draw order via the cached index (O(1) block lookup).
|
// Apply draw order via the cached index (O(1) block lookup).
|
||||||
|
|
@ -1052,6 +1100,8 @@ impl Scene {
|
||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
let blk_cache = self.block_cache_arc();
|
let blk_cache = self.block_cache_arc();
|
||||||
|
// tessellate_one is used for one-off lookups (hit test, properties).
|
||||||
|
// Skip culling here so the caller always gets the full geometry.
|
||||||
tessellate_entity(
|
tessellate_entity(
|
||||||
&self.document,
|
&self.document,
|
||||||
&self.selected,
|
&self.selected,
|
||||||
|
|
@ -1061,6 +1111,7 @@ impl Scene {
|
||||||
anno,
|
anno,
|
||||||
e,
|
e,
|
||||||
Some(&blk_cache),
|
Some(&blk_cache),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1116,7 +1167,7 @@ impl Scene {
|
||||||
// Prefer the already-computed wire AABB cache when available — avoids re-tessellating.
|
// Prefer the already-computed wire AABB cache when available — avoids re-tessellating.
|
||||||
if self.current_layout == "Model" {
|
if self.current_layout == "Model" {
|
||||||
let cache = self.wire_cache.borrow();
|
let cache = self.wire_cache.borrow();
|
||||||
if let Some((epoch, ref arc)) = *cache {
|
if let Some(((epoch, _cam_gen), ref arc)) = *cache {
|
||||||
if epoch == self.geometry_epoch {
|
if epoch == self.geometry_epoch {
|
||||||
for wire in arc.iter() {
|
for wire in arc.iter() {
|
||||||
let [ax, ay, bx, by] = wire.aabb;
|
let [ax, ay, bx, by] = wire.aabb;
|
||||||
|
|
@ -3564,6 +3615,10 @@ impl Scene {
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.flat_map(|e| {
|
.flat_map(|e| {
|
||||||
|
// Per-viewport tessellation uses the viewport's own camera —
|
||||||
|
// not the model-space camera — so we don't pass a view_aabb
|
||||||
|
// here. (Viewports are typically small enough that culling
|
||||||
|
// them isn't worth the added complexity.)
|
||||||
tessellate_entity(
|
tessellate_entity(
|
||||||
&self.document,
|
&self.document,
|
||||||
&self.selected,
|
&self.selected,
|
||||||
|
|
@ -3573,6 +3628,7 @@ impl Scene {
|
||||||
vp_anno_scale,
|
vp_anno_scale,
|
||||||
e,
|
e,
|
||||||
Some(&blk_cache),
|
Some(&blk_cache),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -3763,10 +3819,29 @@ fn tessellate_entity(
|
||||||
anno_scale: f32,
|
anno_scale: f32,
|
||||||
e: &EntityType,
|
e: &EntityType,
|
||||||
block_cache: Option<&block_cache::BlockCache>,
|
block_cache: Option<&block_cache::BlockCache>,
|
||||||
|
// World-space XY view AABB (post `world_offset` subtraction). When
|
||||||
|
// `Some`, entities whose AABB doesn't intersect this rect are skipped.
|
||||||
|
view_aabb: Option<[f32; 4]>,
|
||||||
) -> Vec<WireModel> {
|
) -> Vec<WireModel> {
|
||||||
let h = e.common().handle;
|
let h = e.common().handle;
|
||||||
let sel = selected.contains(&h);
|
let sel = selected.contains(&h);
|
||||||
|
|
||||||
|
// Frustum cull for non-Insert, non-Viewport entities. Insert is handled
|
||||||
|
// separately (its WCS bbox depends on the block defn AABB × Insert
|
||||||
|
// transform — done inside expand_insert). Viewports always emit so the
|
||||||
|
// viewport frame stays visible regardless of zoom.
|
||||||
|
if let Some(view) = view_aabb {
|
||||||
|
match e {
|
||||||
|
EntityType::Viewport(_) | EntityType::Insert(_) => {}
|
||||||
|
_ => {
|
||||||
|
let ab = entity_aabb(e, world_offset);
|
||||||
|
if ab != WireModel::UNBOUNDED_AABB && block_cache::aabb_disjoint_xy(ab, view) {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let EntityType::Viewport(vp) = e {
|
if let EntityType::Viewport(vp) = e {
|
||||||
// The sheet viewport (overall/id=1) is never shown — it represents the
|
// The sheet viewport (overall/id=1) is never shown — it represents the
|
||||||
// paper boundary, not a user-defined content window.
|
// paper boundary, not a user-defined content window.
|
||||||
|
|
@ -3897,6 +3972,7 @@ fn tessellate_entity(
|
||||||
sel,
|
sel,
|
||||||
world_offset,
|
world_offset,
|
||||||
pslt_factor,
|
pslt_factor,
|
||||||
|
view_aabb,
|
||||||
) {
|
) {
|
||||||
wires.push(marker);
|
wires.push(marker);
|
||||||
return wires;
|
return wires;
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,10 @@ pub struct Pipeline {
|
||||||
gpu_face3d_fill: Option<Face3DGpu>,
|
gpu_face3d_fill: Option<Face3DGpu>,
|
||||||
gpu_face3d_edges: Vec<WireGpu>,
|
gpu_face3d_edges: Vec<WireGpu>,
|
||||||
pub viewcube: ViewCubePipeline,
|
pub viewcube: ViewCubePipeline,
|
||||||
/// Last geometry epoch for which GPU buffers were uploaded.
|
/// Last `(geometry_epoch, camera_generation)` value for which GPU buffers
|
||||||
/// Initialized to u64::MAX so the first frame always uploads.
|
/// were uploaded. We re-upload when either side changes — pan/zoom bumps
|
||||||
pub cached_epoch: u64,
|
/// camera_generation, which triggers re-culling and a fresh upload.
|
||||||
|
pub cached_epoch: (u64, u64),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pipeline {
|
impl Pipeline {
|
||||||
|
|
@ -595,7 +596,7 @@ impl Pipeline {
|
||||||
gpu_face3d_fill: None,
|
gpu_face3d_fill: None,
|
||||||
gpu_face3d_edges: vec![],
|
gpu_face3d_edges: vec![],
|
||||||
viewcube,
|
viewcube,
|
||||||
cached_epoch: u64::MAX,
|
cached_epoch: (u64::MAX, u64::MAX),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,10 @@ pub struct Primitive {
|
||||||
pub(super) bg_color: [f32; 4],
|
pub(super) bg_color: [f32; 4],
|
||||||
pub(super) show_viewcube: bool,
|
pub(super) show_viewcube: bool,
|
||||||
pub(super) geometry_epoch: u64,
|
pub(super) geometry_epoch: u64,
|
||||||
|
/// Camera generation captured when this Primitive was assembled. Paired
|
||||||
|
/// with `geometry_epoch` so the wire buffers re-upload when the view
|
||||||
|
/// changes (frustum culling produces a different wire list).
|
||||||
|
pub(super) camera_generation: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── shader::Primitive impl ────────────────────────────────────────────────
|
// ── shader::Primitive impl ────────────────────────────────────────────────
|
||||||
|
|
@ -123,14 +127,21 @@ impl shader::Primitive for Primitive {
|
||||||
pipeline.ensure_depth_texture(device, clip_size);
|
pipeline.ensure_depth_texture(device, clip_size);
|
||||||
pipeline.viewcube.ensure_depth_texture(device, full_size);
|
pipeline.viewcube.ensure_depth_texture(device, full_size);
|
||||||
pipeline.upload_uniforms(queue, &self.uniforms);
|
pipeline.upload_uniforms(queue, &self.uniforms);
|
||||||
if self.geometry_epoch != pipeline.cached_epoch {
|
let cur_key = (self.geometry_epoch, self.camera_generation);
|
||||||
pipeline.upload_hatches(device, &self.hatches[..]);
|
if cur_key != pipeline.cached_epoch {
|
||||||
pipeline.upload_wipeouts(device, &self.wipeout_hatches[..]);
|
// Static buffers (hatches/images/meshes) only need refresh on a
|
||||||
pipeline.upload_images(device, queue, &self.images[..]);
|
// real geometry change, not on every camera tick.
|
||||||
pipeline.upload_meshes(device, &self.meshes[..]);
|
if self.geometry_epoch != pipeline.cached_epoch.0 {
|
||||||
|
pipeline.upload_hatches(device, &self.hatches[..]);
|
||||||
|
pipeline.upload_wipeouts(device, &self.wipeout_hatches[..]);
|
||||||
|
pipeline.upload_images(device, queue, &self.images[..]);
|
||||||
|
pipeline.upload_meshes(device, &self.meshes[..]);
|
||||||
|
}
|
||||||
|
// Wires re-upload on every camera change because the visible
|
||||||
|
// subset shifts under frustum culling.
|
||||||
pipeline.upload_wires(device, &self.wires[..]);
|
pipeline.upload_wires(device, &self.wires[..]);
|
||||||
pipeline.upload_face3d(device, &self.face3d_wires[..], &self.wires[..]);
|
pipeline.upload_face3d(device, &self.face3d_wires[..], &self.wires[..]);
|
||||||
pipeline.cached_epoch = self.geometry_epoch;
|
pipeline.cached_epoch = cur_key;
|
||||||
}
|
}
|
||||||
pipeline.compute_wire_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_wire_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
if self.show_viewcube {
|
if self.show_viewcube {
|
||||||
|
|
@ -306,6 +317,11 @@ impl Scene {
|
||||||
) -> Primitive {
|
) -> Primitive {
|
||||||
let cam = self.camera.borrow();
|
let cam = self.camera.borrow();
|
||||||
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
|
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
|
||||||
|
// Record the active widget's aspect so view_world_aabb() can compute
|
||||||
|
// a correct culling rectangle before entity_wires_arc() runs.
|
||||||
|
if bounds.height > 0.0 {
|
||||||
|
self.set_render_aspect(bounds.width / bounds.height);
|
||||||
|
}
|
||||||
|
|
||||||
let entity_arc = self.entity_wires_arc();
|
let entity_arc = self.entity_wires_arc();
|
||||||
let (face3d_wires, other_wires) = split_face3d_wires(&entity_arc, &self.document);
|
let (face3d_wires, other_wires) = split_face3d_wires(&entity_arc, &self.document);
|
||||||
|
|
@ -339,6 +355,7 @@ impl Scene {
|
||||||
bg_color,
|
bg_color,
|
||||||
show_viewcube,
|
show_viewcube,
|
||||||
geometry_epoch: self.geometry_epoch,
|
geometry_epoch: self.geometry_epoch,
|
||||||
|
camera_generation: self.camera_generation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -381,6 +398,7 @@ impl Scene {
|
||||||
bg_color: self.bg_color,
|
bg_color: self.bg_color,
|
||||||
show_viewcube: false,
|
show_viewcube: false,
|
||||||
geometry_epoch: self.geometry_epoch,
|
geometry_epoch: self.geometry_epoch,
|
||||||
|
camera_generation: self.camera_generation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue