perf: speed large drawing workflows
This commit is contained in:
parent
97be4fefaa
commit
39449ec684
10 changed files with 700 additions and 195 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -74,7 +74,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/OpenAEC-Foundation/acadifc?rev=4c1e5d51ff2c451a031fb5bedd8a7d4ef43e0f56#4c1e5d51ff2c451a031fb5bedd8a7d4ef43e0f56"
|
source = "git+https://github.com/OpenAEC-Foundation/acadifc?rev=545e6a6c0194ac18f22e5c26e428418d40408b0a#545e6a6c0194ac18f22e5c26e428418d40408b0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
@ -131,6 +131,7 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"serde",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ lyon_tessellation = "1.0.20"
|
||||||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
|
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Track the verified parallel DWG builder and table-control round-trip fix.
|
# Track the verified DWG read/write performance pass.
|
||||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc", rev = "4c1e5d51ff2c451a031fb5bedd8a7d4ef43e0f56" }
|
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc", rev = "545e6a6c0194ac18f22e5c26e428418d40408b0a" }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
# Native enables the plugin host runtime (out-of-process plugins).
|
# Native enables the plugin host runtime (out-of-process plugins).
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,9 @@ pub(super) struct DocumentTab {
|
||||||
pub(super) active_mleader_style: String,
|
pub(super) active_mleader_style: String,
|
||||||
/// Last camera_generation value written back to the document.
|
/// Last camera_generation value written back to the document.
|
||||||
pub(super) last_synced_camera_gen: u64,
|
pub(super) last_synced_camera_gen: u64,
|
||||||
|
/// Render-state key of `scene.document.preview`. Matching saves reuse the
|
||||||
|
/// encoded DWG thumbnail instead of rescanning every resident wire.
|
||||||
|
pub(super) thumbnail_cache_key: Option<super::ThumbnailCacheKey>,
|
||||||
/// Sentinel "Welcome / Start" tab. Always at index 0 when present.
|
/// Sentinel "Welcome / Start" tab. Always at index 0 when present.
|
||||||
/// Cannot be closed; the viewport area renders a welcome page instead
|
/// Cannot be closed; the viewport area renders a welcome page instead
|
||||||
/// of the model-space shader. The scene is still constructed so the
|
/// of the model-space shader. The scene is still constructed so the
|
||||||
|
|
@ -460,6 +463,7 @@ impl DocumentTab {
|
||||||
block_edit: None,
|
block_edit: None,
|
||||||
active_mleader_style: "Standard".to_string(),
|
active_mleader_style: "Standard".to_string(),
|
||||||
last_synced_camera_gen: 0,
|
last_synced_camera_gen: 0,
|
||||||
|
thumbnail_cache_key: None,
|
||||||
is_start: false,
|
is_start: false,
|
||||||
pan_mode: false,
|
pan_mode: false,
|
||||||
plugin_state: HashMap::new(),
|
plugin_state: HashMap::new(),
|
||||||
|
|
|
||||||
|
|
@ -865,6 +865,15 @@ pub(super) enum SaveContinuation {
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(super) struct ThumbnailCacheKey {
|
||||||
|
epoch: u64,
|
||||||
|
camera_generation: u64,
|
||||||
|
bg_color: [u32; 4],
|
||||||
|
png: bool,
|
||||||
|
viewport: [u32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SaveOutcome {
|
pub struct SaveOutcome {
|
||||||
job_id: u64,
|
job_id: u64,
|
||||||
|
|
@ -877,6 +886,8 @@ pub struct SaveOutcome {
|
||||||
set_current_path: bool,
|
set_current_path: bool,
|
||||||
purpose: SavePurpose,
|
purpose: SavePurpose,
|
||||||
continuation: SaveContinuation,
|
continuation: SaveContinuation,
|
||||||
|
thumbnail_key: Option<ThumbnailCacheKey>,
|
||||||
|
refreshed_preview: Option<Option<acadrust::Preview>>,
|
||||||
result: Result<(), String>,
|
result: Result<(), String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -750,24 +750,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rasterize the current drawing into the document's DWG preview image, so
|
|
||||||
/// a saved file shows a thumbnail in file browsers and other CAD apps.
|
|
||||||
/// A degenerate/empty drawing clears any stale preview.
|
|
||||||
/// `target` is the version the file is about to be written as — a PNG
|
|
||||||
/// preview (few KB vs a ~180 KB DIB) is only valid from R2013 (AC1027) on.
|
|
||||||
pub(super) fn stamp_thumbnail(&mut self, i: usize, target: acadrust::DxfVersion) {
|
|
||||||
let png = target >= acadrust::DxfVersion::AC1027;
|
|
||||||
// Frame the thumbnail exactly as the model pane shows it now (current
|
|
||||||
// pan/zoom/rotation, visible region only), so pass the pane pixel size.
|
|
||||||
self.tabs[i].scene.document.preview =
|
|
||||||
crate::io::thumbnail::from_scene(&self.tabs[i].scene, png, self.vp_size);
|
|
||||||
// The on-disk thumbnail will change — drop the cached Start-page handle
|
|
||||||
// so it re-reads the updated file on the next refresh.
|
|
||||||
if let Some(p) = self.tabs[i].current_path.clone() {
|
|
||||||
self.recent_thumbs.remove(&p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
||||||
self.sync_vport_display(i);
|
self.sync_vport_display(i);
|
||||||
|
|
@ -800,17 +782,29 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
let epoch = self.tabs[i].scene.geometry_epoch;
|
let epoch = self.tabs[i].scene.geometry_epoch;
|
||||||
let revision = self.tabs[i].edit_revision;
|
let revision = self.tabs[i].edit_revision;
|
||||||
let camera_generation = self.tabs[i].scene.camera_generation;
|
let camera_generation = self.tabs[i].scene.camera_generation;
|
||||||
let thumbnail = if purpose == crate::app::SavePurpose::Autosave {
|
let thumbnail_key = (purpose != crate::app::SavePurpose::Autosave).then(|| {
|
||||||
|
let scene = &self.tabs[i].scene;
|
||||||
|
crate::app::ThumbnailCacheKey {
|
||||||
|
epoch,
|
||||||
|
camera_generation,
|
||||||
|
bg_color: scene.bg_color.map(f32::to_bits),
|
||||||
|
png: version >= acadrust::DxfVersion::AC1027,
|
||||||
|
viewport: [self.vp_size.0.to_bits(), self.vp_size.1.to_bits()],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let thumbnail = if thumbnail_key == self.tabs[i].thumbnail_cache_key {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let scene = &self.tabs[i].scene;
|
thumbnail_key.map(|key| {
|
||||||
Some((
|
let scene = &self.tabs[i].scene;
|
||||||
scene.entity_wires(),
|
(
|
||||||
scene.camera.borrow().clone(),
|
scene.entity_wires(),
|
||||||
scene.bg_color,
|
scene.camera.borrow().clone(),
|
||||||
version >= acadrust::DxfVersion::AC1027,
|
scene.bg_color,
|
||||||
self.vp_size,
|
key.png,
|
||||||
))
|
self.vp_size,
|
||||||
|
)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
let clone_started = std::time::Instant::now();
|
let clone_started = std::time::Instant::now();
|
||||||
let mut snapshot = self.tabs[i].scene.document.clone();
|
let mut snapshot = self.tabs[i].scene.document.clone();
|
||||||
|
|
@ -833,7 +827,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
|
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
let result = std::thread::spawn(move || {
|
let (result, refreshed_preview) = std::thread::spawn(move || {
|
||||||
|
let mut refreshed_preview = None;
|
||||||
if let Some((wires, camera, bg_color, png, viewport)) = thumbnail {
|
if let Some((wires, camera, bg_color, png, viewport)) = thumbnail {
|
||||||
let started = std::time::Instant::now();
|
let started = std::time::Instant::now();
|
||||||
snapshot.preview = crate::io::thumbnail::from_snapshot(
|
snapshot.preview = crate::io::thumbnail::from_snapshot(
|
||||||
|
|
@ -850,16 +845,18 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
wires.len(),
|
wires.len(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
refreshed_preview = Some(snapshot.preview.clone());
|
||||||
}
|
}
|
||||||
crate::io::save_owned_as_version_atomic(
|
let result = crate::io::save_owned_as_version_atomic(
|
||||||
snapshot,
|
snapshot,
|
||||||
&worker_path,
|
&worker_path,
|
||||||
version,
|
version,
|
||||||
backup,
|
backup,
|
||||||
)
|
);
|
||||||
|
(result, refreshed_preview)
|
||||||
})
|
})
|
||||||
.join()
|
.join()
|
||||||
.unwrap_or_else(|_| Err("save worker panicked".to_string()));
|
.unwrap_or_else(|_| (Err("save worker panicked".to_string()), None));
|
||||||
crate::app::SaveOutcome {
|
crate::app::SaveOutcome {
|
||||||
job_id,
|
job_id,
|
||||||
tab_id,
|
tab_id,
|
||||||
|
|
@ -871,6 +868,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
set_current_path,
|
set_current_path,
|
||||||
purpose,
|
purpose,
|
||||||
continuation,
|
continuation,
|
||||||
|
thumbnail_key,
|
||||||
|
refreshed_preview,
|
||||||
result,
|
result,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -921,6 +920,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
let snapshot_is_current = self.tabs[i].scene.geometry_epoch == outcome.epoch
|
let snapshot_is_current = self.tabs[i].scene.geometry_epoch == outcome.epoch
|
||||||
&& self.tabs[i].edit_revision == outcome.revision
|
&& self.tabs[i].edit_revision == outcome.revision
|
||||||
&& self.tabs[i].scene.camera_generation == outcome.camera_generation;
|
&& self.tabs[i].scene.camera_generation == outcome.camera_generation;
|
||||||
|
if snapshot_is_current && outcome.purpose != crate::app::SavePurpose::Autosave {
|
||||||
|
if let Some(preview) = outcome.refreshed_preview {
|
||||||
|
self.tabs[i].scene.document.preview = preview;
|
||||||
|
}
|
||||||
|
self.tabs[i].thumbnail_cache_key = outcome.thumbnail_key;
|
||||||
|
}
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
match outcome.purpose {
|
match outcome.purpose {
|
||||||
crate::app::SavePurpose::Autosave => {
|
crate::app::SavePurpose::Autosave => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! DWG preview thumbnails.
|
//! DWG preview thumbnails.
|
||||||
//!
|
//!
|
||||||
//! - [`from_scene`] rasterizes the drawing exactly as it is framed on screen —
|
//! - [`from_snapshot`] rasterizes the drawing exactly as it is framed on screen —
|
||||||
//! the live camera's pan / zoom / rotation, only the currently visible region,
|
//! the live camera's pan / zoom / rotation, only the currently visible region,
|
||||||
//! not the whole extent — into a small [`acadrust::Preview`] embedded on save
|
//! not the whole extent — into a small [`acadrust::Preview`] embedded on save
|
||||||
//! so OCS drawings show a thumbnail in file browsers and other CAD apps.
|
//! so OCS drawings show a thumbnail in file browsers and other CAD apps.
|
||||||
|
|
@ -14,7 +14,7 @@ use iced::Rectangle;
|
||||||
use image::{ImageFormat, Rgb, RgbImage};
|
use image::{ImageFormat, Rgb, RgbImage};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use crate::scene::{Scene, WireModel};
|
use crate::scene::WireModel;
|
||||||
use crate::scene::view::camera::Camera;
|
use crate::scene::view::camera::Camera;
|
||||||
|
|
||||||
/// Longest edge of the generated thumbnail, in pixels.
|
/// Longest edge of the generated thumbnail, in pixels.
|
||||||
|
|
@ -30,13 +30,8 @@ const MAX_DIM: u32 = 256;
|
||||||
/// one colour, so a **PNG** collapses to a few KB where the uncompressed
|
/// one colour, so a **PNG** collapses to a few KB where the uncompressed
|
||||||
/// **BMP/DIB** stays ~180 KB at 256². PNG previews are only valid from R2013
|
/// **BMP/DIB** stays ~180 KB at 256². PNG previews are only valid from R2013
|
||||||
/// (AC1027) on, so the caller passes `false` for older targets → BMP/DIB.
|
/// (AC1027) on, so the caller passes `false` for older targets → BMP/DIB.
|
||||||
pub fn from_scene(scene: &Scene, png: bool, viewport: (f32, f32)) -> Option<Preview> {
|
///
|
||||||
let wires = scene.entity_wires();
|
/// Native background saves retain
|
||||||
let camera = scene.camera.borrow();
|
|
||||||
from_snapshot(&wires, &camera, scene.bg_color, png, viewport)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a preview from immutable render inputs. Native background saves retain
|
|
||||||
/// the resident wire `Arc` and a small camera copy, then run the point scan and
|
/// the resident wire `Arc` and a small camera copy, then run the point scan and
|
||||||
/// image encoding on the save worker instead of blocking the UI thread.
|
/// image encoding on the save worker instead of blocking the UI thread.
|
||||||
pub fn from_snapshot(
|
pub fn from_snapshot(
|
||||||
|
|
@ -64,10 +59,17 @@ pub fn from_snapshot(
|
||||||
width: cw as f32,
|
width: cw as f32,
|
||||||
height: ch as f32,
|
height: ch as f32,
|
||||||
};
|
};
|
||||||
rasterize(wires, cw, ch, bg_color, png, |x, y, z| {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
camera.project(glam::DVec3::new(x, y, z), bounds)
|
{
|
||||||
.map(|s| (s.x.round() as i32, s.y.round() as i32))
|
rasterize_snapshot_parallel(wires, camera, bounds, cw, ch, bg_color, png)
|
||||||
})
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
rasterize(wires, cw, ch, bg_color, png, |x, y, z| {
|
||||||
|
camera.project(glam::DVec3::new(x, y, z), bounds)
|
||||||
|
.map(|s| (s.x.round() as i32, s.y.round() as i32))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Canvas dimensions for an aspect ratio, longest edge = [`MAX_DIM`].
|
/// Canvas dimensions for an aspect ratio, longest edge = [`MAX_DIM`].
|
||||||
|
|
@ -79,6 +81,145 @@ fn canvas_dims(aspect: f64) -> (u32, u32) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn rasterize_snapshot_parallel(
|
||||||
|
wires: &[WireModel],
|
||||||
|
camera: &Camera,
|
||||||
|
bounds: Rectangle,
|
||||||
|
cw: u32,
|
||||||
|
ch: u32,
|
||||||
|
bg: [f32; 4],
|
||||||
|
png: bool,
|
||||||
|
) -> Option<Preview> {
|
||||||
|
use crate::par::prelude::*;
|
||||||
|
|
||||||
|
let total_segments: usize = wires
|
||||||
|
.iter()
|
||||||
|
.map(|wire| wire.points.len().saturating_sub(1))
|
||||||
|
.sum();
|
||||||
|
if total_segments < 100_000 {
|
||||||
|
return rasterize(wires, cw, ch, bg, png, |x, y, z| {
|
||||||
|
camera.project(glam::DVec3::new(x, y, z), bounds)
|
||||||
|
.map(|point| (point.x.round() as i32, point.y.round() as i32))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let task_count = (rayon::current_num_threads().max(1) * 2).min(total_segments);
|
||||||
|
let target_work = total_segments.div_ceil(task_count).max(1);
|
||||||
|
let mut tasks: Vec<Vec<(usize, usize, usize)>> = Vec::with_capacity(task_count);
|
||||||
|
let mut task = Vec::new();
|
||||||
|
let mut task_work = 0usize;
|
||||||
|
for (wire_index, wire) in wires.iter().enumerate() {
|
||||||
|
let segment_count = wire.points.len().saturating_sub(1);
|
||||||
|
let mut start = 0usize;
|
||||||
|
while start < segment_count {
|
||||||
|
let take = (target_work - task_work).min(segment_count - start);
|
||||||
|
task.push((wire_index, start, start + take));
|
||||||
|
task_work += take;
|
||||||
|
start += take;
|
||||||
|
if task_work == target_work {
|
||||||
|
tasks.push(std::mem::take(&mut task));
|
||||||
|
task_work = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !task.is_empty() {
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
let layers: Vec<Vec<u32>> = tasks
|
||||||
|
.par_iter()
|
||||||
|
.map(|task| {
|
||||||
|
let mut pixels = vec![u32::MAX; (cw * ch) as usize];
|
||||||
|
for &(wire_index, start, end) in task {
|
||||||
|
let wire = &wires[wire_index];
|
||||||
|
let color = to_rgb(wire.color);
|
||||||
|
let packed =
|
||||||
|
color[0] as u32 | (color[1] as u32) << 8 | (color[2] as u32) << 16;
|
||||||
|
let mut previous = projected_wire_point(wire, start, camera, bounds);
|
||||||
|
for index in start + 1..=end {
|
||||||
|
let current = projected_wire_point(wire, index, camera, bounds);
|
||||||
|
if let (Some(a), Some(b)) = (previous, current) {
|
||||||
|
draw_line_layer(&mut pixels, cw, ch, a, b, packed);
|
||||||
|
}
|
||||||
|
previous = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixels
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut image = RgbImage::from_pixel(cw, ch, Rgb(to_rgb(bg)));
|
||||||
|
for layer in layers {
|
||||||
|
for (pixel, packed) in image.pixels_mut().zip(layer) {
|
||||||
|
if packed != u32::MAX {
|
||||||
|
*pixel = Rgb([
|
||||||
|
packed as u8,
|
||||||
|
(packed >> 8) as u8,
|
||||||
|
(packed >> 16) as u8,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encode(image, png)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn projected_wire_point(
|
||||||
|
wire: &WireModel,
|
||||||
|
index: usize,
|
||||||
|
camera: &Camera,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) -> Option<(i32, i32)> {
|
||||||
|
let point = wire.points.get(index)?;
|
||||||
|
if !point[0].is_finite() || !point[1].is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (x, y, z) = abs_xyz(wire, index, point);
|
||||||
|
camera
|
||||||
|
.project(glam::DVec3::new(x, y, z), bounds)
|
||||||
|
.map(|screen| (screen.x.round() as i32, screen.y.round() as i32))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn draw_line_layer(
|
||||||
|
pixels: &mut [u32],
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
(x0, y0): (i32, i32),
|
||||||
|
(x1, y1): (i32, i32),
|
||||||
|
color: u32,
|
||||||
|
) {
|
||||||
|
let (width, height) = (width as i32, height as i32);
|
||||||
|
let Some(((mut x0, mut y0), (x1, y1))) =
|
||||||
|
clip_line_to_rect((x0, y0), (x1, y1), width, height)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let dx = (x1 - x0).abs();
|
||||||
|
let dy = -(y1 - y0).abs();
|
||||||
|
let sx = if x0 < x1 { 1 } else { -1 };
|
||||||
|
let sy = if y0 < y1 { 1 } else { -1 };
|
||||||
|
let mut error = dx + dy;
|
||||||
|
loop {
|
||||||
|
if x0 >= 0 && x0 < width && y0 >= 0 && y0 < height {
|
||||||
|
pixels[y0 as usize * width as usize + x0 as usize] = color;
|
||||||
|
}
|
||||||
|
if x0 == x1 && y0 == y1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let twice = 2 * error;
|
||||||
|
if twice >= dy {
|
||||||
|
error += dy;
|
||||||
|
x0 += sx;
|
||||||
|
}
|
||||||
|
if twice <= dx {
|
||||||
|
error += dx;
|
||||||
|
y0 += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Rasterize `wires` onto a `bg`-filled `cw`×`ch` canvas, placing each vertex
|
/// Rasterize `wires` onto a `bg`-filled `cw`×`ch` canvas, placing each vertex
|
||||||
/// with `project` (world XYZ → canvas pixel, `None` = not projectable), and
|
/// with `project` (world XYZ → canvas pixel, `None` = not projectable), and
|
||||||
/// encode the result. A `None` from `project` breaks the polyline run, as does a
|
/// encode the result. A `None` from `project` breaks the polyline run, as does a
|
||||||
|
|
@ -349,7 +490,11 @@ mod tests {
|
||||||
/// Bresenham line, clipped to the image bounds.
|
/// Bresenham line, clipped to the image bounds.
|
||||||
fn draw_line(img: &mut RgbImage, (x0, y0): (i32, i32), (x1, y1): (i32, i32), col: Rgb<u8>) {
|
fn draw_line(img: &mut RgbImage, (x0, y0): (i32, i32), (x1, y1): (i32, i32), col: Rgb<u8>) {
|
||||||
let (w, h) = (img.width() as i32, img.height() as i32);
|
let (w, h) = (img.width() as i32, img.height() as i32);
|
||||||
let (mut x0, mut y0) = (x0, y0);
|
let Some(((mut x0, mut y0), (x1, y1))) =
|
||||||
|
clip_line_to_rect((x0, y0), (x1, y1), w, h)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let dx = (x1 - x0).abs();
|
let dx = (x1 - x0).abs();
|
||||||
let dy = -(y1 - y0).abs();
|
let dy = -(y1 - y0).abs();
|
||||||
let sx = if x0 < x1 { 1 } else { -1 };
|
let sx = if x0 < x1 { 1 } else { -1 };
|
||||||
|
|
@ -373,3 +518,47 @@ fn draw_line(img: &mut RgbImage, (x0, y0): (i32, i32), (x1, y1): (i32, i32), col
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clip_line_to_rect(
|
||||||
|
(x0, y0): (i32, i32),
|
||||||
|
(x1, y1): (i32, i32),
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Option<((i32, i32), (i32, i32))> {
|
||||||
|
if width <= 0 || height <= 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (x0, y0, x1, y1) = (x0 as f64, y0 as f64, x1 as f64, y1 as f64);
|
||||||
|
let (dx, dy) = (x1 - x0, y1 - y0);
|
||||||
|
let mut enter = 0.0f64;
|
||||||
|
let mut leave = 1.0f64;
|
||||||
|
for (p, q) in [
|
||||||
|
(-dx, x0),
|
||||||
|
(dx, width.saturating_sub(1) as f64 - x0),
|
||||||
|
(-dy, y0),
|
||||||
|
(dy, height.saturating_sub(1) as f64 - y0),
|
||||||
|
] {
|
||||||
|
if p == 0.0 {
|
||||||
|
if q < 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ratio = q / p;
|
||||||
|
if p < 0.0 {
|
||||||
|
enter = enter.max(ratio);
|
||||||
|
} else {
|
||||||
|
leave = leave.min(ratio);
|
||||||
|
}
|
||||||
|
if enter > leave {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let point = |t: f64| {
|
||||||
|
(
|
||||||
|
(x0 + t * dx).round().clamp(0.0, width.saturating_sub(1) as f64) as i32,
|
||||||
|
(y0 + t * dy).round().clamp(0.0, height.saturating_sub(1) as f64) as i32,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Some((point(enter), point(leave)))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ struct SpatialGrid {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpatialGrid {
|
impl SpatialGrid {
|
||||||
fn build<T>(entries: &[Entry3<T>]) -> Self {
|
fn build<T: Sync>(entries: &[Entry3<T>]) -> Self {
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return Self {
|
return Self {
|
||||||
min: [0.0; 2],
|
min: [0.0; 2],
|
||||||
|
|
@ -314,57 +314,137 @@ impl SpatialGrid {
|
||||||
let cols = (((ext_x / cell).ceil() as u64) + 1).clamp(1, MAX_AXIS_CELLS as u64) as u32;
|
let cols = (((ext_x / cell).ceil() as u64) + 1).clamp(1, MAX_AXIS_CELLS as u64) as u32;
|
||||||
let rows = (((ext_y / cell).ceil() as u64) + 1).clamp(1, MAX_AXIS_CELLS as u64) as u32;
|
let rows = (((ext_y / cell).ceil() as u64) + 1).clamp(1, MAX_AXIS_CELLS as u64) as u32;
|
||||||
let cell_count = cols as usize * rows as usize;
|
let cell_count = cols as usize * rows as usize;
|
||||||
let mut counts = vec![0u32; cell_count];
|
|
||||||
let mut oversized = Vec::new();
|
|
||||||
|
|
||||||
let col_of = |x: f64| (((x - min[0]) / cell).floor()).clamp(0.0, (cols - 1) as f64) as u32;
|
let col_of = |x: f64| (((x - min[0]) / cell).floor()).clamp(0.0, (cols - 1) as f64) as u32;
|
||||||
let row_of = |y: f64| (((y - min[1]) / cell).floor()).clamp(0.0, (rows - 1) as f64) as u32;
|
let row_of = |y: f64| (((y - min[1]) / cell).floor()).clamp(0.0, (rows - 1) as f64) as u32;
|
||||||
|
|
||||||
for (idx, entry) in entries.iter().enumerate() {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let aabb = entry_aabb2(entry);
|
let (counts, oversized) = {
|
||||||
let c0 = col_of(aabb[0]);
|
use crate::par::prelude::*;
|
||||||
let c1 = col_of(aabb[2]);
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
let r0 = row_of(aabb[1]);
|
|
||||||
let r1 = row_of(aabb[3]);
|
let counts: Vec<AtomicU32> =
|
||||||
let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64;
|
(0..cell_count).map(|_| AtomicU32::new(0)).collect();
|
||||||
if span > MAX_SPAN_CELLS {
|
let oversized: Vec<u32> = entries
|
||||||
oversized.push(idx as u32);
|
.par_iter()
|
||||||
continue;
|
.enumerate()
|
||||||
}
|
.filter_map(|(idx, entry)| {
|
||||||
for row in r0..=r1 {
|
let aabb = entry_aabb2(entry);
|
||||||
let base = row as usize * cols as usize;
|
let c0 = col_of(aabb[0]);
|
||||||
for col in c0..=c1 {
|
let c1 = col_of(aabb[2]);
|
||||||
counts[base + col as usize] += 1;
|
let r0 = row_of(aabb[1]);
|
||||||
|
let r1 = row_of(aabb[3]);
|
||||||
|
let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64;
|
||||||
|
if span > MAX_SPAN_CELLS {
|
||||||
|
return Some(idx as u32);
|
||||||
|
}
|
||||||
|
for row in r0..=r1 {
|
||||||
|
let base = row as usize * cols as usize;
|
||||||
|
for col in c0..=c1 {
|
||||||
|
counts[base + col as usize].fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(
|
||||||
|
counts
|
||||||
|
.into_iter()
|
||||||
|
.map(AtomicU32::into_inner)
|
||||||
|
.collect::<Vec<u32>>(),
|
||||||
|
oversized,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let (counts, oversized) = {
|
||||||
|
let mut counts = vec![0u32; cell_count];
|
||||||
|
let mut oversized = Vec::new();
|
||||||
|
for (idx, entry) in entries.iter().enumerate() {
|
||||||
|
let aabb = entry_aabb2(entry);
|
||||||
|
let c0 = col_of(aabb[0]);
|
||||||
|
let c1 = col_of(aabb[2]);
|
||||||
|
let r0 = row_of(aabb[1]);
|
||||||
|
let r1 = row_of(aabb[3]);
|
||||||
|
let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64;
|
||||||
|
if span > MAX_SPAN_CELLS {
|
||||||
|
oversized.push(idx as u32);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for row in r0..=r1 {
|
||||||
|
let base = row as usize * cols as usize;
|
||||||
|
for col in c0..=c1 {
|
||||||
|
counts[base + col as usize] += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
(counts, oversized)
|
||||||
|
};
|
||||||
let mut cell_offsets = Vec::with_capacity(cell_count + 1);
|
let mut cell_offsets = Vec::with_capacity(cell_count + 1);
|
||||||
cell_offsets.push(0);
|
cell_offsets.push(0);
|
||||||
for count in counts {
|
for count in counts {
|
||||||
cell_offsets.push(cell_offsets.last().copied().unwrap_or(0) + count);
|
cell_offsets.push(cell_offsets.last().copied().unwrap_or(0) + count);
|
||||||
}
|
}
|
||||||
let mut cell_entries = vec![0u32; *cell_offsets.last().unwrap_or(&0) as usize];
|
|
||||||
let mut cursors = cell_offsets[..cell_count].to_vec();
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
for (idx, entry) in entries.iter().enumerate() {
|
let cell_entries = {
|
||||||
let aabb = entry_aabb2(entry);
|
use crate::par::prelude::*;
|
||||||
let c0 = col_of(aabb[0]);
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
let c1 = col_of(aabb[2]);
|
|
||||||
let r0 = row_of(aabb[1]);
|
let cursors: Vec<AtomicU32> = cell_offsets[..cell_count]
|
||||||
let r1 = row_of(aabb[3]);
|
.iter()
|
||||||
let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64;
|
.copied()
|
||||||
if span > MAX_SPAN_CELLS {
|
.map(AtomicU32::new)
|
||||||
continue;
|
.collect();
|
||||||
}
|
let slots: Vec<AtomicU32> = (0..*cell_offsets.last().unwrap_or(&0) as usize)
|
||||||
for row in r0..=r1 {
|
.map(|_| AtomicU32::new(0))
|
||||||
let base = row as usize * cols as usize;
|
.collect();
|
||||||
for col in c0..=c1 {
|
entries.par_iter().enumerate().for_each(|(idx, entry)| {
|
||||||
let cell_idx = base + col as usize;
|
let aabb = entry_aabb2(entry);
|
||||||
let cursor = &mut cursors[cell_idx];
|
let c0 = col_of(aabb[0]);
|
||||||
cell_entries[*cursor as usize] = idx as u32;
|
let c1 = col_of(aabb[2]);
|
||||||
*cursor += 1;
|
let r0 = row_of(aabb[1]);
|
||||||
|
let r1 = row_of(aabb[3]);
|
||||||
|
let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64;
|
||||||
|
if span > MAX_SPAN_CELLS {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for row in r0..=r1 {
|
||||||
|
let base = row as usize * cols as usize;
|
||||||
|
for col in c0..=c1 {
|
||||||
|
let cell_idx = base + col as usize;
|
||||||
|
let cursor = cursors[cell_idx].fetch_add(1, Ordering::Relaxed);
|
||||||
|
slots[cursor as usize].store(idx as u32, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
slots.into_iter().map(AtomicU32::into_inner).collect()
|
||||||
|
};
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let cell_entries = {
|
||||||
|
let mut cell_entries = vec![0u32; *cell_offsets.last().unwrap_or(&0) as usize];
|
||||||
|
let mut cursors = cell_offsets[..cell_count].to_vec();
|
||||||
|
for (idx, entry) in entries.iter().enumerate() {
|
||||||
|
let aabb = entry_aabb2(entry);
|
||||||
|
let c0 = col_of(aabb[0]);
|
||||||
|
let c1 = col_of(aabb[2]);
|
||||||
|
let r0 = row_of(aabb[1]);
|
||||||
|
let r1 = row_of(aabb[3]);
|
||||||
|
let span = (c1 - c0 + 1) as u64 * (r1 - r0 + 1) as u64;
|
||||||
|
if span > MAX_SPAN_CELLS {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for row in r0..=r1 {
|
||||||
|
let base = row as usize * cols as usize;
|
||||||
|
for col in c0..=c1 {
|
||||||
|
let cell_idx = base + col as usize;
|
||||||
|
let cursor = &mut cursors[cell_idx];
|
||||||
|
cell_entries[*cursor as usize] = idx as u32;
|
||||||
|
*cursor += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
cell_entries
|
||||||
|
};
|
||||||
let oversized = SpatialBvh2::build(&entries, oversized);
|
let oversized = SpatialBvh2::build(&entries, oversized);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -426,7 +506,7 @@ struct SpatialSet<T> {
|
||||||
xyz: std::sync::OnceLock<SpatialBvh3>,
|
xyz: std::sync::OnceLock<SpatialBvh3>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Copy + Ord> SpatialSet<T> {
|
impl<T: Copy + Ord + Sync> SpatialSet<T> {
|
||||||
fn build(entries: Vec<Entry3<T>>) -> Self {
|
fn build(entries: Vec<Entry3<T>>) -> Self {
|
||||||
let xy = SpatialGrid::build(&entries);
|
let xy = SpatialGrid::build(&entries);
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -601,6 +681,19 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
|
||||||
entries
|
entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten_entry_parts<T>(mut parts: Vec<Vec<T>>) -> Vec<T> {
|
||||||
|
let Some((largest, _)) = parts.iter().enumerate().max_by_key(|(_, part)| part.len()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let mut output = parts.swap_remove(largest);
|
||||||
|
let remaining: usize = parts.iter().map(Vec::len).sum();
|
||||||
|
output.reserve(remaining);
|
||||||
|
for mut part in parts {
|
||||||
|
output.append(&mut part);
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
impl InteractionHandleIndex {
|
impl InteractionHandleIndex {
|
||||||
pub fn build(entries: impl IntoIterator<Item = (u64, [f64; 6])>) -> Self {
|
pub fn build(entries: impl IntoIterator<Item = (u64, [f64; 6])>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -649,6 +742,10 @@ impl InteractionIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(wires: &[WireModel]) -> Self {
|
pub fn build(wires: &[WireModel]) -> Self {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let build_started = std::time::Instant::now();
|
||||||
let wire_handles: Vec<Option<u64>> = wires
|
let wire_handles: Vec<Option<u64>> = wires
|
||||||
.iter()
|
.iter()
|
||||||
.map(|wire| wire.name.parse::<u64>().ok())
|
.map(|wire| wire.name.parse::<u64>().ok())
|
||||||
|
|
@ -665,14 +762,11 @@ impl InteractionIndex {
|
||||||
Some(current)
|
Some(current)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let handles_elapsed = build_started.elapsed();
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let collect_started = std::time::Instant::now();
|
||||||
let mut wire_entries = Vec::with_capacity(wires.len());
|
let mut wire_entries = Vec::with_capacity(wires.len());
|
||||||
let mut segment_entries = Vec::new();
|
|
||||||
let mut snap_point_entries = Vec::new();
|
|
||||||
let mut key_vertex_entries = Vec::new();
|
|
||||||
let mut key_segment_entries = Vec::new();
|
|
||||||
let mut fill_triangle_entries = Vec::new();
|
|
||||||
let mut pick_triangle_entries = Vec::new();
|
|
||||||
let mut glyph_entries = Vec::new();
|
|
||||||
let mut unbounded_wires = Vec::new();
|
let mut unbounded_wires = Vec::new();
|
||||||
let mut max_line_half_width_px = 0.0f32;
|
let mut max_line_half_width_px = 0.0f32;
|
||||||
|
|
||||||
|
|
@ -691,46 +785,18 @@ impl InteractionIndex {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, wire)| collect_wire_index_entries(index as u32, wire))
|
.map(|(index, wire)| collect_wire_index_entries(index as u32, wire))
|
||||||
.collect();
|
.collect();
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
// Every per-wire worker knows its exact output sizes. Reserve the flat
|
let collect_elapsed = collect_started.elapsed();
|
||||||
// arrays once before draining them so a dense block drawing does not
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
// repeatedly copy already-flattened entries while the Vecs grow.
|
let flatten_started = std::time::Instant::now();
|
||||||
wire_entries.reserve(per_wire.iter().filter(|entries| entries.wire.is_some()).count());
|
let mut segment_parts = Vec::with_capacity(per_wire.len());
|
||||||
unbounded_wires.reserve(per_wire.iter().filter(|entries| entries.unbounded).count());
|
let mut snap_point_parts = Vec::with_capacity(per_wire.len());
|
||||||
segment_entries.reserve(per_wire.iter().map(|entries| entries.segments.len()).sum());
|
let mut key_vertex_parts = Vec::with_capacity(per_wire.len());
|
||||||
snap_point_entries.reserve(
|
let mut key_segment_parts = Vec::with_capacity(per_wire.len());
|
||||||
per_wire
|
let mut fill_triangle_parts = Vec::with_capacity(per_wire.len());
|
||||||
.iter()
|
let mut pick_triangle_parts = Vec::with_capacity(per_wire.len());
|
||||||
.map(|entries| entries.snap_points.len())
|
let mut glyph_parts = Vec::with_capacity(per_wire.len());
|
||||||
.sum(),
|
for (wire_idx, entries) in per_wire.into_iter().enumerate() {
|
||||||
);
|
|
||||||
key_vertex_entries.reserve(
|
|
||||||
per_wire
|
|
||||||
.iter()
|
|
||||||
.map(|entries| entries.key_vertices.len())
|
|
||||||
.sum(),
|
|
||||||
);
|
|
||||||
key_segment_entries.reserve(
|
|
||||||
per_wire
|
|
||||||
.iter()
|
|
||||||
.map(|entries| entries.key_segments.len())
|
|
||||||
.sum(),
|
|
||||||
);
|
|
||||||
fill_triangle_entries.reserve(
|
|
||||||
per_wire
|
|
||||||
.iter()
|
|
||||||
.map(|entries| entries.fill_triangles.len())
|
|
||||||
.sum(),
|
|
||||||
);
|
|
||||||
pick_triangle_entries.reserve(
|
|
||||||
per_wire
|
|
||||||
.iter()
|
|
||||||
.map(|entries| entries.pick_triangles.len())
|
|
||||||
.sum(),
|
|
||||||
);
|
|
||||||
glyph_entries.reserve(per_wire.iter().map(|entries| entries.glyphs.len()).sum());
|
|
||||||
|
|
||||||
for (wire_idx, mut entries) in per_wire.into_iter().enumerate() {
|
|
||||||
max_line_half_width_px =
|
max_line_half_width_px =
|
||||||
max_line_half_width_px.max(entries.max_line_half_width_px);
|
max_line_half_width_px.max(entries.max_line_half_width_px);
|
||||||
if let Some(entry) = entries.wire {
|
if let Some(entry) = entries.wire {
|
||||||
|
|
@ -739,14 +805,65 @@ impl InteractionIndex {
|
||||||
if entries.unbounded {
|
if entries.unbounded {
|
||||||
unbounded_wires.push(wire_idx as u32);
|
unbounded_wires.push(wire_idx as u32);
|
||||||
}
|
}
|
||||||
segment_entries.append(&mut entries.segments);
|
segment_parts.push(entries.segments);
|
||||||
snap_point_entries.append(&mut entries.snap_points);
|
snap_point_parts.push(entries.snap_points);
|
||||||
key_vertex_entries.append(&mut entries.key_vertices);
|
key_vertex_parts.push(entries.key_vertices);
|
||||||
key_segment_entries.append(&mut entries.key_segments);
|
key_segment_parts.push(entries.key_segments);
|
||||||
fill_triangle_entries.append(&mut entries.fill_triangles);
|
fill_triangle_parts.push(entries.fill_triangles);
|
||||||
pick_triangle_entries.append(&mut entries.pick_triangles);
|
pick_triangle_parts.push(entries.pick_triangles);
|
||||||
glyph_entries.append(&mut entries.glyphs);
|
glyph_parts.push(entries.glyphs);
|
||||||
}
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let (
|
||||||
|
((segment_entries, snap_point_entries), (key_vertex_entries, key_segment_entries)),
|
||||||
|
((fill_triangle_entries, pick_triangle_entries), glyph_entries),
|
||||||
|
) = rayon::join(
|
||||||
|
|| {
|
||||||
|
rayon::join(
|
||||||
|
|| {
|
||||||
|
rayon::join(
|
||||||
|
|| flatten_entry_parts(segment_parts),
|
||||||
|
|| flatten_entry_parts(snap_point_parts),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|| {
|
||||||
|
rayon::join(
|
||||||
|
|| flatten_entry_parts(key_vertex_parts),
|
||||||
|
|| flatten_entry_parts(key_segment_parts),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|| {
|
||||||
|
let (fill, pick) = rayon::join(
|
||||||
|
|| flatten_entry_parts(fill_triangle_parts),
|
||||||
|
|| flatten_entry_parts(pick_triangle_parts),
|
||||||
|
);
|
||||||
|
((fill, pick), flatten_entry_parts(glyph_parts))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let (
|
||||||
|
segment_entries,
|
||||||
|
snap_point_entries,
|
||||||
|
key_vertex_entries,
|
||||||
|
key_segment_entries,
|
||||||
|
fill_triangle_entries,
|
||||||
|
pick_triangle_entries,
|
||||||
|
glyph_entries,
|
||||||
|
) = (
|
||||||
|
flatten_entry_parts(segment_parts),
|
||||||
|
flatten_entry_parts(snap_point_parts),
|
||||||
|
flatten_entry_parts(key_vertex_parts),
|
||||||
|
flatten_entry_parts(key_segment_parts),
|
||||||
|
flatten_entry_parts(fill_triangle_parts),
|
||||||
|
flatten_entry_parts(pick_triangle_parts),
|
||||||
|
flatten_entry_parts(glyph_parts),
|
||||||
|
);
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let flatten_elapsed = flatten_started.elapsed();
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let spatial_started = std::time::Instant::now();
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let (
|
let (
|
||||||
|
|
@ -806,6 +923,17 @@ impl InteractionIndex {
|
||||||
SpatialSet::build(pick_triangle_entries),
|
SpatialSet::build(pick_triangle_entries),
|
||||||
SpatialSet::build(glyph_entries),
|
SpatialSet::build(glyph_entries),
|
||||||
);
|
);
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
if perf {
|
||||||
|
eprintln!(
|
||||||
|
"[perf] interaction-index-detail total={:.1}ms handles={:.1} collect={:.1} flatten={:.1} spatial={:.1}",
|
||||||
|
build_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
handles_elapsed.as_secs_f64() * 1000.0,
|
||||||
|
collect_elapsed.as_secs_f64() * 1000.0,
|
||||||
|
flatten_elapsed.as_secs_f64() * 1000.0,
|
||||||
|
spatial_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
wires,
|
wires,
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,14 @@ pub struct Pipeline {
|
||||||
/// both patch incrementally. Shares `wire_arena_id`.
|
/// both patch incrementally. Shares `wire_arena_id`.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) wire_arena_mesh: Option<wire_arena::WireArena>,
|
pub(crate) wire_arena_mesh: Option<wire_arena::WireArena>,
|
||||||
|
/// Chunked resident buffers for whichever arena partition exceeded one
|
||||||
|
/// GPU buffer. `Some(false)` = regular wires, `Some(true)` = mesh edges.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(crate) wire_arena_fallback: std::sync::Arc<Vec<WireGpu>>,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(crate) wire_arena_fallback_kind: Option<bool>,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(crate) wire_arena_fallback_handles: rustc_hash::FxHashSet<acadrust::Handle>,
|
||||||
/// The Model content id both arenas currently mirror (`u64::MAX` = none).
|
/// The Model content id both arenas currently mirror (`u64::MAX` = none).
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) wire_arena_id: u64,
|
pub(crate) wire_arena_id: u64,
|
||||||
|
|
@ -1455,6 +1463,12 @@ impl Pipeline {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
wire_arena_mesh: None,
|
wire_arena_mesh: None,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
wire_arena_fallback: std::sync::Arc::new(Vec::new()),
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
wire_arena_fallback_kind: None,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
wire_arena_fallback_handles: rustc_hash::FxHashSet::default(),
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
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),
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ impl WirePipelineMode {
|
||||||
|
|
||||||
// ── GPU handle ────────────────────────────────────────────────────────────
|
// ── GPU handle ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct WireGpu {
|
pub struct WireGpu {
|
||||||
pub instance_buffer: wgpu::Buffer,
|
pub instance_buffer: wgpu::Buffer,
|
||||||
/// First instance in a shared arena buffer. Standalone buffers start at 0.
|
/// First instance in a shared arena buffer. Standalone buffers start at 0.
|
||||||
|
|
@ -609,6 +610,55 @@ fn build_const_bind_group(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WireGpu {
|
impl WireGpu {
|
||||||
|
/// Native-only equivalent of [`from_run`] for an already partitioned set
|
||||||
|
/// of borrowed wires. Used when one arena partition exceeds the 256 MB
|
||||||
|
/// buffer limit: the compatible partition stays patchable while only the
|
||||||
|
/// oversized side uses chunked resident buffers.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn from_run_refs(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
wires: &[&WireModel],
|
||||||
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
|
mesh_edge: bool,
|
||||||
|
const_bgl: &wgpu::BindGroupLayout,
|
||||||
|
) -> Vec<Self> {
|
||||||
|
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)| {
|
||||||
|
let depth = if mesh_edge {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
wire_draw_depth(wire, depth_map)
|
||||||
|
};
|
||||||
|
emit_wire_native(wire, idx as u32, wire.color, depth)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut instances: Vec<WireInstance> =
|
||||||
|
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);
|
||||||
|
instances
|
||||||
|
.chunks(MAX_INSTANCES)
|
||||||
|
.map(|chunk| Self {
|
||||||
|
instance_buffer: instance_buffer_mapped(device, "wire.run.hybrid.ibuf", chunk),
|
||||||
|
first_instance: 0,
|
||||||
|
instance_count: chunk.len() as u32,
|
||||||
|
is_3d_mesh_edge: mesh_edge,
|
||||||
|
const_bind_group: Some(bind_group.clone()),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Merge a run of WireModels that share scissor + mesh-edge state into one
|
/// Merge a run of WireModels that share scissor + mesh-edge state into one
|
||||||
/// (or, past the 256 MB GPU limit, a few) instance buffer(s), then stamp
|
/// (or, past the 256 MB GPU limit, a few) instance buffer(s), then stamp
|
||||||
|
|
|
||||||
|
|
@ -499,28 +499,48 @@ impl shader::Primitive for Primitive {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let fallback_touched = |mesh_edge: bool| {
|
||||||
|
patch.map_or(true, |patch| {
|
||||||
|
patch.changes.iter().any(|(handle, _)| {
|
||||||
|
inner.wire_arena_fallback_handles.contains(handle)
|
||||||
|
|| if mesh_edge {
|
||||||
|
mesh_changed.get(handle).is_some_and(|run| !run.is_empty())
|
||||||
|
} else {
|
||||||
|
regular_changed
|
||||||
|
.get(handle)
|
||||||
|
.is_some_and(|run| !run.is_empty())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
let reg_ok = base_ok
|
let reg_ok = base_ok
|
||||||
&& inner.wire_arena.as_mut().map_or(false, |a| {
|
&& if let Some(arena) = inner.wire_arena.as_mut() {
|
||||||
let patch = patch.unwrap();
|
let patch = patch.unwrap();
|
||||||
a.patch(
|
arena.patch(
|
||||||
queue,
|
queue,
|
||||||
&patch.changes,
|
&patch.changes,
|
||||||
®ular_changed,
|
®ular_changed,
|
||||||
patch.new_handles_are_suffix,
|
patch.new_handles_are_suffix,
|
||||||
&vp.draw_depths,
|
&vp.draw_depths,
|
||||||
)
|
)
|
||||||
});
|
} else {
|
||||||
|
inner.wire_arena_fallback_kind == Some(false)
|
||||||
|
&& !fallback_touched(false)
|
||||||
|
};
|
||||||
let mesh_ok = base_ok
|
let mesh_ok = base_ok
|
||||||
&& inner.wire_arena_mesh.as_mut().map_or(false, |a| {
|
&& if let Some(arena) = inner.wire_arena_mesh.as_mut() {
|
||||||
let patch = patch.unwrap();
|
let patch = patch.unwrap();
|
||||||
a.patch(
|
arena.patch(
|
||||||
queue,
|
queue,
|
||||||
&patch.changes,
|
&patch.changes,
|
||||||
&mesh_changed,
|
&mesh_changed,
|
||||||
patch.new_handles_are_suffix,
|
patch.new_handles_are_suffix,
|
||||||
&vp.draw_depths,
|
&vp.draw_depths,
|
||||||
)
|
)
|
||||||
});
|
} else {
|
||||||
|
inner.wire_arena_fallback_kind == Some(true)
|
||||||
|
&& !fallback_touched(true)
|
||||||
|
};
|
||||||
if !reg_ok || !mesh_ok {
|
if !reg_ok || !mesh_ok {
|
||||||
// Initial upload or a patch that outgrew arena capacity:
|
// Initial upload or a patch that outgrew arena capacity:
|
||||||
// only then pay the full regular/mesh split.
|
// only then pay the full regular/mesh split.
|
||||||
|
|
@ -529,14 +549,18 @@ impl shader::Primitive for Primitive {
|
||||||
.filter(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty())
|
.filter(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty())
|
||||||
.filter_map(|w| w.name.parse::<u64>().ok())
|
.filter_map(|w| w.name.parse::<u64>().ok())
|
||||||
.collect();
|
.collect();
|
||||||
|
let regular: Vec<&crate::scene::WireModel> = vp_wires
|
||||||
|
.iter()
|
||||||
|
.filter(|w| {
|
||||||
|
!w.points.is_empty()
|
||||||
|
&& !wire_arena::is_mesh_edge(w, &mesh_names)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mesh: Vec<&crate::scene::WireModel> = vp_wires
|
||||||
|
.iter()
|
||||||
|
.filter(|w| wire_arena::is_mesh_edge(w, &mesh_names))
|
||||||
|
.collect();
|
||||||
if !reg_ok {
|
if !reg_ok {
|
||||||
let regular: Vec<&crate::scene::WireModel> = vp_wires
|
|
||||||
.iter()
|
|
||||||
.filter(|w| {
|
|
||||||
!w.points.is_empty()
|
|
||||||
&& !wire_arena::is_mesh_edge(w, &mesh_names)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
inner.wire_arena = WireArena::build(
|
inner.wire_arena = WireArena::build(
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
|
|
@ -545,12 +569,34 @@ impl shader::Primitive for Primitive {
|
||||||
bgl,
|
bgl,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
if inner.wire_arena.is_none() && !regular.is_empty() {
|
||||||
|
inner.wire_arena_fallback = std::sync::Arc::new(
|
||||||
|
crate::scene::pipeline::WireGpu::from_run_refs(
|
||||||
|
device,
|
||||||
|
®ular,
|
||||||
|
&vp.draw_depths,
|
||||||
|
false,
|
||||||
|
bgl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
inner.wire_arena_fallback_kind = Some(false);
|
||||||
|
inner.wire_arena_fallback_handles = regular
|
||||||
|
.iter()
|
||||||
|
.filter_map(|wire| {
|
||||||
|
wire.name
|
||||||
|
.parse::<u64>()
|
||||||
|
.ok()
|
||||||
|
.map(acadrust::Handle::new)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
} else if inner.wire_arena_fallback_kind == Some(false) {
|
||||||
|
inner.wire_arena_fallback =
|
||||||
|
std::sync::Arc::new(Vec::new());
|
||||||
|
inner.wire_arena_fallback_kind = None;
|
||||||
|
inner.wire_arena_fallback_handles.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !mesh_ok {
|
if !mesh_ok {
|
||||||
let mesh: Vec<&crate::scene::WireModel> = vp_wires
|
|
||||||
.iter()
|
|
||||||
.filter(|w| wire_arena::is_mesh_edge(w, &mesh_names))
|
|
||||||
.collect();
|
|
||||||
inner.wire_arena_mesh = WireArena::build(
|
inner.wire_arena_mesh = WireArena::build(
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
|
|
@ -559,15 +605,58 @@ impl shader::Primitive for Primitive {
|
||||||
bgl,
|
bgl,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
if inner.wire_arena_mesh.is_none() && !mesh.is_empty() {
|
||||||
|
inner.wire_arena_fallback = std::sync::Arc::new(
|
||||||
|
crate::scene::pipeline::WireGpu::from_run_refs(
|
||||||
|
device,
|
||||||
|
&mesh,
|
||||||
|
&vp.draw_depths,
|
||||||
|
true,
|
||||||
|
bgl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
inner.wire_arena_fallback_kind = Some(true);
|
||||||
|
inner.wire_arena_fallback_handles = mesh
|
||||||
|
.iter()
|
||||||
|
.filter_map(|wire| {
|
||||||
|
wire.name
|
||||||
|
.parse::<u64>()
|
||||||
|
.ok()
|
||||||
|
.map(acadrust::Handle::new)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
} else if inner.wire_arena_fallback_kind == Some(true) {
|
||||||
|
inner.wire_arena_fallback =
|
||||||
|
std::sync::Arc::new(Vec::new());
|
||||||
|
inner.wire_arena_fallback_kind = None;
|
||||||
|
inner.wire_arena_fallback_handles.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_patched = reg_ok && mesh_ok;
|
_patched = reg_ok && mesh_ok;
|
||||||
|
|
||||||
if let (Some(reg), Some(me)) =
|
let regular_ready = inner.wire_arena.is_some()
|
||||||
(inner.wire_arena.as_ref(), inner.wire_arena_mesh.as_ref())
|
|| inner.wire_arena_fallback_kind == Some(false);
|
||||||
|
let mesh_ready = inner.wire_arena_mesh.is_some()
|
||||||
|
|| inner.wire_arena_fallback_kind == Some(true);
|
||||||
|
if regular_ready
|
||||||
|
&& mesh_ready
|
||||||
|
&& (inner.wire_arena.is_some() || inner.wire_arena_mesh.is_some())
|
||||||
{
|
{
|
||||||
let mut gpus = reg.wire_gpus();
|
let mut gpus = if inner.wire_arena_fallback_kind == Some(false) {
|
||||||
gpus.extend(me.wire_gpus());
|
inner.wire_arena_fallback.as_ref().clone()
|
||||||
|
} else {
|
||||||
|
inner
|
||||||
|
.wire_arena
|
||||||
|
.as_ref()
|
||||||
|
.map(WireArena::wire_gpus)
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
if inner.wire_arena_fallback_kind == Some(true) {
|
||||||
|
gpus.extend(inner.wire_arena_fallback.iter().cloned());
|
||||||
|
} else if let Some(arena) = inner.wire_arena_mesh.as_ref() {
|
||||||
|
gpus.extend(arena.wire_gpus());
|
||||||
|
}
|
||||||
inner.gpu_wires = std::sync::Arc::new(gpus);
|
inner.gpu_wires = std::sync::Arc::new(gpus);
|
||||||
if _patched {
|
if _patched {
|
||||||
wire_arena::patch_handle_index(
|
wire_arena::patch_handle_index(
|
||||||
|
|
@ -583,6 +672,9 @@ impl shader::Primitive for Primitive {
|
||||||
} else {
|
} else {
|
||||||
inner.wire_arena = None;
|
inner.wire_arena = None;
|
||||||
inner.wire_arena_mesh = None;
|
inner.wire_arena_mesh = None;
|
||||||
|
inner.wire_arena_fallback = std::sync::Arc::new(Vec::new());
|
||||||
|
inner.wire_arena_fallback_kind = None;
|
||||||
|
inner.wire_arena_fallback_handles.clear();
|
||||||
inner.wire_arena_id = u64::MAX;
|
inner.wire_arena_id = u64::MAX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -594,28 +686,31 @@ impl shader::Primitive for Primitive {
|
||||||
// `.cloned()` releases the immutable cache borrow before the
|
// `.cloned()` releases the immutable cache borrow before the
|
||||||
// miss branch takes a mutable one.
|
// miss branch takes a mutable one.
|
||||||
if !arena_served {
|
if !arena_served {
|
||||||
let cached = pipeline.wire_buffer_cache.get(&vp.wire_content_id).cloned();
|
let cached = pipeline
|
||||||
let built = match cached {
|
.wire_buffer_cache
|
||||||
Some(entry) => entry,
|
.get(&vp.wire_content_id)
|
||||||
None => {
|
.cloned();
|
||||||
let entry =
|
let built = match cached {
|
||||||
inner.build_wire_buffers(device, &vp_wires[..], &vp.draw_depths);
|
Some(entry) => entry,
|
||||||
pipeline
|
None => {
|
||||||
.wire_buffer_cache
|
let entry =
|
||||||
.insert(vp.wire_content_id, entry.clone());
|
inner.build_wire_buffers(device, &vp_wires[..], &vp.draw_depths);
|
||||||
// Evict entries no slot still holds (only the cache
|
|
||||||
// references them). An entry drawn by any pane keeps a
|
|
||||||
// strong count ≥ 2, so this never drops live geometry.
|
|
||||||
if pipeline.wire_buffer_cache.len() > 16 {
|
|
||||||
pipeline
|
pipeline
|
||||||
.wire_buffer_cache
|
.wire_buffer_cache
|
||||||
.retain(|_, (w, _)| std::sync::Arc::strong_count(w) > 1);
|
.insert(vp.wire_content_id, entry.clone());
|
||||||
|
// Evict entries no slot still holds (only the cache
|
||||||
|
// references them). An entry drawn by any pane keeps a
|
||||||
|
// strong count ≥ 2, so this never drops live geometry.
|
||||||
|
if pipeline.wire_buffer_cache.len() > 16 {
|
||||||
|
pipeline
|
||||||
|
.wire_buffer_cache
|
||||||
|
.retain(|_, (w, _)| std::sync::Arc::strong_count(w) > 1);
|
||||||
|
}
|
||||||
|
entry
|
||||||
}
|
}
|
||||||
entry
|
};
|
||||||
}
|
inner.gpu_wires = built.0;
|
||||||
};
|
inner.wire_handle_index = built.1;
|
||||||
inner.gpu_wires = built.0;
|
|
||||||
inner.wire_handle_index = built.1;
|
|
||||||
} // end !arena_served
|
} // end !arena_served
|
||||||
inner.cached_wire_id = vp.wire_content_id;
|
inner.cached_wire_id = vp.wire_content_id;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -625,6 +720,8 @@ impl shader::Primitive for Primitive {
|
||||||
"shared-fullupload"
|
"shared-fullupload"
|
||||||
} else if _patched {
|
} else if _patched {
|
||||||
"arena-patch"
|
"arena-patch"
|
||||||
|
} else if inner.wire_arena_fallback_kind.is_some() {
|
||||||
|
"arena-hybrid"
|
||||||
} else {
|
} else {
|
||||||
"arena-build"
|
"arena-build"
|
||||||
};
|
};
|
||||||
|
|
@ -724,19 +821,25 @@ impl shader::Primitive for Primitive {
|
||||||
if inner.wire_arena_id == vp.wire_content_id
|
if inner.wire_arena_id == vp.wire_content_id
|
||||||
&& inner.wire_cull_key != cull_key
|
&& inner.wire_cull_key != cull_key
|
||||||
{
|
{
|
||||||
let mut visible = inner
|
let mut visible = if inner.wire_arena_fallback_kind == Some(false) {
|
||||||
.wire_arena
|
inner.wire_arena_fallback.as_ref().clone()
|
||||||
.as_ref()
|
} else {
|
||||||
.map(|arena| {
|
inner
|
||||||
arena.wire_gpus_visible(
|
.wire_arena
|
||||||
view_rot,
|
.as_ref()
|
||||||
eye,
|
.map(|arena| {
|
||||||
clip_size.width,
|
arena.wire_gpus_visible(
|
||||||
clip_size.height,
|
view_rot,
|
||||||
)
|
eye,
|
||||||
})
|
clip_size.width,
|
||||||
.unwrap_or_default();
|
clip_size.height,
|
||||||
if let Some(arena) = inner.wire_arena_mesh.as_ref() {
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
if inner.wire_arena_fallback_kind == Some(true) {
|
||||||
|
visible.extend(inner.wire_arena_fallback.iter().cloned());
|
||||||
|
} else if let Some(arena) = inner.wire_arena_mesh.as_ref() {
|
||||||
visible.extend(arena.wire_gpus_visible(
|
visible.extend(arena.wire_gpus_visible(
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue