perf: optimize large drawing workflows
This commit is contained in:
parent
1c7e4bb850
commit
71e0b88839
17 changed files with 1202 additions and 428 deletions
4
Cargo.lock
generated
4
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?branch=main#3ef1e9c3483c44b93264c93965572c23b8678f17"
|
source = "git+https://github.com/OpenAEC-Foundation/acadifc?rev=f045889e0243d88b6905124907487e0c0a1bf0e3#f045889e0243d88b6905124907487e0c0a1bf0e3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
@ -84,9 +84,11 @@ dependencies = [
|
||||||
"flate2",
|
"flate2",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
"memmap2",
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
"nom 7.1.3",
|
"nom 7.1.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rayon",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
|
|
|
||||||
|
|
@ -86,14 +86,11 @@ lyon_tessellation = "1.0.20"
|
||||||
# (IApplicationAssociationRegistrationUI dialog).
|
# (IApplicationAssociationRegistrationUI dialog).
|
||||||
# Win32_System_Registry — HKCU registration that lists the app under
|
# Win32_System_Registry — HKCU registration that lists the app under
|
||||||
# "Open with" for the portable .exe.
|
# "Open with" for the portable .exe.
|
||||||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "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]
|
||||||
# The `acadrust` crate now tracks the OpenAEC-Foundation/acadifc fork's main
|
# Track the DWG reader/writer performance work used by this release.
|
||||||
# (still the `acadrust` package): the in-flight DWG reader work plus the Arc
|
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc", rev = "f045889e0243d88b6905124907487e0c0a1bf0e3" }
|
||||||
# copy-on-write entity storage that makes the undo document clone cheap. Drop the
|
|
||||||
# patch once these land in a published 0.4 crate.
|
|
||||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc", branch = "main" }
|
|
||||||
|
|
||||||
[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).
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,11 @@ use iced;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
static NEXT_DOCUMENT_TAB_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
// ── Dynamic input ──────────────────────────────────────────────────────────
|
// ── Dynamic input ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// One quantity shown in the dynamic-input overlay near the cursor.
|
/// One quantity shown in the dynamic-input overlay near the cursor.
|
||||||
|
|
@ -154,9 +157,15 @@ impl DocumentTab {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct DocumentTab {
|
pub(super) struct DocumentTab {
|
||||||
|
/// Stable identity across tab insert/remove operations. Background work
|
||||||
|
/// must never target a tab by its transient vector index.
|
||||||
|
pub(super) id: u64,
|
||||||
pub(super) scene: Scene,
|
pub(super) scene: Scene,
|
||||||
pub(super) current_path: Option<PathBuf>,
|
pub(super) current_path: Option<PathBuf>,
|
||||||
pub(super) dirty: bool,
|
pub(super) dirty: bool,
|
||||||
|
/// Monotonic committed-edit/undo/redo revision. Background save completion
|
||||||
|
/// uses it with scene epochs so an older snapshot never clears newer work.
|
||||||
|
pub(super) edit_revision: u64,
|
||||||
pub(super) tab_title: String,
|
pub(super) tab_title: String,
|
||||||
pub(super) properties: PropertiesPanel,
|
pub(super) properties: PropertiesPanel,
|
||||||
pub(super) layers: LayerPanel,
|
pub(super) layers: LayerPanel,
|
||||||
|
|
@ -413,9 +422,11 @@ impl DocumentTab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
|
id: NEXT_DOCUMENT_TAB_ID.fetch_add(1, Ordering::Relaxed),
|
||||||
scene,
|
scene,
|
||||||
current_path: None,
|
current_path: None,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
|
edit_revision: 0,
|
||||||
prev_selection: Vec::new(),
|
prev_selection: Vec::new(),
|
||||||
tab_title: format!("Drawing{}", n),
|
tab_title: format!("Drawing{}", n),
|
||||||
properties: PropertiesPanel::empty(),
|
properties: PropertiesPanel::empty(),
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
fn push_undo_entry(&mut self, i: usize, snapshot: HistorySnapshot) {
|
fn push_undo_entry(&mut self, i: usize, snapshot: HistorySnapshot) {
|
||||||
self.tabs[i].history.undo_stack.push(snapshot);
|
self.tabs[i].history.undo_stack.push(snapshot);
|
||||||
|
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
||||||
self.clear_redo_history(i);
|
self.clear_redo_history(i);
|
||||||
self.trim_history(i);
|
self.trim_history(i);
|
||||||
}
|
}
|
||||||
|
|
@ -480,6 +481,7 @@ impl OpenCADStudio {
|
||||||
had_full: bool,
|
had_full: bool,
|
||||||
changes: &[(Handle, crate::scene::ChangeKind)],
|
changes: &[(Handle, crate::scene::ChangeKind)],
|
||||||
) {
|
) {
|
||||||
|
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
||||||
{
|
{
|
||||||
let scene = &mut self.tabs[i].scene;
|
let scene = &mut self.tabs[i].scene;
|
||||||
if had_full {
|
if had_full {
|
||||||
|
|
|
||||||
|
|
@ -739,6 +739,10 @@ pub(super) struct OpenCADStudio {
|
||||||
// ── Unsaved-changes dialog ────────────────────────────────────────────
|
// ── Unsaved-changes dialog ────────────────────────────────────────────
|
||||||
/// Set when the user tries to close a tab or quit while there are unsaved changes.
|
/// Set when the user tries to close a tab or quit while there are unsaved changes.
|
||||||
pending_close: Option<PendingClose>,
|
pending_close: Option<PendingClose>,
|
||||||
|
/// Latest save job per stable tab id. Older completions may finish, but
|
||||||
|
/// cannot mark a newer document state clean or redirect its path.
|
||||||
|
active_save_jobs: std::collections::HashMap<u64, u64>,
|
||||||
|
save_job_serial: u64,
|
||||||
/// OS window for the unsaved-changes confirmation dialog.
|
/// OS window for the unsaved-changes confirmation dialog.
|
||||||
|
|
||||||
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||||
|
|
@ -847,6 +851,35 @@ pub(super) enum PendingClose {
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(super) enum SavePurpose {
|
||||||
|
Manual,
|
||||||
|
SaveAs,
|
||||||
|
Autosave,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(super) enum SaveContinuation {
|
||||||
|
None,
|
||||||
|
CloseTab,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SaveOutcome {
|
||||||
|
job_id: u64,
|
||||||
|
tab_id: u64,
|
||||||
|
epoch: u64,
|
||||||
|
revision: u64,
|
||||||
|
camera_generation: u64,
|
||||||
|
path: PathBuf,
|
||||||
|
previous_autosave: Option<PathBuf>,
|
||||||
|
set_current_path: bool,
|
||||||
|
purpose: SavePurpose,
|
||||||
|
continuation: SaveContinuation,
|
||||||
|
result: Result<(), String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Where a colour chosen in the standalone palette window should be applied.
|
/// Where a colour chosen in the standalone palette window should be applied.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ColorPickTarget {
|
pub enum ColorPickTarget {
|
||||||
|
|
@ -1404,8 +1437,8 @@ pub enum Message {
|
||||||
AecDropBack,
|
AecDropBack,
|
||||||
/// Periodic autosave tick — write `.sv$` recovery files for dirty tabs.
|
/// Periodic autosave tick — write `.sv$` recovery files for dirty tabs.
|
||||||
AutoSave,
|
AutoSave,
|
||||||
/// Save-as path picked for the unsaved-changes → save → close flow.
|
/// Native background save/autosave completed.
|
||||||
UnsavedPickedSavePath(Option<std::path::PathBuf>),
|
SaveFinished(SaveOutcome),
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
CommandInput(String),
|
CommandInput(String),
|
||||||
CommandSubmit,
|
CommandSubmit,
|
||||||
|
|
@ -2418,6 +2451,8 @@ impl OpenCADStudio {
|
||||||
opening: None,
|
opening: None,
|
||||||
pending_opens: std::collections::VecDeque::new(),
|
pending_opens: std::collections::VecDeque::new(),
|
||||||
pending_close: None,
|
pending_close: None,
|
||||||
|
active_save_jobs: std::collections::HashMap::new(),
|
||||||
|
save_job_serial: 0,
|
||||||
save_dialog_format: "DWG 2018".to_string(),
|
save_dialog_format: "DWG 2018".to_string(),
|
||||||
save_dialog_filename: "drawing.dwg".to_string(),
|
save_dialog_filename: "drawing.dwg".to_string(),
|
||||||
save_dialog_for_unsaved: false,
|
save_dialog_for_unsaved: false,
|
||||||
|
|
|
||||||
|
|
@ -234,130 +234,83 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_unsaved_dialog_save(&mut self) -> Task<Message> {
|
pub(super) fn on_unsaved_dialog_save(&mut self) -> Task<Message> {
|
||||||
match self.pending_close.take() {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
Some(crate::app::PendingClose::Tab(idx)) => {
|
{
|
||||||
if let Some(path) = if cfg!(target_arch = "wasm32") { None } else { self.tabs[idx].current_path.clone() } {
|
let Some(pending) = self.pending_close.clone() else {
|
||||||
match crate::io::save(&self.tabs[idx].scene.document, &path) {
|
return Task::none();
|
||||||
Ok(()) => {
|
};
|
||||||
self.command_line
|
let (idx, continuation) = match pending {
|
||||||
.push_output(&format!("Saved: {}", path.display()));
|
crate::app::PendingClose::Tab(idx) => {
|
||||||
self.tabs[idx].dirty = false;
|
(idx, crate::app::SaveContinuation::CloseTab)
|
||||||
let close_win = self.close_unsaved_dialog_window();
|
|
||||||
let close_tab = self.update(Message::TabClose(idx));
|
|
||||||
return Task::batch(vec![close_win, close_tab]);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Keep dialog open for retry.
|
|
||||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
|
||||||
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No path — close unsaved dialog and save a default
|
|
||||||
// DWG 2018 via the native destination dialog (no
|
|
||||||
// version picker; that is reserved for Save As).
|
|
||||||
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
|
||||||
self.save_dialog_for_unsaved = true;
|
|
||||||
let close_win = self.close_unsaved_dialog_window();
|
|
||||||
let open_save = self.save_default_dwg2018(idx);
|
|
||||||
return Task::batch([close_win, open_save]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(crate::app::PendingClose::Quit) => {
|
|
||||||
if let Some(idx) = self.tabs.iter().position(|t| t.dirty) {
|
|
||||||
if let Some(path) = if cfg!(target_arch = "wasm32") { None } else { self.tabs[idx].current_path.clone() } {
|
|
||||||
match crate::io::save(&self.tabs[idx].scene.document, &path) {
|
|
||||||
Ok(()) => {
|
|
||||||
self.command_line
|
|
||||||
.push_output(&format!("Saved: {}", path.display()));
|
|
||||||
self.tabs[idx].dirty = false;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
|
||||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No path — close unsaved dialog and save a
|
|
||||||
// default DWG 2018 via the native destination
|
|
||||||
// dialog (version picker is Save-As only).
|
|
||||||
self.active_tab = idx;
|
|
||||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
||||||
self.save_dialog_for_unsaved = true;
|
|
||||||
let close_win = self.close_unsaved_dialog_window();
|
|
||||||
let open_save = self.save_default_dwg2018(idx);
|
|
||||||
return Task::batch([close_win, open_save]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.tabs.iter().any(|t| t.dirty) {
|
|
||||||
// More dirty tabs — keep window open.
|
|
||||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
||||||
} else {
|
|
||||||
let close_win = self.close_unsaved_dialog_window();
|
|
||||||
return Task::batch(vec![close_win, self.exit_app()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
}
|
||||||
Task::none()
|
crate::app::PendingClose::Quit => {
|
||||||
|
let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) else {
|
||||||
|
self.pending_close = None;
|
||||||
|
return Task::batch([
|
||||||
|
self.close_unsaved_dialog_window(),
|
||||||
|
self.exit_app(),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
(idx, crate::app::SaveContinuation::Quit)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.active_save_jobs.contains_key(&self.tabs[idx].id) {
|
||||||
|
self.command_line
|
||||||
|
.push_info("Save already running for this drawing.");
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(path) = self.tabs[idx].current_path.clone() {
|
||||||
|
let version = self.tabs[idx].scene.document.version;
|
||||||
|
self.prepare_native_save(idx);
|
||||||
|
let close = self.close_unsaved_dialog_window();
|
||||||
|
let save = self.queue_native_save(
|
||||||
|
idx,
|
||||||
|
path,
|
||||||
|
version,
|
||||||
|
crate::app::SavePurpose::Manual,
|
||||||
|
continuation,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
return Task::batch([close, save]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.active_tab = idx;
|
||||||
|
self.save_dialog_for_unsaved = true;
|
||||||
|
let close = self.close_unsaved_dialog_window();
|
||||||
|
let save = self.save_default_dwg2018(idx);
|
||||||
|
return Task::batch([close, save]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
match self.pending_close.take() {
|
||||||
|
Some(crate::app::PendingClose::Tab(idx)) => {
|
||||||
|
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
||||||
|
self.save_dialog_for_unsaved = true;
|
||||||
|
let close = self.close_unsaved_dialog_window();
|
||||||
|
let save = self.save_default_dwg2018(idx);
|
||||||
|
Task::batch([close, save])
|
||||||
|
}
|
||||||
|
Some(crate::app::PendingClose::Quit) => {
|
||||||
|
if let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) {
|
||||||
|
self.active_tab = idx;
|
||||||
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||||
|
self.save_dialog_for_unsaved = true;
|
||||||
|
let close = self.close_unsaved_dialog_window();
|
||||||
|
let save = self.save_default_dwg2018(idx);
|
||||||
|
Task::batch([close, save])
|
||||||
|
} else {
|
||||||
|
Task::batch([
|
||||||
|
self.close_unsaved_dialog_window(),
|
||||||
|
self.exit_app(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Task::none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_unsaved_picked_save_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
|
||||||
let (_ext, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
|
||||||
match self.pending_close.take() {
|
|
||||||
Some(crate::app::PendingClose::Tab(idx)) => {
|
|
||||||
match crate::io::save_as_version(
|
|
||||||
&self.tabs[idx].scene.document,
|
|
||||||
&path,
|
|
||||||
version,
|
|
||||||
) {
|
|
||||||
Ok(()) => {
|
|
||||||
self.command_line
|
|
||||||
.push_output(&format!("Saved: {}", path.display()));
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
let _ = std::fs::remove_file(self.autosave_target(idx));
|
|
||||||
self.tabs[idx].current_path = Some(path);
|
|
||||||
self.tabs[idx].dirty = false;
|
|
||||||
return self.update(Message::TabClose(idx));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
|
||||||
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
|
||||||
return self.open_unsaved_dialog_window();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(crate::app::PendingClose::Quit) => {
|
|
||||||
let i = self.active_tab;
|
|
||||||
match crate::io::save_as_version(
|
|
||||||
&self.tabs[i].scene.document,
|
|
||||||
&path,
|
|
||||||
version,
|
|
||||||
) {
|
|
||||||
Ok(()) => {
|
|
||||||
self.command_line
|
|
||||||
.push_output(&format!("Saved: {}", path.display()));
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
let _ = std::fs::remove_file(self.autosave_target(i));
|
|
||||||
self.tabs[i].current_path = Some(path);
|
|
||||||
self.tabs[i].dirty = false;
|
|
||||||
if self.tabs.iter().any(|t| t.dirty) {
|
|
||||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
||||||
return self.open_unsaved_dialog_window();
|
|
||||||
} else {
|
|
||||||
return self.exit_app();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
|
||||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
||||||
return self.open_unsaved_dialog_window();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -768,6 +768,213 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
||||||
|
self.sync_vport_display(i);
|
||||||
|
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
||||||
|
self.stamp_header_sysvars(i);
|
||||||
|
self.tabs[i].scene.document.header.user_real1 =
|
||||||
|
self.tabs[i].scene.annotation_scale as f64;
|
||||||
|
self.sync_truck_solids_to_acis(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(in crate::app) fn queue_native_save(
|
||||||
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
purpose: crate::app::SavePurpose,
|
||||||
|
continuation: crate::app::SaveContinuation,
|
||||||
|
set_current_path: bool,
|
||||||
|
) -> Task<Message> {
|
||||||
|
let tab_id = self.tabs[i].id;
|
||||||
|
if self.active_save_jobs.contains_key(&tab_id) {
|
||||||
|
if purpose != crate::app::SavePurpose::Autosave {
|
||||||
|
self.command_line
|
||||||
|
.push_info("Save already running for this drawing.");
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
let epoch = self.tabs[i].scene.geometry_epoch;
|
||||||
|
let revision = self.tabs[i].edit_revision;
|
||||||
|
let camera_generation = self.tabs[i].scene.camera_generation;
|
||||||
|
let thumbnail = if purpose == crate::app::SavePurpose::Autosave {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let scene = &self.tabs[i].scene;
|
||||||
|
Some((
|
||||||
|
scene.entity_wires(),
|
||||||
|
scene.camera.borrow().clone(),
|
||||||
|
scene.bg_color,
|
||||||
|
version >= acadrust::DxfVersion::AC1027,
|
||||||
|
self.vp_size,
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let clone_started = std::time::Instant::now();
|
||||||
|
let mut snapshot = self.tabs[i].scene.document.clone();
|
||||||
|
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if std::env::var_os("OCS_PERF").is_some() {
|
||||||
|
eprintln!(
|
||||||
|
"[perf] save-snapshot {:.1}ms entities={} objects={} purpose={purpose:?}",
|
||||||
|
clone_ms,
|
||||||
|
snapshot.entities().count(),
|
||||||
|
snapshot.objects.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save_job_serial = self.save_job_serial.wrapping_add(1);
|
||||||
|
let job_id = self.save_job_serial;
|
||||||
|
self.active_save_jobs.insert(tab_id, job_id);
|
||||||
|
let previous_autosave = set_current_path.then(|| self.autosave_target(i));
|
||||||
|
let backup = purpose != crate::app::SavePurpose::Autosave && self.backup_on_save;
|
||||||
|
let worker_path = path.clone();
|
||||||
|
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
let result = std::thread::spawn(move || {
|
||||||
|
if let Some((wires, camera, bg_color, png, viewport)) = thumbnail {
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
snapshot.preview = crate::io::thumbnail::from_snapshot(
|
||||||
|
&wires,
|
||||||
|
&camera,
|
||||||
|
bg_color,
|
||||||
|
png,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
if std::env::var_os("OCS_PERF").is_some() {
|
||||||
|
eprintln!(
|
||||||
|
"[perf] save-thumbnail {:.1}ms wires={}",
|
||||||
|
started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
wires.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::io::save_owned_as_version_atomic(
|
||||||
|
snapshot,
|
||||||
|
&worker_path,
|
||||||
|
version,
|
||||||
|
backup,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap_or_else(|_| Err("save worker panicked".to_string()));
|
||||||
|
crate::app::SaveOutcome {
|
||||||
|
job_id,
|
||||||
|
tab_id,
|
||||||
|
epoch,
|
||||||
|
revision,
|
||||||
|
camera_generation,
|
||||||
|
path,
|
||||||
|
previous_autosave,
|
||||||
|
set_current_path,
|
||||||
|
purpose,
|
||||||
|
continuation,
|
||||||
|
result,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message::SaveFinished,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(super) fn on_save_finished(
|
||||||
|
&mut self,
|
||||||
|
outcome: crate::app::SaveOutcome,
|
||||||
|
) -> Task<Message> {
|
||||||
|
let latest = self.active_save_jobs.get(&outcome.tab_id).copied()
|
||||||
|
== Some(outcome.job_id);
|
||||||
|
if latest {
|
||||||
|
self.active_save_jobs.remove(&outcome.tab_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == outcome.tab_id) else {
|
||||||
|
if outcome.purpose == crate::app::SavePurpose::Autosave {
|
||||||
|
let _ = std::fs::remove_file(&outcome.path);
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if !latest {
|
||||||
|
if outcome.purpose == crate::app::SavePurpose::Autosave {
|
||||||
|
let _ = std::fs::remove_file(&outcome.path);
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = outcome.result {
|
||||||
|
self.command_line
|
||||||
|
.push_error(&format!("Save failed: {error}"));
|
||||||
|
return match outcome.continuation {
|
||||||
|
crate::app::SaveContinuation::CloseTab => {
|
||||||
|
self.pending_close = Some(crate::app::PendingClose::Tab(i));
|
||||||
|
self.open_unsaved_dialog_window()
|
||||||
|
}
|
||||||
|
crate::app::SaveContinuation::Quit => {
|
||||||
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||||
|
self.open_unsaved_dialog_window()
|
||||||
|
}
|
||||||
|
crate::app::SaveContinuation::None => Task::none(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot_is_current = self.tabs[i].scene.geometry_epoch == outcome.epoch
|
||||||
|
&& self.tabs[i].edit_revision == outcome.revision
|
||||||
|
&& self.tabs[i].scene.camera_generation == outcome.camera_generation;
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
match outcome.purpose {
|
||||||
|
crate::app::SavePurpose::Autosave => {
|
||||||
|
self.command_line.push_output("Autosaved 1 drawing");
|
||||||
|
}
|
||||||
|
crate::app::SavePurpose::Manual | crate::app::SavePurpose::SaveAs => {
|
||||||
|
self.command_line
|
||||||
|
.push_output(&format!("Saved: {}", outcome.path.display()));
|
||||||
|
self.recent_thumbs.remove(&outcome.path);
|
||||||
|
if let Some(previous) = outcome.previous_autosave {
|
||||||
|
if previous != outcome.path {
|
||||||
|
let _ = std::fs::remove_file(previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if outcome.set_current_path {
|
||||||
|
self.tabs[i].current_path = Some(outcome.path.clone());
|
||||||
|
tasks.push(self.push_recent(outcome.path.clone()));
|
||||||
|
}
|
||||||
|
if snapshot_is_current {
|
||||||
|
self.tabs[i].dirty = false;
|
||||||
|
let _ = std::fs::remove_file(outcome.path.with_extension("sv$"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match outcome.continuation {
|
||||||
|
crate::app::SaveContinuation::None => {}
|
||||||
|
crate::app::SaveContinuation::CloseTab if snapshot_is_current => {
|
||||||
|
self.pending_close = None;
|
||||||
|
tasks.push(self.close_unsaved_dialog_window());
|
||||||
|
tasks.push(self.update(Message::TabClose(i)));
|
||||||
|
}
|
||||||
|
crate::app::SaveContinuation::Quit if snapshot_is_current => {
|
||||||
|
self.pending_close = None;
|
||||||
|
if self.tabs.iter().any(|tab| tab.dirty) {
|
||||||
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||||
|
tasks.push(self.open_unsaved_dialog_window());
|
||||||
|
} else {
|
||||||
|
tasks.push(self.close_unsaved_dialog_window());
|
||||||
|
tasks.push(self.exit_app());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::app::SaveContinuation::CloseTab => {
|
||||||
|
self.pending_close = Some(crate::app::PendingClose::Tab(i));
|
||||||
|
tasks.push(self.open_unsaved_dialog_window());
|
||||||
|
}
|
||||||
|
crate::app::SaveContinuation::Quit => {
|
||||||
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||||
|
tasks.push(self.open_unsaved_dialog_window());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::batch(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn on_save_file(&mut self) -> Task<Message> {
|
pub(super) fn on_save_file(&mut self) -> Task<Message> {
|
||||||
if self.read_only {
|
if self.read_only {
|
||||||
self.command_line
|
self.command_line
|
||||||
|
|
@ -775,37 +982,28 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
// Stamp the live grid/snap toggles onto the VPort so the file
|
// Web serializes immediately below. Native preparation happens
|
||||||
// reflects them even if they came from settings with no
|
// once after the destination/version is known.
|
||||||
// in-session toggle (#121).
|
#[cfg(target_arch = "wasm32")]
|
||||||
self.sync_vport_display(i);
|
{
|
||||||
// Persist Ortho ($ORTHOMODE) + running OSNAP ($OSMODE) into the
|
self.sync_vport_display(i);
|
||||||
// drawing header so they survive save/reopen (per-drawing).
|
self.stamp_header_sysvars(i);
|
||||||
self.stamp_header_sysvars(i);
|
}
|
||||||
// Native: save straight to the known path. Web has no path
|
// Native: save straight to the known path. Web has no path
|
||||||
// (downloads instead), so always go through the Save dialog.
|
// (downloads instead), so always go through the Save dialog.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if let Some(path) = self.tabs[i].current_path.clone() {
|
if let Some(path) = self.tabs[i].current_path.clone() {
|
||||||
self.tabs[i].scene.document.header.user_real1 =
|
|
||||||
self.tabs[i].scene.annotation_scale as f64;
|
|
||||||
// A direct Save preserves the document's current version.
|
// A direct Save preserves the document's current version.
|
||||||
if self.backup_on_save {
|
|
||||||
crate::io::write_backup(&path);
|
|
||||||
}
|
|
||||||
self.sync_truck_solids_to_acis(i);
|
|
||||||
let ver = self.tabs[i].scene.document.version;
|
let ver = self.tabs[i].scene.document.version;
|
||||||
self.stamp_thumbnail(i, ver);
|
self.prepare_native_save(i);
|
||||||
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
return self.queue_native_save(
|
||||||
Ok(()) => {
|
i,
|
||||||
self.command_line
|
path,
|
||||||
.push_output(&format!("Saved: {}", path.display()));
|
ver,
|
||||||
self.tabs[i].dirty = false;
|
crate::app::SavePurpose::Manual,
|
||||||
// A clean save supersedes any autosave recovery copy.
|
crate::app::SaveContinuation::None,
|
||||||
let _ = std::fs::remove_file(path.with_extension("sv$"));
|
false,
|
||||||
}
|
);
|
||||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
|
||||||
}
|
|
||||||
return Task::none();
|
|
||||||
}
|
}
|
||||||
self.save_dialog_for_unsaved = false;
|
self.save_dialog_for_unsaved = false;
|
||||||
self.save_default_dwg2018(i)
|
self.save_default_dwg2018(i)
|
||||||
|
|
@ -937,37 +1135,24 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
};
|
};
|
||||||
let (_ext, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
let (_ext, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
self.prepare_native_save(i);
|
||||||
// Persist Ortho / running OSNAP into the header (Save-As path).
|
let continuation = if self.save_dialog_for_unsaved {
|
||||||
self.stamp_header_sysvars(i);
|
match self.pending_close {
|
||||||
self.sync_truck_solids_to_acis(i);
|
Some(crate::app::PendingClose::Tab(_)) => crate::app::SaveContinuation::CloseTab,
|
||||||
self.stamp_thumbnail(i, version);
|
Some(crate::app::PendingClose::Quit) => crate::app::SaveContinuation::Quit,
|
||||||
if self.backup_on_save {
|
None => crate::app::SaveContinuation::None,
|
||||||
crate::io::write_backup(&path);
|
|
||||||
}
|
|
||||||
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
|
||||||
Ok(()) => {
|
|
||||||
self.command_line
|
|
||||||
.push_output(&format!("Saved: {}", path.display()));
|
|
||||||
// Drop any prior autosave copy — including the temp one used
|
|
||||||
// while the drawing was still unsaved — before the tab takes on
|
|
||||||
// its new path.
|
|
||||||
let _ = std::fs::remove_file(self.autosave_target(i));
|
|
||||||
self.tabs[i].current_path = Some(path.clone());
|
|
||||||
self.tabs[i].dirty = false;
|
|
||||||
let _ = std::fs::remove_file(path.with_extension("sv$"));
|
|
||||||
let recent = self.push_recent(path.clone());
|
|
||||||
if self.save_dialog_for_unsaved {
|
|
||||||
return Task::batch([
|
|
||||||
recent,
|
|
||||||
self.update(Message::UnsavedPickedSavePath(Some(path))),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return recent;
|
|
||||||
}
|
}
|
||||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
} else {
|
||||||
}
|
crate::app::SaveContinuation::None
|
||||||
Task::none()
|
};
|
||||||
|
self.queue_native_save(
|
||||||
|
i,
|
||||||
|
path,
|
||||||
|
version,
|
||||||
|
crate::app::SavePurpose::SaveAs,
|
||||||
|
continuation,
|
||||||
|
true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AEC-drop warning → "Save anyway": accept the loss and proceed with the
|
/// AEC-drop warning → "Save anyway": accept the loss and proceed with the
|
||||||
|
|
@ -1019,25 +1204,25 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
/// touches the original file or the dirty flag.
|
/// touches the original file or the dirty flag.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
||||||
let mut n = 0;
|
let mut tasks = Vec::new();
|
||||||
for i in 0..self.tabs.len() {
|
for i in 0..self.tabs.len() {
|
||||||
if !self.tabs[i].dirty {
|
if !self.tabs[i].dirty
|
||||||
|
|| self.active_save_jobs.contains_key(&self.tabs[i].id)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let version = self.tabs[i].scene.document.version;
|
let version = self.tabs[i].scene.document.version;
|
||||||
if let Ok(bytes) =
|
let target = self.autosave_target(i);
|
||||||
crate::io::save_to_bytes(&self.tabs[i].scene.document, "dwg", version)
|
tasks.push(self.queue_native_save(
|
||||||
{
|
i,
|
||||||
if std::fs::write(self.autosave_target(i), bytes).is_ok() {
|
target,
|
||||||
n += 1;
|
version,
|
||||||
}
|
crate::app::SavePurpose::Autosave,
|
||||||
}
|
crate::app::SaveContinuation::None,
|
||||||
|
false,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if n > 0 {
|
Task::batch(tasks)
|
||||||
self.command_line
|
|
||||||
.push_output(&format!("Autosaved {n} drawing(s)"));
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
|
|
||||||
|
|
@ -3770,17 +3770,11 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::AutoSave => self.on_autosave(),
|
Message::AutoSave => self.on_autosave(),
|
||||||
|
|
||||||
Message::UnsavedPickedSavePath(Some(path)) => {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
self.on_unsaved_picked_save_path_some(path)
|
Message::SaveFinished(outcome) => self.on_save_finished(outcome),
|
||||||
}
|
|
||||||
|
|
||||||
Message::UnsavedPickedSavePath(None) => {
|
#[cfg(target_arch = "wasm32")]
|
||||||
// User cancelled the save-as dialog — re-open the confirmation dialog.
|
Message::SaveFinished(_) => Task::none(),
|
||||||
if self.pending_close.is_some() {
|
|
||||||
return self.open_unsaved_dialog_window();
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Page Setup ────────────────────────────────────────────────
|
// ── Page Setup ────────────────────────────────────────────────
|
||||||
Message::UpdateCheckResult(latest) => {
|
Message::UpdateCheckResult(latest) => {
|
||||||
|
|
|
||||||
153
src/io/mod.rs
153
src/io/mod.rs
|
|
@ -183,6 +183,12 @@ pub fn load_file(path: &Path) -> Result<CadDocument, String> {
|
||||||
|
|
||||||
match effective.as_str() {
|
match effective.as_str() {
|
||||||
"dwg" => {
|
"dwg" => {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let mut doc = DwgReader::from_mmap(path)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.read()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
let mut doc = DwgReader::from_file(path)
|
let mut doc = DwgReader::from_file(path)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.read()
|
.read()
|
||||||
|
|
@ -418,19 +424,130 @@ pub fn save_as_version(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
version: acadrust::DxfVersion,
|
version: acadrust::DxfVersion,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut doc = doc.clone();
|
let clone_started = std::time::Instant::now();
|
||||||
|
let snapshot = doc.clone();
|
||||||
|
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
save_owned_as_version_inner(snapshot, path, version, false, clone_ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save an owned document snapshot. Preparation, serialization, compression and
|
||||||
|
/// disk I/O can therefore run on a worker without borrowing live editor state.
|
||||||
|
/// Output is written beside the destination and atomically renamed only after a
|
||||||
|
/// complete file exists, so a failed save cannot truncate the previous drawing.
|
||||||
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||||
|
pub fn save_owned_as_version_atomic(
|
||||||
|
doc: CadDocument,
|
||||||
|
path: &Path,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
backup: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
save_owned_as_version_inner(doc, path, version, backup, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_owned_as_version_inner(
|
||||||
|
mut doc: CadDocument,
|
||||||
|
path: &Path,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
backup: bool,
|
||||||
|
clone_ms: f64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||||
|
let total_started = std::time::Instant::now();
|
||||||
doc.version = version;
|
doc.version = version;
|
||||||
|
let styles_started = std::time::Instant::now();
|
||||||
sync_current_styles_on_save(&mut doc);
|
sync_current_styles_on_save(&mut doc);
|
||||||
|
let styles_ms = styles_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
let dimensions_started = std::time::Instant::now();
|
||||||
crate::modules::draw::modify::explode::bake_dimension_blocks(&mut doc);
|
crate::modules::draw::modify::explode::bake_dimension_blocks(&mut doc);
|
||||||
let ext = path
|
let dimensions_ms = dimensions_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
let temp_path = save_temp_path(path);
|
||||||
|
let ext = temp_path
|
||||||
.extension()
|
.extension()
|
||||||
.map(|e| e.to_string_lossy().to_lowercase())
|
.map(|e| e.to_string_lossy().to_lowercase())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
match ext.as_str() {
|
let write_started = std::time::Instant::now();
|
||||||
|
let result = match ext.as_str() {
|
||||||
"dxf" => DxfWriter::new(&doc)
|
"dxf" => DxfWriter::new(&doc)
|
||||||
.write_to_file(path)
|
.write_to_file(&temp_path)
|
||||||
.map_err(|e| e.to_string()),
|
.map_err(|e| e.to_string()),
|
||||||
_ => DwgWriter::write_to_file(path, &doc).map_err(|e| e.to_string()),
|
_ => DwgWriter::write_to_file(&temp_path, &doc).map_err(|e| e.to_string()),
|
||||||
|
};
|
||||||
|
if let Err(error) = result {
|
||||||
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
if backup {
|
||||||
|
write_backup(path);
|
||||||
|
}
|
||||||
|
if let Err(error) = replace_save_file(&temp_path, path) {
|
||||||
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
|
return Err(format!("replace {}: {error}", path.display()));
|
||||||
|
}
|
||||||
|
if perf {
|
||||||
|
eprintln!(
|
||||||
|
"[perf] save total={:.1}ms clone={:.1} styles={:.1} dimensions={:.1} write={:.1} entities={} objects={} path={}",
|
||||||
|
total_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
clone_ms,
|
||||||
|
styles_ms,
|
||||||
|
dimensions_ms,
|
||||||
|
write_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
doc.entities().count(),
|
||||||
|
doc.objects.len(),
|
||||||
|
path.display(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_temp_path(path: &Path) -> PathBuf {
|
||||||
|
static SERIAL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||||
|
let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let stem = path
|
||||||
|
.file_stem()
|
||||||
|
.map(|value| value.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "drawing".to_string());
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.map(|value| value.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "dwg".to_string());
|
||||||
|
let name = format!(
|
||||||
|
".{stem}.ocs-save-{}-{serial}.{extension}",
|
||||||
|
std::process::id()
|
||||||
|
);
|
||||||
|
path.parent().unwrap_or_else(|| Path::new(".")).join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn replace_save_file(temp_path: &Path, path: &Path) -> std::io::Result<()> {
|
||||||
|
std::fs::rename(temp_path, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn replace_save_file(temp_path: &Path, path: &Path) -> std::io::Result<()> {
|
||||||
|
if !path.exists() {
|
||||||
|
return std::fs::rename(temp_path, path);
|
||||||
|
}
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
let replaced: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||||
|
let replacement: Vec<u16> = temp_path
|
||||||
|
.as_os_str()
|
||||||
|
.encode_wide()
|
||||||
|
.chain(Some(0))
|
||||||
|
.collect();
|
||||||
|
let ok = unsafe {
|
||||||
|
windows_sys::Win32::Storage::FileSystem::ReplaceFileW(
|
||||||
|
replaced.as_ptr(),
|
||||||
|
replacement.as_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ok == 0 {
|
||||||
|
Err(std::io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -450,18 +567,42 @@ pub fn save_to_bytes(
|
||||||
ext: &str,
|
ext: &str,
|
||||||
version: acadrust::DxfVersion,
|
version: acadrust::DxfVersion,
|
||||||
) -> Result<Vec<u8>, String> {
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||||
|
let total_started = std::time::Instant::now();
|
||||||
|
let clone_started = std::time::Instant::now();
|
||||||
let mut doc = doc.clone();
|
let mut doc = doc.clone();
|
||||||
|
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
doc.version = version;
|
doc.version = version;
|
||||||
|
let styles_started = std::time::Instant::now();
|
||||||
sync_current_styles_on_save(&mut doc);
|
sync_current_styles_on_save(&mut doc);
|
||||||
|
let styles_ms = styles_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
let dimensions_started = std::time::Instant::now();
|
||||||
crate::modules::draw::modify::explode::bake_dimension_blocks(&mut doc);
|
crate::modules::draw::modify::explode::bake_dimension_blocks(&mut doc);
|
||||||
match ext.to_lowercase().as_str() {
|
let dimensions_ms = dimensions_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
let write_started = std::time::Instant::now();
|
||||||
|
let result = match ext.to_lowercase().as_str() {
|
||||||
"dxf" => DxfWriter::new(&doc).write_to_vec().map_err(|e| e.to_string()),
|
"dxf" => DxfWriter::new(&doc).write_to_vec().map_err(|e| e.to_string()),
|
||||||
_ => {
|
_ => {
|
||||||
let mut buf = std::io::Cursor::new(Vec::new());
|
let mut buf = std::io::Cursor::new(Vec::new());
|
||||||
DwgWriter::write_to_writer(&mut buf, &doc).map_err(|e| e.to_string())?;
|
DwgWriter::write_to_writer(&mut buf, &doc).map_err(|e| e.to_string())?;
|
||||||
Ok(buf.into_inner())
|
Ok(buf.into_inner())
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if perf {
|
||||||
|
let bytes = result.as_ref().map_or(0, Vec::len);
|
||||||
|
eprintln!(
|
||||||
|
"[perf] save-bytes total={:.1}ms clone={:.1} styles={:.1} dimensions={:.1} write={:.1} bytes={} entities={} objects={}",
|
||||||
|
total_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
clone_ms,
|
||||||
|
styles_ms,
|
||||||
|
dimensions_ms,
|
||||||
|
write_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
bytes,
|
||||||
|
doc.entities().count(),
|
||||||
|
doc.objects.len(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use image::{ImageFormat, Rgb, RgbImage};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use crate::scene::{Scene, WireModel};
|
use crate::scene::{Scene, WireModel};
|
||||||
|
use crate::scene::view::camera::Camera;
|
||||||
|
|
||||||
/// Longest edge of the generated thumbnail, in pixels.
|
/// Longest edge of the generated thumbnail, in pixels.
|
||||||
const MAX_DIM: u32 = 256;
|
const MAX_DIM: u32 = 256;
|
||||||
|
|
@ -31,6 +32,20 @@ const MAX_DIM: u32 = 256;
|
||||||
/// (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> {
|
pub fn from_scene(scene: &Scene, png: bool, viewport: (f32, f32)) -> Option<Preview> {
|
||||||
let wires = scene.entity_wires();
|
let wires = scene.entity_wires();
|
||||||
|
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
|
||||||
|
/// image encoding on the save worker instead of blocking the UI thread.
|
||||||
|
pub fn from_snapshot(
|
||||||
|
wires: &[WireModel],
|
||||||
|
camera: &Camera,
|
||||||
|
bg_color: [f32; 4],
|
||||||
|
png: bool,
|
||||||
|
viewport: (f32, f32),
|
||||||
|
) -> Option<Preview> {
|
||||||
if wires.is_empty() {
|
if wires.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -49,9 +64,8 @@ pub fn from_scene(scene: &Scene, png: bool, viewport: (f32, f32)) -> Option<Prev
|
||||||
width: cw as f32,
|
width: cw as f32,
|
||||||
height: ch as f32,
|
height: ch as f32,
|
||||||
};
|
};
|
||||||
let cam = scene.camera.borrow();
|
rasterize(wires, cw, ch, bg_color, png, |x, y, z| {
|
||||||
rasterize(&wires, cw, ch, scene.bg_color, png, |x, y, z| {
|
camera.project(glam::DVec3::new(x, y, z), bounds)
|
||||||
cam.project(glam::DVec3::new(x, y, z), bounds)
|
|
||||||
.map(|s| (s.x.round() as i32, s.y.round() as i32))
|
.map(|s| (s.x.round() as i32, s.y.round() as i32))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -944,34 +944,59 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||||
/// field but not `base`, so without this the saved group 10 goes stale and the
|
/// field but not `base`, so without this the saved group 10 goes stale and the
|
||||||
/// dimension's line / leader / origin jumps on reload (#181). Angular-2-line
|
/// dimension's line / leader / origin jumps on reload (#181). Angular-2-line
|
||||||
/// keeps a distinct base point (the second line's point) and is left alone.
|
/// keeps a distinct base point (the second line's point) and is left alone.
|
||||||
fn sync_dimension_base_points(doc: &mut CadDocument) {
|
fn sync_dimension_base_points_and_collect_pending(doc: &mut CadDocument) -> Vec<Handle> {
|
||||||
for e in doc.entities_mut() {
|
let existing_blocks: rustc_hash::FxHashSet<String> = doc
|
||||||
if let EntityType::Dimension(d) = e {
|
.block_records
|
||||||
let def = match d {
|
.iter()
|
||||||
Dimension::Linear(x) => Some(x.definition_point),
|
.map(|record| record.name.to_ascii_lowercase())
|
||||||
Dimension::Aligned(x) => Some(x.definition_point),
|
.collect();
|
||||||
Dimension::Radius(x) => Some(x.definition_point),
|
let dimensions: Vec<(Handle, Option<Vector3>, bool)> = doc
|
||||||
Dimension::Diameter(x) => Some(x.definition_point),
|
.entities()
|
||||||
Dimension::Ordinate(x) => Some(x.definition_point),
|
.filter_map(|entity| {
|
||||||
Dimension::Angular3Pt(x) => Some(x.definition_point),
|
let EntityType::Dimension(dimension) = entity else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let definition_point = match dimension {
|
||||||
|
Dimension::Linear(value) => Some(value.definition_point),
|
||||||
|
Dimension::Aligned(value) => Some(value.definition_point),
|
||||||
|
Dimension::Radius(value) => Some(value.definition_point),
|
||||||
|
Dimension::Diameter(value) => Some(value.definition_point),
|
||||||
|
Dimension::Ordinate(value) => Some(value.definition_point),
|
||||||
|
Dimension::Angular3Pt(value) => Some(value.definition_point),
|
||||||
Dimension::Angular2Ln(_) => None,
|
Dimension::Angular2Ln(_) => None,
|
||||||
};
|
};
|
||||||
if let Some(p) = def {
|
let base = dimension.base();
|
||||||
d.base_mut().definition_point = p;
|
let needs_sync =
|
||||||
|
definition_point.filter(|point| *point != base.definition_point);
|
||||||
|
let block_missing = base.block_name.trim().is_empty()
|
||||||
|
|| !existing_blocks.contains(&base.block_name.to_ascii_lowercase());
|
||||||
|
Some((base.common.handle, needs_sync, block_missing))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut pending = Vec::new();
|
||||||
|
for (handle, definition_point, block_missing) in dimensions {
|
||||||
|
if let Some(point) = definition_point {
|
||||||
|
if let Some(EntityType::Dimension(dimension)) = doc.get_entity_mut(handle) {
|
||||||
|
dimension.base_mut().definition_point = point;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if block_missing {
|
||||||
|
pending.push(handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
pending
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Smallest free `*D<n>` anonymous block name in `doc`.
|
/// Smallest free `*D<n>` anonymous block name in `doc`.
|
||||||
fn next_dimension_block_name(doc: &CadDocument) -> String {
|
fn next_dimension_block_name(doc: &CadDocument, next: &mut u64) -> String {
|
||||||
let mut n = 0u64;
|
|
||||||
loop {
|
loop {
|
||||||
|
let n = *next;
|
||||||
|
*next = (*next).saturating_add(1);
|
||||||
let cand = format!("*D{n}");
|
let cand = format!("*D{n}");
|
||||||
if doc.block_records.get(&cand).is_none() {
|
if doc.block_records.get(&cand).is_none() {
|
||||||
return cand;
|
return cand;
|
||||||
}
|
}
|
||||||
n += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -993,37 +1018,32 @@ fn next_dimension_block_name(doc: &CadDocument) -> String {
|
||||||
/// DWG, or copied via the `*D`-cloning copy path) are left untouched so their
|
/// DWG, or copied via the `*D`-cloning copy path) are left untouched so their
|
||||||
/// original graphics are preserved.
|
/// original graphics are preserved.
|
||||||
pub fn bake_dimension_blocks(doc: &mut CadDocument) {
|
pub fn bake_dimension_blocks(doc: &mut CadDocument) {
|
||||||
// Keep group-10 (base.definition_point) in step with the per-type geometry
|
// Keep group-10 in step and find missing blocks in one entity pass. Existing
|
||||||
// before writing — see sync_dimension_base_points.
|
// `*D` blocks are the save cache: only invalidated/new dimensions enter the
|
||||||
sync_dimension_base_points(doc);
|
// relatively expensive geometry generation below.
|
||||||
|
let pending = sync_dimension_base_points_and_collect_pending(doc);
|
||||||
// Handles of dimensions whose block reference is missing or dangling.
|
let dimensions: Vec<(Handle, Dimension)> = pending
|
||||||
let pending: Vec<Handle> = doc
|
.into_iter()
|
||||||
.entities()
|
.filter_map(|handle| match doc.get_entity(handle) {
|
||||||
.filter_map(|e| match e {
|
Some(EntityType::Dimension(d)) => Some((handle, d.clone())),
|
||||||
EntityType::Dimension(d) => {
|
|
||||||
let bn = &d.base().block_name;
|
|
||||||
if bn.trim().is_empty() || doc.block_records.get(bn).is_none() {
|
|
||||||
Some(d.base().common.handle)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for handle in pending {
|
use crate::par::prelude::*;
|
||||||
let dim = match doc.get_entity(handle) {
|
let doc_ref: &CadDocument = doc;
|
||||||
Some(EntityType::Dimension(d)) => d.clone(),
|
let generated: Vec<(Handle, Vec<EntityType>)> = dimensions
|
||||||
_ => continue,
|
.par_iter()
|
||||||
};
|
.map(|(handle, dim)| (*handle, explode_dimension(dim, doc_ref)))
|
||||||
let subs = explode_dimension(&dim, doc);
|
.collect();
|
||||||
|
|
||||||
|
let mut next_block_number = 0u64;
|
||||||
|
for (handle, subs) in generated {
|
||||||
if subs.is_empty() {
|
if subs.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = next_dimension_block_name(doc);
|
let name = next_dimension_block_name(doc, &mut next_block_number);
|
||||||
// Reserve three consecutive handles for the record / block / endblk.
|
// Reserve three consecutive handles for the record / block / endblk.
|
||||||
// Adding the block + endblk (which carry explicit handles) advances the
|
// Adding the block + endblk (which carry explicit handles) advances the
|
||||||
// document's handle counter past them, so the NULL-handle sub-entities
|
// document's handle counter past them, so the NULL-handle sub-entities
|
||||||
|
|
|
||||||
|
|
@ -479,6 +479,128 @@ pub struct InteractionHandleIndex {
|
||||||
handles: SpatialSet<u64>,
|
handles: SpatialSet<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct WireIndexEntries {
|
||||||
|
wire: Option<Entry3<u32>>,
|
||||||
|
segments: Vec<Entry3<SegmentRef>>,
|
||||||
|
snap_points: Vec<Entry3<SnapPointRef>>,
|
||||||
|
key_vertices: Vec<Entry3<KeyVertexRef>>,
|
||||||
|
key_segments: Vec<Entry3<KeySegmentRef>>,
|
||||||
|
fill_triangles: Vec<Entry3<TriangleRef>>,
|
||||||
|
pick_triangles: Vec<Entry3<TriangleRef>>,
|
||||||
|
glyphs: Vec<Entry3<GlyphRef>>,
|
||||||
|
unbounded: bool,
|
||||||
|
max_line_half_width_px: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntries {
|
||||||
|
let mut entries = WireIndexEntries {
|
||||||
|
wire: finite_wire_aabb3(wire).map(|aabb| Entry3 {
|
||||||
|
aabb,
|
||||||
|
value: wire_idx,
|
||||||
|
}),
|
||||||
|
segments: Vec::with_capacity(wire.points.len().saturating_sub(1)),
|
||||||
|
snap_points: Vec::with_capacity(wire.snap_pts.len()),
|
||||||
|
key_vertices: Vec::with_capacity(wire.key_vertices.len()),
|
||||||
|
key_segments: Vec::with_capacity(wire.key_vertices.len().saturating_sub(1)),
|
||||||
|
fill_triangles: Vec::with_capacity(wire.fill_tris.len() / 3),
|
||||||
|
pick_triangles: Vec::with_capacity(wire.pick_tris.len() / 3),
|
||||||
|
glyphs: Vec::with_capacity(wire.text_verts.len() / 6),
|
||||||
|
unbounded: false,
|
||||||
|
max_line_half_width_px: if wire.line_weight_px.is_finite() {
|
||||||
|
(wire.line_weight_px * 0.5).max(0.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
|
};
|
||||||
|
entries.unbounded = entries.wire.is_none();
|
||||||
|
|
||||||
|
for start in 0..wire.points.len().saturating_sub(1) {
|
||||||
|
let Some(aabb) = points_aabb3([
|
||||||
|
wire_point(wire, start),
|
||||||
|
wire_point(wire, start + 1),
|
||||||
|
]) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
entries.segments.push(Entry3 {
|
||||||
|
aabb,
|
||||||
|
value: SegmentRef {
|
||||||
|
wire: wire_idx,
|
||||||
|
start: start as u32,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (index, (point, _)) in wire.snap_pts.iter().enumerate() {
|
||||||
|
if point.is_finite() {
|
||||||
|
entries.snap_points.push(Entry3 {
|
||||||
|
aabb: [point.x, point.y, point.z, point.x, point.y, point.z],
|
||||||
|
value: SnapPointRef {
|
||||||
|
wire: wire_idx,
|
||||||
|
index: index as u32,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (index, &point) in wire.key_vertices.iter().enumerate() {
|
||||||
|
if point.iter().all(|value| value.is_finite()) {
|
||||||
|
entries.key_vertices.push(Entry3 {
|
||||||
|
aabb: [point[0], point[1], point[2], point[0], point[1], point[2]],
|
||||||
|
value: KeyVertexRef {
|
||||||
|
wire: wire_idx,
|
||||||
|
index: index as u32,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for start in 0..wire.key_vertices.len().saturating_sub(1) {
|
||||||
|
let Some(aabb) =
|
||||||
|
points_aabb3([wire.key_vertices[start], wire.key_vertices[start + 1]])
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
entries.key_segments.push(Entry3 {
|
||||||
|
aabb,
|
||||||
|
value: KeySegmentRef {
|
||||||
|
wire: wire_idx,
|
||||||
|
start: start as u32,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
append_triangle_entries(
|
||||||
|
wire_idx,
|
||||||
|
&wire.fill_tris,
|
||||||
|
&wire.fill_tris_low,
|
||||||
|
&mut entries.fill_triangles,
|
||||||
|
);
|
||||||
|
append_triangle_entries(
|
||||||
|
wire_idx,
|
||||||
|
&wire.pick_tris,
|
||||||
|
&wire.pick_tris_low,
|
||||||
|
&mut entries.pick_triangles,
|
||||||
|
);
|
||||||
|
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||||
|
let Some(quad) = wire.text_verts.get(start..start + 6) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let Some(aabb) = points_aabb3(quad.iter().map(|vertex| {
|
||||||
|
[
|
||||||
|
vertex.pos[0] as f64 + vertex.pos_low[0] as f64,
|
||||||
|
vertex.pos[1] as f64 + vertex.pos_low[1] as f64,
|
||||||
|
vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
|
||||||
|
]
|
||||||
|
})) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
entries.glyphs.push(Entry3 {
|
||||||
|
aabb,
|
||||||
|
value: GlyphRef {
|
||||||
|
wire: wire_idx,
|
||||||
|
start: start as u32,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
@ -554,106 +676,76 @@ impl InteractionIndex {
|
||||||
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;
|
||||||
|
|
||||||
for (wire_idx, wire) in wires.iter().enumerate() {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if wire.line_weight_px.is_finite() {
|
let per_wire: Vec<WireIndexEntries> = {
|
||||||
max_line_half_width_px =
|
use crate::par::prelude::*;
|
||||||
max_line_half_width_px.max((wire.line_weight_px * 0.5).max(0.0));
|
wires
|
||||||
}
|
.par_iter()
|
||||||
let wire_idx = wire_idx as u32;
|
.enumerate()
|
||||||
if let Some(aabb) = finite_wire_aabb3(wire) {
|
.map(|(index, wire)| collect_wire_index_entries(index as u32, wire))
|
||||||
wire_entries.push(Entry3 {
|
.collect()
|
||||||
aabb,
|
};
|
||||||
value: wire_idx,
|
#[cfg(target_arch = "wasm32")]
|
||||||
});
|
let per_wire: Vec<WireIndexEntries> = wires
|
||||||
} else {
|
.iter()
|
||||||
unbounded_wires.push(wire_idx);
|
.enumerate()
|
||||||
}
|
.map(|(index, wire)| collect_wire_index_entries(index as u32, wire))
|
||||||
|
.collect();
|
||||||
|
|
||||||
for start in 0..wire.points.len().saturating_sub(1) {
|
// Every per-wire worker knows its exact output sizes. Reserve the flat
|
||||||
let a = wire_point(wire, start);
|
// arrays once before draining them so a dense block drawing does not
|
||||||
let b = wire_point(wire, start + 1);
|
// repeatedly copy already-flattened entries while the Vecs grow.
|
||||||
let Some(aabb) = points_aabb3([a, b]) else {
|
wire_entries.reserve(per_wire.iter().filter(|entries| entries.wire.is_some()).count());
|
||||||
continue;
|
unbounded_wires.reserve(per_wire.iter().filter(|entries| entries.unbounded).count());
|
||||||
};
|
segment_entries.reserve(per_wire.iter().map(|entries| entries.segments.len()).sum());
|
||||||
segment_entries.push(Entry3 {
|
snap_point_entries.reserve(
|
||||||
aabb,
|
per_wire
|
||||||
value: SegmentRef {
|
.iter()
|
||||||
wire: wire_idx,
|
.map(|entries| entries.snap_points.len())
|
||||||
start: start as u32,
|
.sum(),
|
||||||
},
|
);
|
||||||
});
|
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(entries.max_line_half_width_px);
|
||||||
|
if let Some(entry) = entries.wire {
|
||||||
|
wire_entries.push(entry);
|
||||||
}
|
}
|
||||||
for (index, (point, _)) in wire.snap_pts.iter().enumerate() {
|
if entries.unbounded {
|
||||||
if !point.is_finite() {
|
unbounded_wires.push(wire_idx as u32);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
snap_point_entries.push(Entry3 {
|
|
||||||
aabb: [point.x, point.y, point.z, point.x, point.y, point.z],
|
|
||||||
value: SnapPointRef {
|
|
||||||
wire: wire_idx,
|
|
||||||
index: index as u32,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (index, &point) in wire.key_vertices.iter().enumerate() {
|
|
||||||
if !point.iter().all(|value| value.is_finite()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
key_vertex_entries.push(Entry3 {
|
|
||||||
aabb: [point[0], point[1], point[2], point[0], point[1], point[2]],
|
|
||||||
value: KeyVertexRef {
|
|
||||||
wire: wire_idx,
|
|
||||||
index: index as u32,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for start in 0..wire.key_vertices.len().saturating_sub(1) {
|
|
||||||
let Some(aabb) =
|
|
||||||
points_aabb3([wire.key_vertices[start], wire.key_vertices[start + 1]])
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
key_segment_entries.push(Entry3 {
|
|
||||||
aabb,
|
|
||||||
value: KeySegmentRef {
|
|
||||||
wire: wire_idx,
|
|
||||||
start: start as u32,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
append_triangle_entries(
|
|
||||||
wire_idx,
|
|
||||||
&wire.fill_tris,
|
|
||||||
&wire.fill_tris_low,
|
|
||||||
&mut fill_triangle_entries,
|
|
||||||
);
|
|
||||||
append_triangle_entries(
|
|
||||||
wire_idx,
|
|
||||||
&wire.pick_tris,
|
|
||||||
&wire.pick_tris_low,
|
|
||||||
&mut pick_triangle_entries,
|
|
||||||
);
|
|
||||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
|
||||||
let Some(quad) = wire.text_verts.get(start..start + 6) else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let Some(aabb) = points_aabb3(quad.iter().map(|vertex| {
|
|
||||||
[
|
|
||||||
vertex.pos[0] as f64 + vertex.pos_low[0] as f64,
|
|
||||||
vertex.pos[1] as f64 + vertex.pos_low[1] as f64,
|
|
||||||
vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
|
|
||||||
]
|
|
||||||
})) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
glyph_entries.push(Entry3 {
|
|
||||||
aabb,
|
|
||||||
value: GlyphRef {
|
|
||||||
wire: wire_idx,
|
|
||||||
start: start as u32,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
segment_entries.append(&mut entries.segments);
|
||||||
|
snap_point_entries.append(&mut entries.snap_points);
|
||||||
|
key_vertex_entries.append(&mut entries.key_vertices);
|
||||||
|
key_segment_entries.append(&mut entries.key_segments);
|
||||||
|
fill_triangle_entries.append(&mut entries.fill_triangles);
|
||||||
|
pick_triangle_entries.append(&mut entries.pick_triangles);
|
||||||
|
glyph_entries.append(&mut entries.glyphs);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,9 @@ pub struct Pipeline {
|
||||||
/// ref-counted, so N paper viewports (or Model tiles) drawing one identical
|
/// ref-counted, so N paper viewports (or Model tiles) drawing one identical
|
||||||
/// resident set hold one copy of the GPU vertex buffers between them —
|
/// resident set hold one copy of the GPU vertex buffers between them —
|
||||||
/// their camera uniforms + scissor stay per-slot, only the geometry is
|
/// their camera uniforms + scissor stay per-slot, only the geometry is
|
||||||
/// deduplicated. Never mutated in place, only reassigned on content change.
|
/// deduplicated. Never mutated in place; an arena-backed slot may also
|
||||||
|
/// replace this thin draw-range list on camera changes without touching the
|
||||||
|
/// shared resident buffer.
|
||||||
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
|
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
|
||||||
/// Persistent per-entity wire instance arena (native, `OCS_WIRE_GPU_PATCH`).
|
/// Persistent per-entity wire instance arena (native, `OCS_WIRE_GPU_PATCH`).
|
||||||
/// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an
|
/// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an
|
||||||
|
|
@ -164,6 +166,10 @@ pub struct Pipeline {
|
||||||
/// 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,
|
||||||
|
/// Last content/camera/viewport tuple used to derive visible instance
|
||||||
|
/// ranges from the resident arena.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(crate) wire_cull_key: (u64, u64, u32, u32),
|
||||||
/// This content viewport's non-rectangular clip boundary as a triangle-fan
|
/// This content viewport's non-rectangular clip boundary as a triangle-fan
|
||||||
/// vertex buffer in the render target's normalized device coords (`None` =
|
/// vertex buffer in the render target's normalized device coords (`None` =
|
||||||
/// rectangular / unclipped, where the viewport's own render rectangle does
|
/// rectangular / unclipped, where the viewport's own render rectangle does
|
||||||
|
|
@ -1450,6 +1456,8 @@ impl Pipeline {
|
||||||
wire_arena_mesh: None,
|
wire_arena_mesh: None,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
wire_arena_id: u64::MAX,
|
wire_arena_id: u64::MAX,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
wire_cull_key: (u64::MAX, u64::MAX, 0, 0),
|
||||||
clip_boundary: None,
|
clip_boundary: None,
|
||||||
gpu_selected_wires: vec![],
|
gpu_selected_wires: vec![],
|
||||||
gpu_preview_wires: vec![],
|
gpu_preview_wires: vec![],
|
||||||
|
|
@ -2572,7 +2580,10 @@ impl Pipeline {
|
||||||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||||
}
|
}
|
||||||
pass.set_vertex_buffer(0, edges.instance_buffer.slice(..));
|
pass.set_vertex_buffer(0, edges.instance_buffer.slice(..));
|
||||||
pass.draw(0..6, 0..edges.instance_count);
|
pass.draw(
|
||||||
|
0..6,
|
||||||
|
edges.first_instance..edges.first_instance + edges.instance_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2638,7 +2649,10 @@ impl Pipeline {
|
||||||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||||
}
|
}
|
||||||
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
||||||
pass.draw(0..6, 0..wire.instance_count);
|
pass.draw(
|
||||||
|
0..6,
|
||||||
|
wire.first_instance..wire.first_instance + wire.instance_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Live overlay wires (command preview / interim / grip drag) always
|
// Live overlay wires (command preview / interim / grip drag) always
|
||||||
// on top: the xray pipeline (depth_compare=Always, no depth write)
|
// on top: the xray pipeline (depth_compare=Always, no depth write)
|
||||||
|
|
@ -2653,7 +2667,10 @@ impl Pipeline {
|
||||||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||||
}
|
}
|
||||||
pass.set_vertex_buffer(0, pw.instance_buffer.slice(..));
|
pass.set_vertex_buffer(0, pw.instance_buffer.slice(..));
|
||||||
pass.draw(0..6, 0..pw.instance_count);
|
pass.draw(
|
||||||
|
0..6,
|
||||||
|
pw.first_instance..pw.first_instance + pw.instance_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2791,7 +2808,10 @@ impl Pipeline {
|
||||||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||||
}
|
}
|
||||||
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
||||||
pass.draw(0..6, 0..wire.instance_count);
|
pass.draw(
|
||||||
|
0..6,
|
||||||
|
wire.first_instance..wire.first_instance + wire.instance_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,9 @@ struct Slab {
|
||||||
inst_len: u32,
|
inst_len: u32,
|
||||||
const_off: u32,
|
const_off: u32,
|
||||||
const_len: u32,
|
const_len: u32,
|
||||||
|
/// World-XY bounds for plan-view draw-range culling. Unbounded whenever a
|
||||||
|
/// source wire does not carry a trustworthy entity AABB.
|
||||||
|
aabb: [f32; 4],
|
||||||
/// Entity-level draw depth used when this slab was emitted. Individual
|
/// Entity-level draw depth used when this slab was emitted. Individual
|
||||||
/// consts may carry block-local offsets around it; structural edits shift
|
/// consts may carry block-local offsets around it; structural edits shift
|
||||||
/// the whole slab by the base-depth delta instead of flattening those
|
/// the whole slab by the base-depth delta instead of flattening those
|
||||||
|
|
@ -182,6 +185,37 @@ fn handle_ranges(wires: &[&WireModel]) -> Option<Vec<(Handle, usize, usize)>> {
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_aabb(wires: &[&WireModel]) -> [f32; 4] {
|
||||||
|
let mut out = [
|
||||||
|
f32::INFINITY,
|
||||||
|
f32::INFINITY,
|
||||||
|
f32::NEG_INFINITY,
|
||||||
|
f32::NEG_INFINITY,
|
||||||
|
];
|
||||||
|
for wire in wires {
|
||||||
|
let [x0, y0, x1, y1] = wire.aabb;
|
||||||
|
if !x0.is_finite()
|
||||||
|
|| !y0.is_finite()
|
||||||
|
|| !x1.is_finite()
|
||||||
|
|| !y1.is_finite()
|
||||||
|
|| x0 > x1
|
||||||
|
|| y0 > y1
|
||||||
|
{
|
||||||
|
return WireModel::UNBOUNDED_AABB;
|
||||||
|
}
|
||||||
|
let pad = (wire.world_width * 0.5).max(0.0);
|
||||||
|
out[0] = out[0].min(x0 - pad);
|
||||||
|
out[1] = out[1].min(y0 - pad);
|
||||||
|
out[2] = out[2].max(x1 + pad);
|
||||||
|
out[3] = out[3].max(y1 + pad);
|
||||||
|
}
|
||||||
|
if out[0].is_finite() {
|
||||||
|
out
|
||||||
|
} else {
|
||||||
|
WireModel::UNBOUNDED_AABB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn make_const_bg(
|
fn make_const_bg(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
bgl: &wgpu::BindGroupLayout,
|
bgl: &wgpu::BindGroupLayout,
|
||||||
|
|
@ -197,22 +231,50 @@ fn make_const_bg(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alloc_inst(device: &wgpu::Device, cap: u64) -> wgpu::Buffer {
|
fn alloc_inst_initialized(
|
||||||
device.create_buffer(&wgpu::BufferDescriptor {
|
device: &wgpu::Device,
|
||||||
|
cap: u64,
|
||||||
|
data: &[WireInstance],
|
||||||
|
) -> wgpu::Buffer {
|
||||||
|
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("wire_arena.ibuf"),
|
label: Some("wire_arena.ibuf"),
|
||||||
size: cap * std::mem::size_of::<WireInstance>() as u64,
|
size: cap * std::mem::size_of::<WireInstance>() as u64,
|
||||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
mapped_at_creation: false,
|
mapped_at_creation: true,
|
||||||
})
|
});
|
||||||
|
if !data.is_empty() {
|
||||||
|
let bytes = bytemuck::cast_slice(data);
|
||||||
|
let mut mapped = buffer
|
||||||
|
.slice(..bytes.len() as u64)
|
||||||
|
.get_mapped_range_mut();
|
||||||
|
mapped.copy_from_slice(bytes);
|
||||||
|
drop(mapped);
|
||||||
|
}
|
||||||
|
buffer.unmap();
|
||||||
|
buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alloc_const(device: &wgpu::Device, cap: u64) -> wgpu::Buffer {
|
fn alloc_const_initialized(
|
||||||
device.create_buffer(&wgpu::BufferDescriptor {
|
device: &wgpu::Device,
|
||||||
|
cap: u64,
|
||||||
|
data: &[WireConst],
|
||||||
|
) -> wgpu::Buffer {
|
||||||
|
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("wire_arena.cbuf"),
|
label: Some("wire_arena.cbuf"),
|
||||||
size: cap * std::mem::size_of::<WireConst>() as u64,
|
size: cap * std::mem::size_of::<WireConst>() as u64,
|
||||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
mapped_at_creation: false,
|
mapped_at_creation: true,
|
||||||
})
|
});
|
||||||
|
if !data.is_empty() {
|
||||||
|
let bytes = bytemuck::cast_slice(data);
|
||||||
|
let mut mapped = buffer
|
||||||
|
.slice(..bytes.len() as u64)
|
||||||
|
.get_mapped_range_mut();
|
||||||
|
mapped.copy_from_slice(bytes);
|
||||||
|
drop(mapped);
|
||||||
|
}
|
||||||
|
buffer.unmap();
|
||||||
|
buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
fn blank_const() -> WireConst {
|
fn blank_const() -> WireConst {
|
||||||
|
|
@ -230,8 +292,7 @@ fn blank_instance() -> WireInstance {
|
||||||
distance_a: 0.0,
|
distance_a: 0.0,
|
||||||
distance_b: 0.0,
|
distance_b: 0.0,
|
||||||
wire_id: 0,
|
wire_id: 0,
|
||||||
world_hw_a: 0.0,
|
taper_ratio: [0; 2],
|
||||||
world_hw_b: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,44 +302,128 @@ impl WireArena {
|
||||||
/// path).
|
/// path).
|
||||||
pub fn build(
|
pub fn build(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
queue: &wgpu::Queue,
|
_queue: &wgpu::Queue,
|
||||||
wires: &[&WireModel],
|
wires: &[&WireModel],
|
||||||
depth_map: &FxHashMap<u64, [f32; 2]>,
|
depth_map: &FxHashMap<u64, [f32; 2]>,
|
||||||
const_bgl: &wgpu::BindGroupLayout,
|
const_bgl: &wgpu::BindGroupLayout,
|
||||||
mesh_edge: bool,
|
mesh_edge: bool,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let ranges = handle_ranges(wires)?;
|
let ranges = handle_ranges(wires)?;
|
||||||
|
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||||
|
let total_started = std::time::Instant::now();
|
||||||
|
|
||||||
|
// Reject an oversized batch before parallel emission allocates hundreds
|
||||||
|
// of megabytes. `points.len() - 1` is an upper bound because NaN-break
|
||||||
|
// segments are skipped by emit_wire_native.
|
||||||
|
let max_instances: usize = wires
|
||||||
|
.iter()
|
||||||
|
.map(|w| w.points.len().saturating_sub(1))
|
||||||
|
.sum();
|
||||||
|
if max_instances as u64 > MAX_INSTANCES || wires.len() as u64 + 1 > MAX_CONSTS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BuildPlan {
|
||||||
|
handle: Handle,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
const_off: u32,
|
||||||
|
base_depth: f32,
|
||||||
|
}
|
||||||
|
struct PackedSlab {
|
||||||
|
handle: Handle,
|
||||||
|
const_off: u32,
|
||||||
|
base_depth: f32,
|
||||||
|
aabb: [f32; 4],
|
||||||
|
instances: Vec<WireInstance>,
|
||||||
|
consts: Vec<WireConst>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign global const slots serially so parallel workers can emit final
|
||||||
|
// wire_id values directly. Indexed parallel collect preserves handle and
|
||||||
|
// wire submission order.
|
||||||
|
let mut next_const = 1u32;
|
||||||
|
let plans: Vec<BuildPlan> = ranges
|
||||||
|
.into_iter()
|
||||||
|
.map(|(handle, start, end)| {
|
||||||
|
let const_off = next_const;
|
||||||
|
next_const += (end - start) as u32;
|
||||||
|
let base_depth = if mesh_edge {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
depth_map.get(&handle.value()).map_or(0.0, |d| d[0])
|
||||||
|
};
|
||||||
|
BuildPlan {
|
||||||
|
handle,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
const_off,
|
||||||
|
base_depth,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let pack_started = std::time::Instant::now();
|
||||||
|
use crate::par::prelude::*;
|
||||||
|
let packed: Vec<PackedSlab> = plans
|
||||||
|
.par_iter()
|
||||||
|
.map(|plan| {
|
||||||
|
let run = &wires[plan.start..plan.end];
|
||||||
|
let capacity: usize = run
|
||||||
|
.iter()
|
||||||
|
.map(|w| w.points.len().saturating_sub(1))
|
||||||
|
.sum();
|
||||||
|
let mut instances: Vec<WireInstance> = Vec::with_capacity(capacity);
|
||||||
|
let mut consts: Vec<WireConst> = Vec::with_capacity(run.len());
|
||||||
|
for (local, &w) in run.iter().enumerate() {
|
||||||
|
let wire_id = plan.const_off + local as u32;
|
||||||
|
// 3D mesh outline edges are occluded by true depth and must NOT
|
||||||
|
// take the draw-order z-bias (or hidden back edges peek through
|
||||||
|
// the shaded fill) — matching WireGpu::from_run.
|
||||||
|
let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
|
||||||
|
let (mut emitted, cst) = emit_wire_native(w, wire_id, w.color, dd);
|
||||||
|
instances.append(&mut emitted);
|
||||||
|
consts.push(cst);
|
||||||
|
}
|
||||||
|
PackedSlab {
|
||||||
|
handle: plan.handle,
|
||||||
|
const_off: plan.const_off,
|
||||||
|
base_depth: plan.base_depth,
|
||||||
|
aabb: run_aabb(run),
|
||||||
|
instances,
|
||||||
|
consts,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let pack_ms = pack_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
|
||||||
|
let inst_count: usize = packed.iter().map(|slab| slab.instances.len()).sum();
|
||||||
|
let const_count: usize = 1 + packed.iter().map(|slab| slab.consts.len()).sum::<usize>();
|
||||||
|
if inst_count as u64 > MAX_INSTANCES || const_count as u64 > MAX_CONSTS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// const slot 0 = blank tombstone target.
|
// const slot 0 = blank tombstone target.
|
||||||
let mut instances: Vec<WireInstance> = Vec::new();
|
let mut instances: Vec<WireInstance> = Vec::with_capacity(inst_count);
|
||||||
let mut consts_cpu: Vec<WireConst> = vec![blank_const()];
|
let mut consts_cpu: Vec<WireConst> = Vec::with_capacity(const_count);
|
||||||
let mut slabs: FxHashMap<Handle, Slab> = FxHashMap::default();
|
consts_cpu.push(blank_const());
|
||||||
for (h, i, j) in ranges {
|
let mut slabs: FxHashMap<Handle, Slab> =
|
||||||
|
FxHashMap::with_capacity_and_hasher(packed.len(), Default::default());
|
||||||
|
for mut packed_slab in packed {
|
||||||
let inst_off = instances.len() as u32;
|
let inst_off = instances.len() as u32;
|
||||||
let const_off = consts_cpu.len() as u32;
|
let inst_len = packed_slab.instances.len() as u32;
|
||||||
let base_depth = if mesh_edge {
|
let const_len = packed_slab.consts.len() as u32;
|
||||||
0.0
|
instances.append(&mut packed_slab.instances);
|
||||||
} else {
|
consts_cpu.append(&mut packed_slab.consts);
|
||||||
depth_map.get(&h.value()).map_or(0.0, |d| d[0])
|
|
||||||
};
|
|
||||||
for &w in &wires[i..j] {
|
|
||||||
let wire_id = consts_cpu.len() as u32;
|
|
||||||
// 3D mesh outline edges are occluded by true depth and must NOT
|
|
||||||
// take the draw-order z-bias (or hidden back edges peek through
|
|
||||||
// the shaded fill) — matching WireGpu::from_run.
|
|
||||||
let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
|
|
||||||
let (mut insts, cst) = emit_wire_native(w, wire_id, w.color, dd);
|
|
||||||
instances.append(&mut insts);
|
|
||||||
consts_cpu.push(cst);
|
|
||||||
}
|
|
||||||
slabs.insert(
|
slabs.insert(
|
||||||
h,
|
packed_slab.handle,
|
||||||
Slab {
|
Slab {
|
||||||
inst_off,
|
inst_off,
|
||||||
inst_len: instances.len() as u32 - inst_off,
|
inst_len,
|
||||||
const_off,
|
const_off: packed_slab.const_off,
|
||||||
const_len: consts_cpu.len() as u32 - const_off,
|
const_len,
|
||||||
base_depth,
|
aabb: packed_slab.aabb,
|
||||||
|
base_depth: packed_slab.base_depth,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -296,13 +441,24 @@ impl WireArena {
|
||||||
let const_cap = ((const_tail as u64 * HEADROOM_NUM / HEADROOM_DEN)
|
let const_cap = ((const_tail as u64 * HEADROOM_NUM / HEADROOM_DEN)
|
||||||
.max(MIN_CONST_CAP)
|
.max(MIN_CONST_CAP)
|
||||||
.min(MAX_CONSTS)) as u32;
|
.min(MAX_CONSTS)) as u32;
|
||||||
let inst_buf = alloc_inst(device, inst_cap as u64);
|
let upload_started = std::time::Instant::now();
|
||||||
let const_buf = alloc_const(device, const_cap as u64);
|
let inst_buf = alloc_inst_initialized(device, inst_cap as u64, &instances);
|
||||||
if inst_tail > 0 {
|
let const_buf = alloc_const_initialized(device, const_cap as u64, &consts_cpu);
|
||||||
queue.write_buffer(&inst_buf, 0, bytemuck::cast_slice(&instances));
|
let upload_ms = upload_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
}
|
|
||||||
queue.write_buffer(&const_buf, 0, bytemuck::cast_slice(&consts_cpu));
|
|
||||||
let const_bind_group = make_const_bg(device, const_bgl, &const_buf);
|
let const_bind_group = make_const_bg(device, const_bgl, &const_buf);
|
||||||
|
if perf {
|
||||||
|
eprintln!(
|
||||||
|
"[perf] arena-build-detail total={:.1}ms pack={:.1} mapped-upload={:.1} handles={} wires={} instances={} instance-bytes={} consts={}",
|
||||||
|
total_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
pack_ms,
|
||||||
|
upload_ms,
|
||||||
|
slabs.len(),
|
||||||
|
wires.len(),
|
||||||
|
inst_tail,
|
||||||
|
inst_tail as usize * std::mem::size_of::<WireInstance>(),
|
||||||
|
const_tail,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
inst_buf,
|
inst_buf,
|
||||||
|
|
@ -381,6 +537,7 @@ impl WireArena {
|
||||||
} else {
|
} else {
|
||||||
depth_map.get(&h.value()).map_or(0.0, |d| d[0])
|
depth_map.get(&h.value()).map_or(0.0, |d| d[0])
|
||||||
};
|
};
|
||||||
|
let aabb = run_aabb(run);
|
||||||
|
|
||||||
if !self.slabs.contains_key(&h)
|
if !self.slabs.contains_key(&h)
|
||||||
&& self
|
&& self
|
||||||
|
|
@ -419,7 +576,9 @@ impl WireArena {
|
||||||
const_off as u64 * csz,
|
const_off as u64 * csz,
|
||||||
bytemuck::cast_slice(&csts),
|
bytemuck::cast_slice(&csts),
|
||||||
);
|
);
|
||||||
self.slabs.get_mut(&h).unwrap().base_depth = base_depth;
|
let slab = self.slabs.get_mut(&h).unwrap();
|
||||||
|
slab.base_depth = base_depth;
|
||||||
|
slab.aabb = aabb;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -465,6 +624,7 @@ impl WireArena {
|
||||||
inst_len,
|
inst_len,
|
||||||
const_off,
|
const_off,
|
||||||
const_len,
|
const_len,
|
||||||
|
aabb,
|
||||||
base_depth,
|
base_depth,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -509,9 +669,101 @@ impl WireArena {
|
||||||
}
|
}
|
||||||
vec![WireGpu {
|
vec![WireGpu {
|
||||||
instance_buffer: self.inst_buf.clone(),
|
instance_buffer: self.inst_buf.clone(),
|
||||||
|
first_instance: 0,
|
||||||
instance_count: self.inst_tail,
|
instance_count: self.inst_tail,
|
||||||
is_3d_mesh_edge: self.mesh_edge,
|
is_3d_mesh_edge: self.mesh_edge,
|
||||||
const_bind_group: Some(self.const_bind_group.clone()),
|
const_bind_group: Some(self.const_bind_group.clone()),
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw only plan-view entity ranges that can reach the viewport. The
|
||||||
|
/// instance buffer stays fully resident, so pan/zoom changes only this tiny
|
||||||
|
/// list of offsets — no repack and no GPU upload. Non-plan views use the
|
||||||
|
/// conservative full draw because a 2-D entity AABB does not contain Z.
|
||||||
|
pub fn wire_gpus_visible(
|
||||||
|
&self,
|
||||||
|
view_rot: glam::Mat4,
|
||||||
|
eye: glam::DVec3,
|
||||||
|
clip_w: u32,
|
||||||
|
clip_h: u32,
|
||||||
|
) -> Vec<WireGpu> {
|
||||||
|
if self.inst_tail == 0 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let projected_x = view_rot.transform_vector3(glam::Vec3::X);
|
||||||
|
let projected_y = view_rot.transform_vector3(glam::Vec3::Y);
|
||||||
|
let projected_z = view_rot.transform_vector3(glam::Vec3::Z);
|
||||||
|
let xy_scale = projected_x
|
||||||
|
.truncate()
|
||||||
|
.length()
|
||||||
|
.max(projected_y.truncate().length())
|
||||||
|
.max(f32::MIN_POSITIVE);
|
||||||
|
if projected_z.truncate().length() > xy_scale * 1e-5 {
|
||||||
|
return self.wire_gpus();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ranges: Vec<(u32, u32)> = self
|
||||||
|
.slabs
|
||||||
|
.values()
|
||||||
|
.filter(|slab| {
|
||||||
|
slab.inst_len > 0
|
||||||
|
&& !super::aabb_offscreen(slab.aabb, view_rot, eye, clip_w, clip_h)
|
||||||
|
})
|
||||||
|
.map(|slab| (slab.inst_off, slab.inst_off + slab.inst_len))
|
||||||
|
.collect();
|
||||||
|
ranges.sort_unstable_by_key(|range| range.0);
|
||||||
|
let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
|
||||||
|
for (start, end) in ranges {
|
||||||
|
if let Some((_, previous_end)) = merged.last_mut() {
|
||||||
|
if *previous_end == start {
|
||||||
|
*previous_end = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.push((start, end));
|
||||||
|
}
|
||||||
|
let mut ranges = merged;
|
||||||
|
if ranges.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap CPU draw-call overhead on pathologically interleaved draw order.
|
||||||
|
// Merging a few separated visible spans draws their offscreen gap too,
|
||||||
|
// but retains order and still avoids the rest of a multi-million
|
||||||
|
// instance drawing.
|
||||||
|
const MAX_RANGES: usize = 64;
|
||||||
|
if ranges.len() > MAX_RANGES {
|
||||||
|
let group = (ranges.len() + MAX_RANGES - 1) / MAX_RANGES;
|
||||||
|
ranges = ranges
|
||||||
|
.chunks(group)
|
||||||
|
.map(|chunk| (chunk[0].0, chunk[chunk.len() - 1].1))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if std::env::var_os("OCS_PERF").is_some() {
|
||||||
|
let submitted: u64 = ranges
|
||||||
|
.iter()
|
||||||
|
.map(|(start, end)| (end - start) as u64)
|
||||||
|
.sum();
|
||||||
|
if submitted < self.inst_tail as u64 {
|
||||||
|
eprintln!(
|
||||||
|
"[perf] wire-cull submitted={} resident={} ranges={}",
|
||||||
|
submitted,
|
||||||
|
self.inst_tail,
|
||||||
|
ranges.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges
|
||||||
|
.into_iter()
|
||||||
|
.map(|(start, end)| WireGpu {
|
||||||
|
instance_buffer: self.inst_buf.clone(),
|
||||||
|
first_instance: start,
|
||||||
|
instance_count: end - start,
|
||||||
|
is_3d_mesh_edge: self.mesh_edge,
|
||||||
|
const_bind_group: Some(self.const_bind_group.clone()),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,10 @@ fn instance_buffer_mapped<T: bytemuck::Pod>(
|
||||||
// even though it's constant along the wire. On native we hoist those into a
|
// even though it's constant along the wire. On native we hoist those into a
|
||||||
// per-wire `WireConst` storage buffer indexed by `wire_id`, so the instance
|
// per-wire `WireConst` storage buffer indexed by `wire_id`, so the instance
|
||||||
// keeps only the per-segment data (endpoints + arc-length distances). Cuts the
|
// keeps only the per-segment data (endpoints + arc-length distances). Cuts the
|
||||||
// instance from 104 B to 60 B (~42 %) and removes the redundant per-segment
|
// instance from 104 B to one 64-byte cache line and removes the redundant
|
||||||
// re-fetch of the shared constants. WebGL2 has no vertex-stage storage buffers,
|
// per-segment re-fetch of the shared constants. WebGL2 has no vertex-stage
|
||||||
// so the wasm build below keeps the original self-contained fat instance.
|
// storage buffers, so the wasm build below keeps the original self-contained
|
||||||
|
// fat instance.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
|
@ -80,11 +81,11 @@ pub struct WireInstance {
|
||||||
pub distance_b: f32,
|
pub distance_b: f32,
|
||||||
/// Index into the per-wire `WireConst` storage buffer (group 1).
|
/// Index into the per-wire `WireConst` storage buffer (group 1).
|
||||||
pub wire_id: u32,
|
pub wire_id: u32,
|
||||||
/// World-space half-width at each endpoint for a TAPERED band. `0.0` =
|
/// Endpoint width / the per-wire maximum width, normalized by the vertex
|
||||||
/// use the per-wire constant (`WireConst.world_half_width`). The shader
|
/// fetch unit. `[0, 0]` means use the constant width. Ratios retain the
|
||||||
/// interpolates between the two so the band tapers across the segment.
|
/// full f32 world-width scale in `WireConst` while making every instance
|
||||||
pub world_hw_a: f32,
|
/// exactly one 64-byte cache line.
|
||||||
pub world_hw_b: f32,
|
pub taper_ratio: [u16; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -99,8 +100,7 @@ impl WireInstance {
|
||||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 },
|
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 },
|
||||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
|
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
|
||||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, wire_id) as u64, shader_location: 6, format: wgpu::VertexFormat::Uint32 },
|
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, wire_id) as u64, shader_location: 6, format: wgpu::VertexFormat::Uint32 },
|
||||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_a) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32 },
|
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, taper_ratio) as u64, shader_location: 7, format: wgpu::VertexFormat::Unorm16x2 },
|
||||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_b) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32 },
|
|
||||||
];
|
];
|
||||||
wgpu::VertexBufferLayout {
|
wgpu::VertexBufferLayout {
|
||||||
array_stride: std::mem::size_of::<WireInstance>() as u64,
|
array_stride: std::mem::size_of::<WireInstance>() as u64,
|
||||||
|
|
@ -284,6 +284,8 @@ impl WirePipelineMode {
|
||||||
|
|
||||||
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.
|
||||||
|
pub first_instance: u32,
|
||||||
pub instance_count: u32,
|
pub instance_count: u32,
|
||||||
/// `true` when the source `WireModel` also carries `fill_tris`
|
/// `true` when the source `WireModel` also carries `fill_tris`
|
||||||
/// (i.e. it is a 3D mesh face — PolyfaceMesh / PolygonMesh — whose
|
/// (i.e. it is a 3D mesh face — PolyfaceMesh / PolygonMesh — whose
|
||||||
|
|
@ -520,9 +522,20 @@ pub(crate) fn emit_wire_native(
|
||||||
return (Vec::new(), cst);
|
return (Vec::new(), cst);
|
||||||
}
|
}
|
||||||
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
||||||
// Per-point half-width for a tapered band (empty ⇒ 0 ⇒ shader uses the
|
// Store an endpoint/max-width ratio. The shared f32 maximum keeps drawing
|
||||||
// per-wire constant width).
|
// units and range out of the packed field; UNORM16 contributes only a
|
||||||
let tw = |i: usize| -> f32 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 };
|
// relative error below 1/65535. Preserve zero as the existing constant
|
||||||
|
// width fallback sentinel.
|
||||||
|
let taper_ratio = |i: usize| -> u16 {
|
||||||
|
let width = wire.taper_widths.get(i).copied().unwrap_or(0.0);
|
||||||
|
if width <= 0.0 || wire.world_width <= 0.0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
((width / wire.world_width).clamp(0.0, 1.0) * u16::MAX as f32)
|
||||||
|
.round()
|
||||||
|
.max(1.0) as u16
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
|
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
|
||||||
for i in 0..seg_count {
|
for i in 0..seg_count {
|
||||||
let a = wire.points[i];
|
let a = wire.points[i];
|
||||||
|
|
@ -538,8 +551,7 @@ pub(crate) fn emit_wire_native(
|
||||||
distance_a: dists[i],
|
distance_a: dists[i],
|
||||||
distance_b: dists[i + 1],
|
distance_b: dists[i + 1],
|
||||||
wire_id,
|
wire_id,
|
||||||
world_hw_a: tw(i),
|
taper_ratio: [taper_ratio(i), taper_ratio(i + 1)],
|
||||||
world_hw_b: tw(i + 1),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
(instances, cst)
|
(instances, cst)
|
||||||
|
|
@ -652,6 +664,7 @@ impl WireGpu {
|
||||||
let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk);
|
let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk);
|
||||||
Self {
|
Self {
|
||||||
instance_buffer: buf,
|
instance_buffer: buf,
|
||||||
|
first_instance: 0,
|
||||||
instance_count: chunk.len() as u32,
|
instance_count: chunk.len() as u32,
|
||||||
is_3d_mesh_edge: mesh_edge,
|
is_3d_mesh_edge: mesh_edge,
|
||||||
const_bind_group: Some(bg.clone()),
|
const_bind_group: Some(bg.clone()),
|
||||||
|
|
@ -684,6 +697,7 @@ impl WireGpu {
|
||||||
let buf = instance_buffer_mapped(device, "wire.run.compat.ibuf", chunk);
|
let buf = instance_buffer_mapped(device, "wire.run.compat.ibuf", chunk);
|
||||||
Self {
|
Self {
|
||||||
instance_buffer: buf,
|
instance_buffer: buf,
|
||||||
|
first_instance: 0,
|
||||||
instance_count: chunk.len() as u32,
|
instance_count: chunk.len() as u32,
|
||||||
is_3d_mesh_edge: mesh_edge,
|
is_3d_mesh_edge: mesh_edge,
|
||||||
const_bind_group: None,
|
const_bind_group: None,
|
||||||
|
|
@ -740,6 +754,7 @@ impl WireGpu {
|
||||||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||||
Self {
|
Self {
|
||||||
instance_buffer,
|
instance_buffer,
|
||||||
|
first_instance: 0,
|
||||||
instance_count: chunk.len() as u32,
|
instance_count: chunk.len() as u32,
|
||||||
is_3d_mesh_edge: false,
|
is_3d_mesh_edge: false,
|
||||||
const_bind_group: Some(bg.clone()),
|
const_bind_group: Some(bg.clone()),
|
||||||
|
|
@ -766,6 +781,7 @@ impl WireGpu {
|
||||||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||||
Self {
|
Self {
|
||||||
instance_buffer,
|
instance_buffer,
|
||||||
|
first_instance: 0,
|
||||||
instance_count: chunk.len() as u32,
|
instance_count: chunk.len() as u32,
|
||||||
is_3d_mesh_edge: false,
|
is_3d_mesh_edge: false,
|
||||||
const_bind_group: None,
|
const_bind_group: None,
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,10 @@ impl shader::Primitive for Primitive {
|
||||||
inner.cached_mesh_source = None;
|
inner.cached_mesh_source = None;
|
||||||
inner.cached_face3d_source = None;
|
inner.cached_face3d_source = None;
|
||||||
inner.cached_face3d_depth_source = None;
|
inner.cached_face3d_depth_source = None;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
inner.wire_cull_key = (u64::MAX, u64::MAX, 0, 0);
|
||||||
|
}
|
||||||
inner.render_sig = u64::MAX;
|
inner.render_sig = u64::MAX;
|
||||||
}
|
}
|
||||||
// The MSAA / depth / resolve textures are always sized to the
|
// The MSAA / depth / resolve textures are always sized to the
|
||||||
|
|
@ -709,6 +713,41 @@ impl shader::Primitive for Primitive {
|
||||||
inner.compute_hatch_lod(queue, view_rot, eye, clip_size.width, clip_size.height);
|
inner.compute_hatch_lod(queue, view_rot, eye, clip_size.width, clip_size.height);
|
||||||
inner.compute_wipeout_lod(view_rot, eye, clip_size.width, clip_size.height);
|
inner.compute_wipeout_lod(view_rot, eye, clip_size.width, clip_size.height);
|
||||||
inner.compute_mesh_lod(view_rot, eye, clip_size.width, clip_size.height);
|
inner.compute_mesh_lod(view_rot, eye, clip_size.width, clip_size.height);
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let cull_key = (
|
||||||
|
vp.wire_content_id,
|
||||||
|
vp.camera_generation,
|
||||||
|
clip_size.width,
|
||||||
|
clip_size.height,
|
||||||
|
);
|
||||||
|
if inner.wire_arena_id == vp.wire_content_id
|
||||||
|
&& inner.wire_cull_key != cull_key
|
||||||
|
{
|
||||||
|
let mut visible = inner
|
||||||
|
.wire_arena
|
||||||
|
.as_ref()
|
||||||
|
.map(|arena| {
|
||||||
|
arena.wire_gpus_visible(
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
clip_size.width,
|
||||||
|
clip_size.height,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some(arena) = inner.wire_arena_mesh.as_ref() {
|
||||||
|
visible.extend(arena.wire_gpus_visible(
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
clip_size.width,
|
||||||
|
clip_size.height,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
inner.gpu_wires = std::sync::Arc::new(visible);
|
||||||
|
inner.wire_cull_key = cull_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
if vp.show_viewcube {
|
if vp.show_viewcube {
|
||||||
inner.viewcube.upload(
|
inner.viewcube.upload(
|
||||||
queue,
|
queue,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// Wire shader (native) — same as wire.wgsl, but the per-wire constants
|
// Wire shader (native) — same as wire.wgsl, but the per-wire constants
|
||||||
// (color / half_width / dash pattern / draw_depth) live in a per-wire storage
|
// (color / half_width / dash pattern / draw_depth) live in a per-wire storage
|
||||||
// buffer indexed by `wire_id` instead of being replicated on every segment
|
// buffer indexed by `wire_id` instead of being replicated on every segment
|
||||||
// instance. Cuts the instance from 104 B to 60 B and removes the redundant
|
// instance. Cuts the instance from 104 B to one 64-byte cache line and removes
|
||||||
// per-segment re-fetch of constants. WebGL2 has no vertex-stage storage
|
// the redundant per-segment re-fetch of constants. WebGL2 has no vertex-stage
|
||||||
// buffers, so the wasm build uses wire.wgsl (fat instance) instead.
|
// storage buffers, so the wasm build uses wire.wgsl (fat instance) instead.
|
||||||
|
|
||||||
struct Uniforms {
|
struct Uniforms {
|
||||||
viewport_size: vec2<f32>,
|
viewport_size: vec2<f32>,
|
||||||
|
|
@ -44,10 +44,9 @@ struct InstanceIn {
|
||||||
@location(4) distance_a: f32,
|
@location(4) distance_a: f32,
|
||||||
@location(5) distance_b: f32,
|
@location(5) distance_b: f32,
|
||||||
@location(6) wire_id: u32,
|
@location(6) wire_id: u32,
|
||||||
// Per-endpoint world half-width for a tapered band (0 = use the per-wire
|
// Per-endpoint width / per-wire maximum width. Vertex UNORM16 conversion
|
||||||
// constant `world_half_width`).
|
// expands this to 0..1; zero keeps the constant-width fallback.
|
||||||
@location(7) world_hw_a: f32,
|
@location(7) taper_ratio: vec2<f32>,
|
||||||
@location(8) world_hw_b: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DRAW_ORDER_BIAS: f32 = 0.001;
|
const DRAW_ORDER_BIAS: f32 = 0.001;
|
||||||
|
|
@ -73,8 +72,10 @@ struct VertexOut {
|
||||||
// Half-width of one segment end: a tapered band's own end width wins, then a
|
// Half-width of one segment end: a tapered band's own end width wins, then a
|
||||||
// constant world-unit band, then the screen-pixel lineweight (LWDISPLAY off
|
// constant world-unit band, then the screen-pixel lineweight (LWDISPLAY off
|
||||||
// collapses to a hairline).
|
// collapses to a hairline).
|
||||||
fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 {
|
fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
|
||||||
if taper > 0.0 { return max(taper / u.world_per_pixel, 0.5); }
|
if taper_ratio > 0.0 {
|
||||||
|
return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5);
|
||||||
|
}
|
||||||
if world_hw > 0.0 { return max(world_hw / u.world_per_pixel, 0.5); }
|
if world_hw > 0.0 { return max(world_hw / u.world_per_pixel, 0.5); }
|
||||||
return select(0.5, px_hw, u.lwdisplay_enable > 0.5);
|
return select(0.5, px_hw, u.lwdisplay_enable > 0.5);
|
||||||
}
|
}
|
||||||
|
|
@ -114,12 +115,12 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 {
|
||||||
// by `world_half_width / world_per_pixel` (screen pixels) so the band grows
|
// by `world_half_width / world_per_pixel` (screen pixels) so the band grows
|
||||||
// and shrinks with zoom. A normal wire (world_half_width == 0) uses the
|
// and shrinks with zoom. A normal wire (world_half_width == 0) uses the
|
||||||
// screen-pixel half-width, honouring the LWDISPLAY toggle.
|
// screen-pixel half-width, honouring the LWDISPLAY toggle.
|
||||||
// A tapered band carries a per-endpoint world half-width on the instance:
|
// A tapered band carries normalized endpoint widths on the instance:
|
||||||
// interpolate across the segment so the band narrows/widens smoothly. A
|
// interpolate across the segment so the band narrows/widens smoothly. A
|
||||||
// constant band uses the per-wire `world_half_width`. Both clamp to a
|
// constant band uses the per-wire `world_half_width`. Both clamp to a
|
||||||
// half-pixel so a zoomed-out band stays a hairline instead of vanishing.
|
// half-pixel so a zoomed-out band stays a hairline instead of vanishing.
|
||||||
let hw_a = resolve_hw(in.world_hw_a, c.world_half_width, c.half_width);
|
let hw_a = resolve_hw(in.taper_ratio.x, c.world_half_width, c.half_width);
|
||||||
let hw_b = resolve_hw(in.world_hw_b, c.world_half_width, c.half_width);
|
let hw_b = resolve_hw(in.taper_ratio.y, c.world_half_width, c.half_width);
|
||||||
let hw = mix(hw_a, hw_b, which_end);
|
let hw = mix(hw_a, hw_b, which_end);
|
||||||
|
|
||||||
// Extend the quad longitudinally by the end half-width and let the
|
// Extend the quad longitudinally by the end half-width and let the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue