forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'fork/codex/polysolid-parity' into codex/polysolid-parity
# Conflicts: # Cargo.lock # Cargo.toml # src/scene/model/solid_history.rs
This commit is contained in:
commit
529ecd8cd6
28 files changed
+3047
-499
No files matched your search
Generated
+1
-1
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=39720e3ac907307a653e93ee654afa63b7e4aa76#39720e3ac907307a653e93ee654afa63b7e4aa76"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=349823877944b17d5909e547e900c6f5d85e087b#349823877944b17d5909e547e900c6f5d85e087b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ rfd = "0.17"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "a0f7d444f1607bc4b2c881060cbe7ea1014253cb", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "a0f7d444f1607bc4b2c881060cbe7ea1014253cb", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "39720e3ac907307a653e93ee654afa63b7e4aa76", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "349823877944b17d5909e547e900c6f5d85e087b", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -960,6 +960,20 @@ pub(super) struct OpenCADStudio {
|
|||||||
shortcut_bindings: rustc_hash::FxHashMap<String, String>,
|
shortcut_bindings: rustc_hash::FxHashMap<String, String>,
|
||||||
/// Working rows shown by the shortcut editor until Apply is pressed.
|
/// Working rows shown by the shortcut editor until Apply is pressed.
|
||||||
shortcut_editor_rows: Vec<(String, String)>,
|
shortcut_editor_rows: Vec<(String, String)>,
|
||||||
|
/// Row whose Key cell is armed for capture: the next key combination
|
||||||
|
/// pressed anywhere fills that row's key. While set, the keyboard
|
||||||
|
/// subscription swallows every key so captured shortcuts do not run in
|
||||||
|
/// the drawing.
|
||||||
|
shortcut_capture_row: Option<usize>,
|
||||||
|
/// True while a freshly added (top) row is an unfinished draft: it exists
|
||||||
|
/// only until its key and command are filled (success) or it is cancelled
|
||||||
|
/// (Esc / ✕ / dialog close), so the list never keeps an empty row.
|
||||||
|
shortcut_pending_add: bool,
|
||||||
|
/// True while the "Reset to default" confirmation is showing.
|
||||||
|
shortcut_reset_confirm: bool,
|
||||||
|
/// True while the "unsaved changes will be discarded" confirmation
|
||||||
|
/// overlays the editor: the user tried to close with un-applied rows.
|
||||||
|
shortcut_close_confirm: bool,
|
||||||
|
|
||||||
// ── Command Aliases ───────────────────────────────────────────────────
|
// ── Command Aliases ───────────────────────────────────────────────────
|
||||||
/// Command-line aliases: uppercase abbreviation → uppercase command
|
/// Command-line aliases: uppercase abbreviation → uppercase command
|
||||||
@@ -2552,6 +2566,28 @@ pub enum Message {
|
|||||||
ShortcutEditorAdd,
|
ShortcutEditorAdd,
|
||||||
ShortcutEditorRemove(usize),
|
ShortcutEditorRemove(usize),
|
||||||
ShortcutEditorApply,
|
ShortcutEditorApply,
|
||||||
|
/// Apply the working rows and close the dialog.
|
||||||
|
ShortcutEditorApplyExit,
|
||||||
|
/// The check button on a draft row: finish the addition without applying.
|
||||||
|
ShortcutEditorDraftAccept,
|
||||||
|
/// Show the "Reset to default" confirmation.
|
||||||
|
ShortcutEditorResetAsk,
|
||||||
|
/// Reset bindings and rows to the shipped defaults.
|
||||||
|
ShortcutEditorResetConfirm,
|
||||||
|
/// Hide the reset confirmation without resetting.
|
||||||
|
ShortcutEditorResetDeny,
|
||||||
|
/// Discard un-applied rows and close the editor.
|
||||||
|
ShortcutEditorCloseDiscard,
|
||||||
|
/// Keep editing: hide the discard confirmation.
|
||||||
|
ShortcutEditorCloseKeep,
|
||||||
|
/// The user clicked a row's Key cell: arm capture for that row.
|
||||||
|
ShortcutCaptureStart(usize),
|
||||||
|
/// A key combination was pressed while a row's Key cell was armed.
|
||||||
|
ShortcutCaptureKey(String),
|
||||||
|
/// Disarm capture without filling the row.
|
||||||
|
ShortcutCaptureClear,
|
||||||
|
/// Cancel capture (Esc): also discards an unfinished draft row.
|
||||||
|
ShortcutCaptureCancel,
|
||||||
/// Canonical key emitted by the global keyboard subscription.
|
/// Canonical key emitted by the global keyboard subscription.
|
||||||
ShortcutPressed(String),
|
ShortcutPressed(String),
|
||||||
// ── Command Alias Editor (ALIASEDIT) ────────────────────────────────
|
// ── Command Alias Editor (ALIASEDIT) ────────────────────────────────
|
||||||
@@ -3414,6 +3450,10 @@ impl OpenCADStudio {
|
|||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
shortcut_bindings: rustc_hash::FxHashMap::default(),
|
shortcut_bindings: rustc_hash::FxHashMap::default(),
|
||||||
shortcut_editor_rows: Vec::new(),
|
shortcut_editor_rows: Vec::new(),
|
||||||
|
shortcut_capture_row: None,
|
||||||
|
shortcut_pending_add: false,
|
||||||
|
shortcut_reset_confirm: false,
|
||||||
|
shortcut_close_confirm: false,
|
||||||
// Command aliases (populated from ocad.pgp just after construction)
|
// Command aliases (populated from ocad.pgp just after construction)
|
||||||
command_aliases: rustc_hash::FxHashMap::default(),
|
command_aliases: rustc_hash::FxHashMap::default(),
|
||||||
alias_editor_rows: Vec::new(),
|
alias_editor_rows: Vec::new(),
|
||||||
|
|||||||
@@ -651,8 +651,10 @@ impl super::OpenCADStudio {
|
|||||||
let history = solid_history::pyramid_op(
|
let history = solid_history::pyramid_op(
|
||||||
glam::DMat4::IDENTITY.to_cols_array(),
|
glam::DMat4::IDENTITY.to_cols_array(),
|
||||||
radius,
|
radius,
|
||||||
|
0.0,
|
||||||
height,
|
height,
|
||||||
n,
|
n,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
let handle = self.add_solid_model(entity, solid, history);
|
let handle = self.add_solid_model(entity, solid, history);
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
|
|||||||
@@ -2074,6 +2074,9 @@ impl OpenCADStudio {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if specialized_primitive {
|
||||||
|
retain_specialized_primitive_sections(&mut sections);
|
||||||
|
}
|
||||||
let title = match entity {
|
let title = match entity {
|
||||||
acadrust::EntityType::Insert(ins) => {
|
acadrust::EntityType::Insert(ins) => {
|
||||||
let is_xref = self.tabs[i]
|
let is_xref = self.tabs[i]
|
||||||
@@ -3047,6 +3050,30 @@ fn aggregate_solid_history_sections(
|
|||||||
merged
|
merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn retain_specialized_primitive_sections(
|
||||||
|
sections: &mut Vec<crate::scene::model::object::PropSection>,
|
||||||
|
) {
|
||||||
|
sections.iter_mut().for_each(|section| {
|
||||||
|
section.props.retain(|property| {
|
||||||
|
matches!(
|
||||||
|
property.field,
|
||||||
|
"color"
|
||||||
|
| "layer"
|
||||||
|
| "linetype"
|
||||||
|
| "linetype_scale"
|
||||||
|
| "plot_style"
|
||||||
|
| "lineweight"
|
||||||
|
| "transparency"
|
||||||
|
| "hyperlink"
|
||||||
|
| "material"
|
||||||
|
| "solid_history_type"
|
||||||
|
) || crate::scene::model::solid_history::is_primitive_property(property.field)
|
||||||
|
|| crate::scene::model::solid_history::is_history_choice(property.field)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
sections.retain(|section| !section.props.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
fn merge_sections(
|
fn merge_sections(
|
||||||
left: &[crate::scene::model::object::PropSection],
|
left: &[crate::scene::model::object::PropSection],
|
||||||
right: &[crate::scene::model::object::PropSection],
|
right: &[crate::scene::model::object::PropSection],
|
||||||
|
|||||||
@@ -141,7 +141,113 @@ fn is_named_key(key: &str) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Input actions `run_shortcut` resolves directly instead of routing through
|
||||||
|
/// the command dispatcher — valid shortcut commands that never appear in the
|
||||||
|
/// command registry. Used to validate the shortcut editor's command column.
|
||||||
|
pub(super) const INPUT_ACTIONS: &[&str] = &[
|
||||||
|
"FINALIZE",
|
||||||
|
"COMMANDSPACE",
|
||||||
|
"CANCEL",
|
||||||
|
"DELETESELECTED",
|
||||||
|
"BACKSPACE",
|
||||||
|
"DYNTAB",
|
||||||
|
"HISTORYPREV",
|
||||||
|
"HISTORYNEXT",
|
||||||
|
"CARETLEFT",
|
||||||
|
"CARETRIGHT",
|
||||||
|
"COMMANDHISTORY",
|
||||||
|
"TOGGLEOSNAP",
|
||||||
|
"OTRACK",
|
||||||
|
"DYNINPUT",
|
||||||
|
"SELECTALL",
|
||||||
|
"PASTECLIP",
|
||||||
|
];
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
|
/// Commit the working rows to the live bindings, discarding an
|
||||||
|
/// unfinished draft and reporting duplicates. Shared by Apply and
|
||||||
|
/// Apply-and-Exit.
|
||||||
|
pub(super) fn finish_shortcut_editor(&mut self) {
|
||||||
|
// An add must end in a complete shortcut or a cancellation;
|
||||||
|
// applying with a half-filled draft discards the draft.
|
||||||
|
if self.shortcut_pending_add {
|
||||||
|
self.shortcut_editor_rows.remove(0);
|
||||||
|
self.shortcut_pending_add = false;
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::tf!("Incomplete shortcut row discarded.").as_ref());
|
||||||
|
}
|
||||||
|
self.shortcut_capture_row = None;
|
||||||
|
// Rows normalizing to the same key silently overwrite each other in
|
||||||
|
// the binding map — surface the conflict instead.
|
||||||
|
let mut seen = rustc_hash::FxHashSet::default();
|
||||||
|
let mut duplicates = Vec::new();
|
||||||
|
for (key, _) in &self.shortcut_editor_rows {
|
||||||
|
let key = normalize_key(key);
|
||||||
|
if key.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !seen.insert(key.clone()) {
|
||||||
|
duplicates.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !duplicates.is_empty() {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::tf!(
|
||||||
|
"Duplicate shortcut(s) ignored on Apply: {}",
|
||||||
|
duplicates.join(", ")
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.apply_shortcut_editor_rows();
|
||||||
|
self.command_line.push_info(
|
||||||
|
crate::tf!("{} shortcut(s) applied.", self.shortcut_bindings.len()).as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the working rows and live bindings to the shipped defaults.
|
||||||
|
pub(super) fn reset_shortcuts_to_defaults(&mut self) {
|
||||||
|
let mut rows: Vec<(String, String)> = default_bindings()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
self.shortcut_editor_rows = rows;
|
||||||
|
self.shortcut_pending_add = false;
|
||||||
|
self.shortcut_capture_row = None;
|
||||||
|
self.shortcut_reset_confirm = false;
|
||||||
|
self.apply_shortcut_editor_rows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the working rows differ from the live bindings — the editor
|
||||||
|
/// has un-applied changes a close would discard.
|
||||||
|
pub(super) fn shortcut_editor_dirty(&self) -> bool {
|
||||||
|
let rows: FxHashMap<String, String> = self
|
||||||
|
.shortcut_editor_rows
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(key, action)| {
|
||||||
|
let key = normalize_key(key);
|
||||||
|
let action = action.trim().to_uppercase();
|
||||||
|
(!key.is_empty() && !action.is_empty()).then_some((key, action))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
rows != self.shortcut_bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pending add finishes successfully once its draft row (row 0) has
|
||||||
|
/// both a key and a command; it then becomes a regular row of the table.
|
||||||
|
/// Called after each edit while a draft is pending.
|
||||||
|
pub(super) fn finish_pending_add(&mut self) {
|
||||||
|
if !self.shortcut_pending_add {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some((key, command)) = self.shortcut_editor_rows.first() {
|
||||||
|
if !key.trim().is_empty() && !command.trim().is_empty() {
|
||||||
|
self.shortcut_pending_add = false;
|
||||||
|
self.shortcut_capture_row = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn apply_shortcut_editor_rows(&mut self) {
|
pub(super) fn apply_shortcut_editor_rows(&mut self) {
|
||||||
let bindings: FxHashMap<String, String> = self
|
let bindings: FxHashMap<String, String> = self
|
||||||
.shortcut_editor_rows
|
.shortcut_editor_rows
|
||||||
|
|||||||
+114
-4
@@ -199,7 +199,13 @@ impl OpenCADStudio {
|
|||||||
// Closing (✕) discards edits made since the last Apply — matching the
|
// Closing (✕) discards edits made since the last Apply — matching the
|
||||||
// style editors. Committing happens only through the Apply button.
|
// style editors. Committing happens only through the Apply button.
|
||||||
Some(Aliases) => self.alias_editor_rows.clear(),
|
Some(Aliases) => self.alias_editor_rows.clear(),
|
||||||
Some(Shortcuts) => self.shortcut_editor_rows.clear(),
|
Some(Shortcuts) => {
|
||||||
|
self.shortcut_editor_rows.clear();
|
||||||
|
self.shortcut_capture_row = None;
|
||||||
|
self.shortcut_pending_add = false;
|
||||||
|
self.shortcut_reset_confirm = false;
|
||||||
|
self.shortcut_close_confirm = false;
|
||||||
|
}
|
||||||
Some(LayerStateEditor) => {
|
Some(LayerStateEditor) => {
|
||||||
self.layer_state_edit_draft = None;
|
self.layer_state_edit_draft = None;
|
||||||
self.layer_state_edit_filter.clear();
|
self.layer_state_edit_filter.clear();
|
||||||
@@ -5564,13 +5570,31 @@ impl OpenCADStudio {
|
|||||||
.collect();
|
.collect();
|
||||||
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
self.shortcut_editor_rows = rows;
|
self.shortcut_editor_rows = rows;
|
||||||
|
self.shortcut_capture_row = None;
|
||||||
|
self.shortcut_pending_add = false;
|
||||||
|
self.shortcut_reset_confirm = false;
|
||||||
|
self.shortcut_close_confirm = false;
|
||||||
self.active_modal = Some(super::ModalKind::Shortcuts);
|
self.active_modal = Some(super::ModalKind::Shortcuts);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ShortcutsPanelClose => {
|
Message::ShortcutsPanelClose => {
|
||||||
|
if !self.shortcut_close_confirm && self.shortcut_editor_dirty() {
|
||||||
|
self.shortcut_close_confirm = true;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.shortcut_close_confirm = false;
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::ShortcutEditorCloseDiscard => {
|
||||||
|
self.shortcut_close_confirm = false;
|
||||||
|
self.close_active_modal();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutEditorCloseKeep => {
|
||||||
|
self.shortcut_close_confirm = false;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::ShortcutEditorInput { idx, field, value } => {
|
Message::ShortcutEditorInput { idx, field, value } => {
|
||||||
use crate::ui::window::shortcuts::ShortcutField;
|
use crate::ui::window::shortcuts::ShortcutField;
|
||||||
if let Some(row) = self.shortcut_editor_rows.get_mut(idx) {
|
if let Some(row) = self.shortcut_editor_rows.get_mut(idx) {
|
||||||
@@ -5579,21 +5603,96 @@ impl OpenCADStudio {
|
|||||||
ShortcutField::Command => row.1 = value.to_uppercase(),
|
ShortcutField::Command => row.1 = value.to_uppercase(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Typing never finishes the addition — only the draft row's
|
||||||
|
// check button does, so no half-visible "ghost" row appears.
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ShortcutEditorAdd => {
|
Message::ShortcutEditorAdd => {
|
||||||
self.shortcut_editor_rows
|
if self.shortcut_pending_add {
|
||||||
.push((String::new(), String::new()));
|
// One draft at a time: re-arm its Key cell instead.
|
||||||
|
self.shortcut_capture_row = Some(0);
|
||||||
|
} else {
|
||||||
|
// The draft row goes to the top of the list so it is
|
||||||
|
// visible without scrolling, with its Key cell armed.
|
||||||
|
self.shortcut_editor_rows
|
||||||
|
.insert(0, (String::new(), String::new()));
|
||||||
|
self.shortcut_pending_add = true;
|
||||||
|
self.shortcut_capture_row = Some(0);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutCaptureStart(idx) => {
|
||||||
|
// Clicking the armed cell again cancels capture.
|
||||||
|
self.shortcut_capture_row = match self.shortcut_capture_row {
|
||||||
|
Some(armed) if armed == idx => None,
|
||||||
|
_ => Some(idx),
|
||||||
|
};
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutCaptureKey(raw) => {
|
||||||
|
if let Some(idx) = self.shortcut_capture_row.take() {
|
||||||
|
let key = crate::app::shortcuts::normalize_key(&raw);
|
||||||
|
if !key.is_empty() && self.shortcut_editor_rows.get_mut(idx).is_some() {
|
||||||
|
self.shortcut_editor_rows[idx].0 = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutCaptureClear => {
|
||||||
|
self.shortcut_capture_row = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutCaptureCancel => {
|
||||||
|
// Esc backs out of capture and abandons an unfinished draft.
|
||||||
|
self.shortcut_capture_row = None;
|
||||||
|
if self.shortcut_pending_add {
|
||||||
|
self.shortcut_editor_rows.remove(0);
|
||||||
|
self.shortcut_pending_add = false;
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ShortcutEditorRemove(idx) => {
|
Message::ShortcutEditorRemove(idx) => {
|
||||||
if idx < self.shortcut_editor_rows.len() {
|
if idx < self.shortcut_editor_rows.len() {
|
||||||
self.shortcut_editor_rows.remove(idx);
|
self.shortcut_editor_rows.remove(idx);
|
||||||
}
|
}
|
||||||
|
if idx == 0 {
|
||||||
|
// Removing the draft row is the ✕ cancel path.
|
||||||
|
self.shortcut_pending_add = false;
|
||||||
|
}
|
||||||
|
// Armed indices shift when a row above them disappears.
|
||||||
|
self.shortcut_capture_row = match self.shortcut_capture_row {
|
||||||
|
Some(armed) if armed == idx => None,
|
||||||
|
Some(armed) if armed > idx => Some(armed - 1),
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ShortcutEditorApply => {
|
Message::ShortcutEditorApply => {
|
||||||
self.apply_shortcut_editor_rows();
|
self.finish_shortcut_editor();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutEditorApplyExit => {
|
||||||
|
self.finish_shortcut_editor();
|
||||||
|
self.close_active_modal();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutEditorDraftAccept => {
|
||||||
|
// The check button: finish the addition (keep the row in the
|
||||||
|
// working table) without applying it. Only complete drafts
|
||||||
|
// can be accepted — the button is disabled otherwise.
|
||||||
|
self.finish_pending_add();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutEditorResetAsk => {
|
||||||
|
self.shortcut_reset_confirm = true;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutEditorResetDeny => {
|
||||||
|
self.shortcut_reset_confirm = false;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ShortcutEditorResetConfirm => {
|
||||||
|
self.reset_shortcuts_to_defaults();
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
crate::tf!("{} shortcut(s) applied.", self.shortcut_bindings.len()).as_ref(),
|
crate::tf!("{} shortcut(s) applied.", self.shortcut_bindings.len()).as_ref(),
|
||||||
);
|
);
|
||||||
@@ -5793,6 +5892,17 @@ impl OpenCADStudio {
|
|||||||
if self.active_modal == Some(super::ModalKind::RecoveryPrompt) {
|
if self.active_modal == Some(super::ModalKind::RecoveryPrompt) {
|
||||||
return self.update(Message::RecoveryDecline);
|
return self.update(Message::RecoveryDecline);
|
||||||
}
|
}
|
||||||
|
// Closing the shortcut editor with un-applied rows needs an
|
||||||
|
// explicit discard; a second close attempt (or the overlay's
|
||||||
|
// Discard button) goes through.
|
||||||
|
if self.active_modal == Some(super::ModalKind::Shortcuts)
|
||||||
|
&& !self.shortcut_close_confirm
|
||||||
|
&& self.shortcut_editor_dirty()
|
||||||
|
{
|
||||||
|
self.shortcut_close_confirm = true;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.shortcut_close_confirm = false;
|
||||||
let resume_open_queue = self.active_modal == Some(super::ModalKind::Recovery);
|
let resume_open_queue = self.active_modal == Some(super::ModalKind::Recovery);
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
if resume_open_queue {
|
if resume_open_queue {
|
||||||
|
|||||||
+52
-14
@@ -118,6 +118,36 @@ fn hatch_pattern_key_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shortcut_capture_key_event(
|
||||||
|
event: iced::Event,
|
||||||
|
_status: iced::event::Status,
|
||||||
|
_window: window::Id,
|
||||||
|
) -> Option<Message> {
|
||||||
|
let iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
// Ignore bare modifier presses so holding Ctrl+Shift before the final
|
||||||
|
// key doesn't end the capture early.
|
||||||
|
if let keyboard::Key::Named(named) = &key {
|
||||||
|
if matches!(
|
||||||
|
named,
|
||||||
|
keyboard::key::Named::Control
|
||||||
|
| keyboard::key::Named::Shift
|
||||||
|
| keyboard::key::Named::Alt
|
||||||
|
| keyboard::key::Named::Super
|
||||||
|
| keyboard::key::Named::Meta
|
||||||
|
) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Esc backs out of capture (and abandons an unfinished draft row) —
|
||||||
|
// binding the ESCAPE key itself is still possible via SHORTCUTS SET.
|
||||||
|
if matches!(key, keyboard::Key::Named(keyboard::key::Named::Escape)) {
|
||||||
|
return Some(Message::ShortcutCaptureCancel);
|
||||||
|
}
|
||||||
|
shortcut_key_name(&key, modifiers).map(Message::ShortcutCaptureKey)
|
||||||
|
}
|
||||||
|
|
||||||
fn shortcut_key_name(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<String> {
|
fn shortcut_key_name(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<String> {
|
||||||
let key = match key {
|
let key = match key {
|
||||||
keyboard::Key::Character(value) if !value.is_empty() => value.to_uppercase(),
|
keyboard::Key::Character(value) if !value.is_empty() => value.to_uppercase(),
|
||||||
@@ -2198,19 +2228,12 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
Subscription::none()
|
Subscription::none()
|
||||||
};
|
};
|
||||||
iced::Subscription::batch([
|
// While a shortcut-editor Key cell is armed for capture, a dedicated
|
||||||
frames,
|
// listener records the pressed combination and swallows every other
|
||||||
history_tick,
|
// key so captured shortcuts do not run in the drawing.
|
||||||
grip_dwell,
|
let keyboard_events = if self.shortcut_capture_row.is_some() {
|
||||||
hover_dwell,
|
event::listen_with(shortcut_capture_key_event)
|
||||||
nav_settle,
|
} else {
|
||||||
thumbnail_capture,
|
|
||||||
caret_blink,
|
|
||||||
web_fonts,
|
|
||||||
autosave,
|
|
||||||
plugin_drain,
|
|
||||||
single_instance,
|
|
||||||
hatch_pattern_keys,
|
|
||||||
event::listen_with(|ev, status, win_id| {
|
event::listen_with(|ev, status, win_id| {
|
||||||
use iced::event::Status;
|
use iced::event::Status;
|
||||||
match ev {
|
match ev {
|
||||||
@@ -2340,7 +2363,22 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
};
|
||||||
|
iced::Subscription::batch([
|
||||||
|
frames,
|
||||||
|
history_tick,
|
||||||
|
grip_dwell,
|
||||||
|
hover_dwell,
|
||||||
|
nav_settle,
|
||||||
|
thumbnail_capture,
|
||||||
|
caret_blink,
|
||||||
|
web_fonts,
|
||||||
|
autosave,
|
||||||
|
plugin_drain,
|
||||||
|
single_instance,
|
||||||
|
hatch_pattern_keys,
|
||||||
|
keyboard_events,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,68 @@ impl OpenCADStudio {
|
|||||||
automatic_flow(ex, crate::ui::window::about::view_window)
|
automatic_flow(ex, crate::ui::window::about::view_window)
|
||||||
}
|
}
|
||||||
super::super::ModalKind::Shortcuts => {
|
super::super::ModalKind::Shortcuts => {
|
||||||
|
// Keys claimed by two rows — the cells turn red and a
|
||||||
|
// persistent warning names the command already using each
|
||||||
|
// key; the last row wins in the binding map on Apply.
|
||||||
|
let mut key_rows: std::collections::BTreeMap<String, Vec<&(String, String)>> =
|
||||||
|
std::collections::BTreeMap::new();
|
||||||
|
for row in &self.shortcut_editor_rows {
|
||||||
|
let key = crate::app::shortcuts::normalize_key(&row.0);
|
||||||
|
if !key.is_empty() {
|
||||||
|
key_rows.entry(key).or_default().push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let duplicate_keys: Vec<String> = key_rows
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, rows)| rows.len() > 1)
|
||||||
|
.map(|(key, _)| key.clone())
|
||||||
|
.collect();
|
||||||
|
let duplicate_conflicts: Vec<(String, String)> = key_rows
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, rows)| rows.len() > 1)
|
||||||
|
.map(|(key, rows)| {
|
||||||
|
// Name the established binding: the first row with
|
||||||
|
// this key that actually has a command.
|
||||||
|
let command = rows
|
||||||
|
.iter()
|
||||||
|
.find(|(_, command)| !command.is_empty())
|
||||||
|
.map(|(_, command)| command.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
(key.clone(), command)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let duplicate_set: rustc_hash::FxHashSet<String> =
|
||||||
|
duplicate_keys.into_iter().collect();
|
||||||
|
// Commands the dispatcher can't run: not a registered
|
||||||
|
// command, plugin command, alias, or input action.
|
||||||
|
let valid: rustc_hash::FxHashSet<String> =
|
||||||
|
crate::command::all_registered_command_names()
|
||||||
|
.into_iter()
|
||||||
|
.map(str::to_uppercase)
|
||||||
|
.chain(
|
||||||
|
self.command_line
|
||||||
|
.dynamic_commands
|
||||||
|
.iter()
|
||||||
|
.map(|cmd| cmd.to_uppercase()),
|
||||||
|
)
|
||||||
|
.chain(self.command_aliases.keys().cloned())
|
||||||
|
.chain(
|
||||||
|
crate::app::shortcuts::INPUT_ACTIONS
|
||||||
|
.iter()
|
||||||
|
.map(|action| action.to_string()),
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
let unknown_commands: Vec<String> = self
|
||||||
|
.shortcut_editor_rows
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, command)| {
|
||||||
|
let command = command.trim();
|
||||||
|
(!command.is_empty() && !valid.contains(command))
|
||||||
|
.then(|| command.to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let unknown_set: rustc_hash::FxHashSet<String> =
|
||||||
|
unknown_commands.into_iter().collect();
|
||||||
sized_flow(
|
sized_flow(
|
||||||
ex,
|
ex,
|
||||||
720,
|
720,
|
||||||
@@ -85,6 +147,13 @@ impl OpenCADStudio {
|
|||||||
|flow| {
|
|flow| {
|
||||||
crate::ui::window::shortcuts::view_window(
|
crate::ui::window::shortcuts::view_window(
|
||||||
&self.shortcut_editor_rows,
|
&self.shortcut_editor_rows,
|
||||||
|
self.shortcut_capture_row,
|
||||||
|
self.shortcut_pending_add,
|
||||||
|
self.shortcut_reset_confirm,
|
||||||
|
&duplicate_set,
|
||||||
|
&duplicate_conflicts,
|
||||||
|
&unknown_set,
|
||||||
|
self.shortcut_close_confirm,
|
||||||
flow,
|
flow,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
+75
-56
@@ -189,16 +189,19 @@ pub(crate) fn insert_attribute_entities(
|
|||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
insert: &acadrust::entities::Insert,
|
insert: &acadrust::entities::Insert,
|
||||||
annotation_scale: f32,
|
annotation_scale: f32,
|
||||||
|
scale_policy: crate::scene::BlockScalePolicy,
|
||||||
) -> Vec<EntityType> {
|
) -> Vec<EntityType> {
|
||||||
let visibility = document.header.attribute_visibility;
|
let visibility = document.header.attribute_visibility;
|
||||||
if visibility == 0 || insert.attributes.is_empty() {
|
if visibility == 0 || insert.attributes.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let block_scale = if insert
|
let block_scale = if scale_policy
|
||||||
.common
|
== crate::scene::BlockScalePolicy::FromInsert
|
||||||
.extended_data
|
&& insert
|
||||||
.get_record("AcAnnotativeData")
|
.common
|
||||||
.is_some()
|
.extended_data
|
||||||
|
.get_record("AcAnnotativeData")
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
annotation_scale as f64
|
annotation_scale as f64
|
||||||
} else {
|
} else {
|
||||||
@@ -253,59 +256,75 @@ pub(crate) fn append_insert_attribute_wires(
|
|||||||
pslt_factor: f32,
|
pslt_factor: f32,
|
||||||
anno_scale: f32,
|
anno_scale: f32,
|
||||||
) {
|
) {
|
||||||
for attr_entity in insert_attribute_entities(document, ins, anno_scale) {
|
let attributes = insert_attribute_entities(
|
||||||
let attr = attr_entity.common();
|
document,
|
||||||
let (sub_color, sub_plen, sub_pat, sub_lw_px, sub_aci) = render::render_style_for_block_sub(
|
ins,
|
||||||
|
anno_scale,
|
||||||
|
crate::scene::BlockScalePolicy::FromInsert,
|
||||||
|
);
|
||||||
|
for offset in crate::scene::render_graph::array_offsets(ins) {
|
||||||
|
let delta = crate::scene::render_graph::insert_instance_translation_delta(
|
||||||
document,
|
document,
|
||||||
&attr_entity,
|
ins,
|
||||||
ins_color,
|
offset,
|
||||||
ins_pat_len,
|
anno_scale,
|
||||||
ins_pat,
|
crate::scene::BlockScalePolicy::FromInsert,
|
||||||
ins_lw_px,
|
|
||||||
ins_layer,
|
|
||||||
);
|
);
|
||||||
let sub_color = render::adapt_to_bg(sub_color, bg_color);
|
for source in &attributes {
|
||||||
let sub_color = if is_xref && !sel {
|
let mut attr_entity = source.clone();
|
||||||
block_cache::fade_toward_bg(sub_color, bg_color)
|
if delta != Vector3::ZERO {
|
||||||
} else {
|
attr_entity.apply_transform(&Transform::from_translation(delta));
|
||||||
sub_color
|
}
|
||||||
};
|
let attr = attr_entity.common();
|
||||||
let attr_plottable = if render::is_effective_layer_zero(&attr.layer) {
|
let (sub_color, sub_plen, sub_pat, sub_lw_px, sub_aci) =
|
||||||
ins_layer_plottable
|
render::render_style_for_block_sub(
|
||||||
} else {
|
document,
|
||||||
document
|
&attr_entity,
|
||||||
.layers
|
ins_color,
|
||||||
.get(&attr.layer)
|
ins_pat_len,
|
||||||
.map(|layer| layer.is_plottable)
|
ins_pat,
|
||||||
.unwrap_or(true)
|
ins_lw_px,
|
||||||
};
|
ins_layer,
|
||||||
let attr_plottable = ins_layer_plottable && attr_plottable;
|
);
|
||||||
let sub_aabb = crate::scene::entity_aabb(&attr_entity);
|
let sub_color = render::adapt_to_bg(sub_color, bg_color);
|
||||||
let mut attr_wires = tessellate::tessellate(
|
let sub_color = if is_xref && !sel {
|
||||||
document,
|
block_cache::fade_toward_bg(sub_color, bg_color)
|
||||||
insert_handle,
|
} else {
|
||||||
&attr_entity,
|
sub_color
|
||||||
sel,
|
};
|
||||||
sub_color,
|
let attr_plottable = if render::is_effective_layer_zero(&attr.layer) {
|
||||||
sub_plen * pslt_factor,
|
ins_layer_plottable
|
||||||
sub_pat.map(|v| v * pslt_factor),
|
} else {
|
||||||
sub_lw_px,
|
document
|
||||||
// Neutral: the attribute is already scaled as part of the block unit
|
.layers
|
||||||
// above, so the text path must not scale it a second time.
|
.get(&attr.layer)
|
||||||
1.0,
|
.map(|layer| layer.is_plottable)
|
||||||
None,
|
.unwrap_or(true)
|
||||||
None,
|
};
|
||||||
bg_color,
|
let attr_plottable = ins_layer_plottable && attr_plottable;
|
||||||
false,
|
let sub_aabb = crate::scene::entity_aabb(&attr_entity);
|
||||||
);
|
let mut attr_wires = tessellate::tessellate(
|
||||||
// Use the INSERT's handle so selection / picking groups attribute
|
document,
|
||||||
// text with the parent insert instead of treating it as a stray text.
|
insert_handle,
|
||||||
for w in &mut attr_wires {
|
&attr_entity,
|
||||||
w.name = insert_handle.value().to_string();
|
sel,
|
||||||
w.aci = sub_aci;
|
sub_color,
|
||||||
w.aabb = sub_aabb;
|
sub_plen * pslt_factor,
|
||||||
w.plot_visible &= attr_plottable;
|
sub_pat.map(|v| v * pslt_factor),
|
||||||
|
sub_lw_px,
|
||||||
|
1.0,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
bg_color,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
for w in &mut attr_wires {
|
||||||
|
w.name = insert_handle.value().to_string();
|
||||||
|
w.aci = sub_aci;
|
||||||
|
w.aabb = sub_aabb;
|
||||||
|
w.plot_visible &= attr_plottable;
|
||||||
|
}
|
||||||
|
wires.extend(attr_wires);
|
||||||
}
|
}
|
||||||
wires.extend(attr_wires);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2129,7 +2129,10 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
view_aabb,
|
view_aabb,
|
||||||
world_per_pixel,
|
world_per_pixel,
|
||||||
1.0,
|
1.0,
|
||||||
crate::scene::convert::tess::BlockObjectOptions::default(),
|
crate::scene::convert::tess::BlockObjectOptions {
|
||||||
|
scale_policy: block_use.scale_policy,
|
||||||
|
..crate::scene::convert::tess::BlockObjectOptions::default()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.wires,
|
.wires,
|
||||||
);
|
);
|
||||||
|
|||||||
+1297
-21
File diff suppressed because it is too large.
Load diff
Vendored
+118
-116
@@ -140,12 +140,16 @@ pub enum LocalSub {
|
|||||||
Nested(NestedRef),
|
Nested(NestedRef),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct BlockMetrics {
|
||||||
|
pub aabb_local: [f32; 4],
|
||||||
|
pub has_point_marker: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct BlockDefn {
|
pub struct BlockDefn {
|
||||||
pub subs: Vec<LocalSub>,
|
pub subs: Vec<LocalSub>,
|
||||||
pub has_point_marker: bool,
|
pub metrics: BlockMetrics,
|
||||||
/// Union of direct and nested geometry in definition coordinates.
|
|
||||||
pub aabb_local: [f32; 4],
|
|
||||||
/// Raw entity count of the source block record (`entity_handles.len()`).
|
/// Raw entity count of the source block record (`entity_handles.len()`).
|
||||||
/// Divisor for nested depth composition: a nested insert's children get a
|
/// Divisor for nested depth composition: a nested insert's children get a
|
||||||
/// sub-range of `parent_scale / (child_count + 1)`, shared with the scene
|
/// sub-range of `parent_scale / (child_count + 1)`, shared with the scene
|
||||||
@@ -247,8 +251,8 @@ impl BlockCache {
|
|||||||
// by-name references (`LocalSub::Nested`), never expanded here, so a
|
// by-name references (`LocalSub::Nested`), never expanded here, so a
|
||||||
// block's build never depends on another block's defn. That makes the
|
// block's build never depends on another block's defn. That makes the
|
||||||
// builds embarrassingly parallel over the read-only `doc` — no
|
// builds embarrassingly parallel over the read-only `doc` — no
|
||||||
// topological ordering required. `compute_block_aabbs` stays a serial
|
// topological ordering required. Metrics are resolved after all
|
||||||
// post-pass (it resolves nested references and is comparatively cheap).
|
// definitions exist.
|
||||||
cache.defns = referenced
|
cache.defns = referenced
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.map(|name| {
|
.map(|name| {
|
||||||
@@ -267,7 +271,7 @@ impl BlockCache {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
cache.compute_block_aabbs(&referenced);
|
cache.compute_block_metrics(&referenced);
|
||||||
cache
|
cache
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,106 +308,73 @@ impl BlockCache {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
cache.compute_block_aabbs(&referenced);
|
cache.compute_block_metrics(&referenced);
|
||||||
cache
|
cache
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute and store the `aabb_local` for every cached defn. Direct wires
|
/// Resolve recursive bounds and point-marker presence.
|
||||||
/// contribute their own aabb_local; nested INSERT references look up the
|
fn compute_block_metrics(&mut self, names: &[String]) {
|
||||||
/// nested defn (already cached) and transform its aabb_local by the
|
|
||||||
/// nested Insert's transform before unioning.
|
|
||||||
///
|
|
||||||
/// Run as a post-pass so it doesn't matter which order build_defn was
|
|
||||||
/// called in. Cycle guard: a self-referential block keeps an empty AABB
|
|
||||||
/// (will fail every frustum test → not emitted, which is correct).
|
|
||||||
fn compute_block_aabbs(&mut self, names: &[String]) {
|
|
||||||
use crate::par::prelude::*;
|
use crate::par::prelude::*;
|
||||||
// Phase 1 (parallel, read-only): each defn's union AABB is resolved
|
// Definitions are complete, so each recursive read can run independently.
|
||||||
// by `defn_aabb_recursive`, which only *reads* `self.defns` (the map
|
|
||||||
// is fully built by now). There's no memoization, so a defn shared by
|
|
||||||
// many parents is re-walked per parent — real work on block-heavy
|
|
||||||
// drawings — and the per-name walks are independent, so they fan out.
|
|
||||||
let this: &Self = self;
|
let this: &Self = self;
|
||||||
let resolved: Vec<(&String, [f32; 4], bool)> = names
|
let resolved: Vec<(&String, BlockMetrics)> = names
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.map(|name| {
|
.map(|name| {
|
||||||
let mut visited: Vec<String> = Vec::new();
|
let mut visited: Vec<String> = Vec::new();
|
||||||
let aabb = this.defn_aabb_recursive(name, &mut visited);
|
(name, this.defn_metrics_recursive(name, &mut visited))
|
||||||
visited.clear();
|
|
||||||
let has_marker = this.defn_has_point_marker_recursive(name, &mut visited);
|
|
||||||
(name, aabb, has_marker)
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// Phase 2 (serial): store the AABB back into each defn.
|
for (name, metrics) in resolved {
|
||||||
for (name, aabb, has_marker) in resolved {
|
|
||||||
if let Some(defn_arc) = self.defns.get_mut(name) {
|
if let Some(defn_arc) = self.defns.get_mut(name) {
|
||||||
let mut defn = (**defn_arc).clone();
|
let mut defn = (**defn_arc).clone();
|
||||||
defn.aabb_local = aabb;
|
defn.metrics = metrics;
|
||||||
defn.has_point_marker = has_marker;
|
|
||||||
*defn_arc = Arc::new(defn);
|
*defn_arc = Arc::new(defn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn defn_has_point_marker_recursive(
|
fn defn_metrics_recursive(
|
||||||
&self,
|
&self,
|
||||||
block_name: &str,
|
block_name: &str,
|
||||||
visited: &mut Vec<String>,
|
visited: &mut Vec<String>,
|
||||||
) -> bool {
|
) -> BlockMetrics {
|
||||||
if visited.iter().any(|name| name == block_name) {
|
if visited.iter().any(|name| name == block_name) {
|
||||||
return false;
|
return BlockMetrics::default();
|
||||||
}
|
}
|
||||||
let Some(defn) = self.defns.get(block_name) else {
|
let Some(defn) = self.defns.get(block_name) else {
|
||||||
return false;
|
return BlockMetrics::default();
|
||||||
};
|
};
|
||||||
visited.push(block_name.to_string());
|
visited.push(block_name.to_string());
|
||||||
let found = defn.subs.iter().any(|sub| match sub {
|
let mut metrics = BlockMetrics::default();
|
||||||
LocalSub::Wire(wire) => wire.point_marker.is_some(),
|
|
||||||
LocalSub::Nested(insert) => {
|
|
||||||
insert
|
|
||||||
.attachments
|
|
||||||
.iter()
|
|
||||||
.any(|wire| wire.point_marker.is_some())
|
|
||||||
|| self.defn_has_point_marker_recursive(&insert.block_name, visited)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
visited.pop();
|
|
||||||
found
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the union AABB for `block_name`'s defn, expressed in **that
|
|
||||||
/// defn's offset frame** (so its caller can store it in
|
|
||||||
/// `BlockDefn.aabb_local` without a coordinate-frame mismatch).
|
|
||||||
///
|
|
||||||
/// LocalWire contributions are already in the parent defn's offset
|
|
||||||
/// frame. Nested-INSERT contributions live in the *child* defn's offset
|
|
||||||
/// frame, so we re-add `child.local_offset` (f64), apply the nested
|
|
||||||
/// Insert's transform to get parent-native coordinates, then subtract
|
|
||||||
/// `parent.local_offset` to land back in the parent's offset frame.
|
|
||||||
fn defn_aabb_recursive(&self, block_name: &str, visited: &mut Vec<String>) -> [f32; 4] {
|
|
||||||
if visited.iter().any(|n| n == block_name) {
|
|
||||||
return [0.0, 0.0, 0.0, 0.0];
|
|
||||||
}
|
|
||||||
let Some(defn) = self.defns.get(block_name) else {
|
|
||||||
return [0.0, 0.0, 0.0, 0.0];
|
|
||||||
};
|
|
||||||
visited.push(block_name.to_string());
|
|
||||||
let mut acc = [0.0_f32, 0.0, 0.0, 0.0];
|
|
||||||
for sub in &defn.subs {
|
for sub in &defn.subs {
|
||||||
let aabb = match sub {
|
match sub {
|
||||||
LocalSub::Wire(lw) => lw.aabb_local,
|
LocalSub::Wire(wire) => {
|
||||||
LocalSub::Nested(nref) => {
|
metrics.aabb_local = aabb_union(metrics.aabb_local, wire.aabb_local);
|
||||||
let nested = self.defn_aabb_recursive(&nref.block_name, visited);
|
metrics.has_point_marker |= wire.point_marker.is_some();
|
||||||
nref.attachments.iter().fold(
|
|
||||||
transform_aabb_xy(nested, &nref.xform),
|
|
||||||
|aabb, wire| aabb_union(aabb, wire.aabb_local),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
};
|
LocalSub::Nested(nref) => {
|
||||||
acc = aabb_union(acc, aabb);
|
let nested = self.defn_metrics_recursive(&nref.block_name, visited);
|
||||||
|
metrics.has_point_marker |= nested.has_point_marker;
|
||||||
|
for offset in &nref.instance_offsets {
|
||||||
|
let transform = nested_instance_transform(nref, *offset);
|
||||||
|
metrics.aabb_local = aabb_union(
|
||||||
|
metrics.aabb_local,
|
||||||
|
transform_aabb_xy(nested.aabb_local, &transform),
|
||||||
|
);
|
||||||
|
let attachment_transform = nested_attachment_transform(nref, *offset);
|
||||||
|
for wire in &nref.attachments {
|
||||||
|
metrics.aabb_local = aabb_union(
|
||||||
|
metrics.aabb_local,
|
||||||
|
transform_aabb_xy(wire.aabb_local, &attachment_transform),
|
||||||
|
);
|
||||||
|
metrics.has_point_marker |= wire.point_marker.is_some();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
visited.pop();
|
visited.pop();
|
||||||
acc
|
metrics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,6 +450,21 @@ fn layer_hidden(doc: &CadDocument, layer: &str) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn nested_instance_transform(nref: &NestedRef, offset: [f64; 3]) -> Transform {
|
||||||
|
if offset == [0.0; 3] {
|
||||||
|
nref.xform.clone()
|
||||||
|
} else {
|
||||||
|
Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2]))
|
||||||
|
.then(&nref.xform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nested_attachment_transform(nref: &NestedRef, offset: [f64; 3]) -> Transform {
|
||||||
|
let base = nref.xform.apply(Vector3::ZERO);
|
||||||
|
let instance = nested_instance_transform(nref, offset).apply(Vector3::ZERO);
|
||||||
|
Transform::from_translation(instance - base)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_defn(
|
fn build_defn(
|
||||||
doc: &CadDocument,
|
doc: &CadDocument,
|
||||||
block_name: &str,
|
block_name: &str,
|
||||||
@@ -549,6 +535,7 @@ fn build_defn(
|
|||||||
for block_use in &block_uses {
|
for block_use in &block_uses {
|
||||||
let mut nested = build_nested_ref(
|
let mut nested = build_nested_ref(
|
||||||
&block_use.insert,
|
&block_use.insert,
|
||||||
|
block_use.scale_policy,
|
||||||
doc,
|
doc,
|
||||||
anno_scale,
|
anno_scale,
|
||||||
bg_color,
|
bg_color,
|
||||||
@@ -597,14 +584,14 @@ fn build_defn(
|
|||||||
}
|
}
|
||||||
BlockDefn {
|
BlockDefn {
|
||||||
subs,
|
subs,
|
||||||
has_point_marker: false,
|
metrics: BlockMetrics::default(),
|
||||||
aabb_local: [0.0; 4],
|
|
||||||
child_count: br.entity_handles.len(),
|
child_count: br.entity_handles.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_nested_ref(
|
fn build_nested_ref(
|
||||||
nested_ins: &acadrust::entities::Insert,
|
nested_ins: &acadrust::entities::Insert,
|
||||||
|
scale_policy: crate::scene::BlockScalePolicy,
|
||||||
doc: &CadDocument,
|
doc: &CadDocument,
|
||||||
anno_scale: f32,
|
anno_scale: f32,
|
||||||
bg_color: [f32; 4],
|
bg_color: [f32; 4],
|
||||||
@@ -614,10 +601,11 @@ fn build_nested_ref(
|
|||||||
// Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps
|
// Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps
|
||||||
// its clip when the parent block is expanded — the spatial filter object
|
// its clip when the parent block is expanded — the spatial filter object
|
||||||
// isn't reachable at expand time.
|
// isn't reachable at expand time.
|
||||||
let xform = crate::scene::render_graph::insert_transform_at_scale(
|
let xform = crate::scene::render_graph::insert_transform_with_policy(
|
||||||
doc,
|
doc,
|
||||||
nested_ins,
|
nested_ins,
|
||||||
anno_scale,
|
anno_scale,
|
||||||
|
scale_policy,
|
||||||
);
|
);
|
||||||
let clip_poly = crate::scene::pick::xclip::insert_spatial_filter(doc, nested_ins)
|
let clip_poly = crate::scene::pick::xclip::insert_spatial_filter(doc, nested_ins)
|
||||||
.map(|filter| {
|
.map(|filter| {
|
||||||
@@ -635,6 +623,7 @@ fn build_nested_ref(
|
|||||||
doc,
|
doc,
|
||||||
nested_ins,
|
nested_ins,
|
||||||
anno_scale,
|
anno_scale,
|
||||||
|
scale_policy,
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|attribute| {
|
.flat_map(|attribute| {
|
||||||
@@ -977,12 +966,18 @@ pub fn expand_insert(
|
|||||||
// Current annotation scale. An annotative block scales as one uniform unit
|
// Current annotation scale. An annotative block scales as one uniform unit
|
||||||
// about its insertion point; a non-annotative block is unaffected.
|
// about its insertion point; a non-annotative block is unaffected.
|
||||||
anno_scale: f32,
|
anno_scale: f32,
|
||||||
|
scale_policy: crate::scene::BlockScalePolicy,
|
||||||
// Dimension picture blocks store definition POINTs at their root. They are
|
// Dimension picture blocks store definition POINTs at their root. They are
|
||||||
// metadata, not visible geometry; nested POINTs remain regular block data.
|
// metadata, not visible geometry; nested POINTs remain regular block data.
|
||||||
suppress_root_points: bool,
|
suppress_root_points: bool,
|
||||||
) -> Option<Vec<WireModel>> {
|
) -> Option<Vec<WireModel>> {
|
||||||
let defn = cache.defn(&ins.block_name)?;
|
let defn = cache.defn(&ins.block_name)?;
|
||||||
let xform = crate::scene::render_graph::insert_transform_at_scale(doc, ins, anno_scale);
|
let xform = crate::scene::render_graph::insert_transform_with_policy(
|
||||||
|
doc,
|
||||||
|
ins,
|
||||||
|
anno_scale,
|
||||||
|
scale_policy,
|
||||||
|
);
|
||||||
let name = ins_handle.value().to_string();
|
let name = ins_handle.value().to_string();
|
||||||
let prototype_key = if !ins.is_array()
|
let prototype_key = if !ins.is_array()
|
||||||
&& cache.prototype_blocks.contains(&ins.block_name)
|
&& cache.prototype_blocks.contains(&ins.block_name)
|
||||||
@@ -1042,10 +1037,22 @@ pub fn expand_insert(
|
|||||||
let mut batches = Batches::default();
|
let mut batches = Batches::default();
|
||||||
let mut visited: Vec<String> = Vec::with_capacity(8);
|
let mut visited: Vec<String> = Vec::with_capacity(8);
|
||||||
|
|
||||||
// `defn.aabb_local` is in the defn's offset frame — re-add
|
let offsets = crate::scene::render_graph::array_offsets(ins);
|
||||||
// `defn.local_offset` (f64) before transforming so the world AABB is
|
let cell_xform = |offset: &[f64; 3]| {
|
||||||
// accurate for distant content.
|
crate::scene::render_graph::insert_instance_transform(
|
||||||
let insert_world = transform_aabb_xy(defn.aabb_local, &xform);
|
doc,
|
||||||
|
ins,
|
||||||
|
*offset,
|
||||||
|
anno_scale,
|
||||||
|
scale_policy,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let insert_world = offsets.iter().fold([0.0_f32; 4], |aabb, offset| {
|
||||||
|
aabb_union(
|
||||||
|
aabb,
|
||||||
|
transform_aabb_xy(defn.metrics.aabb_local, &cell_xform(offset)),
|
||||||
|
)
|
||||||
|
});
|
||||||
let insert_local = [
|
let insert_local = [
|
||||||
insert_world[0] as f32,
|
insert_world[0] as f32,
|
||||||
insert_world[1] as f32,
|
insert_world[1] as f32,
|
||||||
@@ -1062,7 +1069,7 @@ pub fn expand_insert(
|
|||||||
// Whole-Insert pixel-size LOD: if the entire Insert footprint projects
|
// Whole-Insert pixel-size LOD: if the entire Insert footprint projects
|
||||||
// to sub-pixel size, skip it entirely.
|
// to sub-pixel size, skip it entirely.
|
||||||
if let Some(wpp) = world_per_pixel {
|
if let Some(wpp) = world_per_pixel {
|
||||||
if !defn.has_point_marker && aabb_pixel_size(insert_local, wpp) < MIN_PIXEL_SIZE {
|
if !defn.metrics.has_point_marker && aabb_pixel_size(insert_local, wpp) < MIN_PIXEL_SIZE {
|
||||||
return Some(vec![]);
|
return Some(vec![]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1085,16 +1092,7 @@ pub fn expand_insert(
|
|||||||
is_xref,
|
is_xref,
|
||||||
bg_color,
|
bg_color,
|
||||||
};
|
};
|
||||||
let offsets = crate::scene::render_graph::array_offsets(ins);
|
|
||||||
if offsets.len() > 1 {
|
if offsets.len() > 1 {
|
||||||
let cell_xform = |offset: &[f64; 3]| {
|
|
||||||
if offset == &[0.0; 3] {
|
|
||||||
xform.clone()
|
|
||||||
} else {
|
|
||||||
Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2]))
|
|
||||||
.then(&xform)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let first_xform = cell_xform(&offsets[0]);
|
let first_xform = cell_xform(&offsets[0]);
|
||||||
let first_translation = transform_translation(&first_xform);
|
let first_translation = transform_translation(&first_xform);
|
||||||
let mut first_batches = Batches::default();
|
let mut first_batches = Batches::default();
|
||||||
@@ -1135,14 +1133,7 @@ pub fn expand_insert(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for offset in &offsets {
|
for offset in &offsets {
|
||||||
let base_xform = if offset == &[0.0; 3] {
|
let base_xform = cell_xform(offset);
|
||||||
xform.clone()
|
|
||||||
} else {
|
|
||||||
let translation = Transform::from_translation(Vector3::new(
|
|
||||||
offset[0], offset[1], offset[2],
|
|
||||||
));
|
|
||||||
translation.then(&xform)
|
|
||||||
};
|
|
||||||
expand_defn(
|
expand_defn(
|
||||||
defn,
|
defn,
|
||||||
&base_xform,
|
&base_xform,
|
||||||
@@ -1816,8 +1807,18 @@ fn expand_defn(
|
|||||||
is_xref: ctx.is_xref,
|
is_xref: ctx.is_xref,
|
||||||
bg_color: ctx.bg_color,
|
bg_color: ctx.bg_color,
|
||||||
};
|
};
|
||||||
for attachment in &nref.attachments {
|
for offset in &nref.instance_offsets {
|
||||||
emit_wire(attachment, accum_xform, &inner_ctx, out, d_range);
|
let attachment_transform = nested_attachment_transform(nref, *offset)
|
||||||
|
.then(accum_xform);
|
||||||
|
for attachment in &nref.attachments {
|
||||||
|
emit_wire(
|
||||||
|
attachment,
|
||||||
|
&attachment_transform,
|
||||||
|
&inner_ctx,
|
||||||
|
out,
|
||||||
|
d_range,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if visited.iter().any(|n| n == &nref.block_name) {
|
if visited.iter().any(|n| n == &nref.block_name) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1825,8 +1826,17 @@ fn expand_defn(
|
|||||||
let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else {
|
let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let composed = nref.xform.then(accum_xform);
|
let world = nref.instance_offsets.iter().fold(
|
||||||
let world = transform_aabb_xy(nested_defn.aabb_local, &composed);
|
[0.0_f32; 4],
|
||||||
|
|aabb, offset| {
|
||||||
|
let composed = nested_instance_transform(nref, *offset)
|
||||||
|
.then(accum_xform);
|
||||||
|
aabb_union(
|
||||||
|
aabb,
|
||||||
|
transform_aabb_xy(nested_defn.metrics.aabb_local, &composed),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
let local = [
|
let local = [
|
||||||
world[0] as f32,
|
world[0] as f32,
|
||||||
world[1] as f32,
|
world[1] as f32,
|
||||||
@@ -1851,15 +1861,7 @@ fn expand_defn(
|
|||||||
d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0),
|
d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0),
|
||||||
);
|
);
|
||||||
let composed_for = |offset: &[f64; 3]| {
|
let composed_for = |offset: &[f64; 3]| {
|
||||||
if offset == &[0.0; 3] {
|
nested_instance_transform(nref, *offset).then(accum_xform)
|
||||||
nref.xform.then(accum_xform)
|
|
||||||
} else {
|
|
||||||
Transform::from_translation(Vector3::new(
|
|
||||||
offset[0], offset[1], offset[2],
|
|
||||||
))
|
|
||||||
.then(&nref.xform)
|
|
||||||
.then(accum_xform)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if let Some(cp) = &nref.clip_poly {
|
if let Some(cp) = &nref.clip_poly {
|
||||||
let base_composed = nref.xform.then(accum_xform);
|
let base_composed = nref.xform.then(accum_xform);
|
||||||
|
|||||||
@@ -107,10 +107,11 @@ fn block_entity_transform(
|
|||||||
visited,
|
visited,
|
||||||
annotation_scale,
|
annotation_scale,
|
||||||
)?;
|
)?;
|
||||||
Some(inner.then(&crate::scene::render_graph::insert_transform_at_scale(
|
Some(inner.then(&crate::scene::render_graph::insert_transform_with_policy(
|
||||||
document,
|
document,
|
||||||
&block_use.insert,
|
&block_use.insert,
|
||||||
annotation_scale,
|
annotation_scale,
|
||||||
|
block_use.scale_policy,
|
||||||
)))
|
)))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ pub fn symbol_radius(doc: &CadDocument, block: Handle, scale: f64) -> f64 {
|
|||||||
) else {
|
) else {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
};
|
};
|
||||||
graph.walk_insert(
|
graph.walk_block_use(
|
||||||
&block_use.insert,
|
&block_use,
|
||||||
block,
|
block,
|
||||||
|_, _| true,
|
|_, _| true,
|
||||||
|entity, context| {
|
|entity, context| {
|
||||||
@@ -322,16 +322,15 @@ pub fn place_block_wires(
|
|||||||
) else {
|
) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let insert = &mut block_use.insert;
|
block_use.insert.set_x_scale(s);
|
||||||
insert.set_x_scale(s);
|
block_use.insert.set_y_scale(s);
|
||||||
insert.set_y_scale(s);
|
block_use.insert.set_z_scale(s);
|
||||||
insert.set_z_scale(s);
|
|
||||||
let depths = rustc_hash::FxHashMap::default();
|
let depths = rustc_hash::FxHashMap::default();
|
||||||
let graph = crate::scene::render_graph::RenderSceneGraph::new(doc, None, None, true, &depths)
|
let graph = crate::scene::render_graph::RenderSceneGraph::new(doc, None, None, true, &depths)
|
||||||
.with_annotation_scale(anno_scale);
|
.with_annotation_scale(anno_scale);
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
graph.walk_insert(
|
graph.walk_block_use(
|
||||||
insert,
|
&block_use,
|
||||||
block,
|
block,
|
||||||
|_, _| true,
|
|_, _| true,
|
||||||
|entity, context| {
|
|entity, context| {
|
||||||
|
|||||||
+31
-16
@@ -39,6 +39,7 @@ pub(crate) struct ExpandedBlockObject {
|
|||||||
pub(crate) struct BlockObjectOptions {
|
pub(crate) struct BlockObjectOptions {
|
||||||
pub(crate) suppress_root_points: bool,
|
pub(crate) suppress_root_points: bool,
|
||||||
pub(crate) apply_xclip: bool,
|
pub(crate) apply_xclip: bool,
|
||||||
|
pub(crate) scale_policy: crate::scene::BlockScalePolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn block_object_style(
|
fn block_object_style(
|
||||||
@@ -141,16 +142,18 @@ pub(crate) fn expand_block_object(
|
|||||||
is_xref,
|
is_xref,
|
||||||
bg_color,
|
bg_color,
|
||||||
anno_scale,
|
anno_scale,
|
||||||
|
options.scale_policy,
|
||||||
options.suppress_root_points,
|
options.suppress_root_points,
|
||||||
)
|
)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if options.apply_xclip {
|
if options.apply_xclip {
|
||||||
if let Some(filter) = pick::xclip::insert_spatial_filter(document, insert) {
|
if let Some(filter) = pick::xclip::insert_spatial_filter(document, insert) {
|
||||||
let transform = crate::scene::render_graph::insert_transform_at_scale(
|
let transform = crate::scene::render_graph::insert_transform_with_policy(
|
||||||
document,
|
document,
|
||||||
insert,
|
insert,
|
||||||
anno_scale,
|
anno_scale,
|
||||||
|
options.scale_policy,
|
||||||
);
|
);
|
||||||
let polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform);
|
let polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform);
|
||||||
pick::xclip::clip_wires(&mut wires, &polygon);
|
pick::xclip::clip_wires(&mut wires, &polygon);
|
||||||
@@ -568,7 +571,7 @@ fn tessellate_entity_inner(
|
|||||||
// True only when tessellating content shown inside a paper-space viewport.
|
// True only when tessellating content shown inside a paper-space viewport.
|
||||||
// Retained through recursive INSERT expansion; PSLTSCALE itself is applied
|
// Retained through recursive INSERT expansion; PSLTSCALE itself is applied
|
||||||
// by the viewport's GPU uniform so it never changes resident wire content.
|
// by the viewport's GPU uniform so it never changes resident wire content.
|
||||||
paper_space: bool,
|
_paper_space: bool,
|
||||||
) -> Vec<WireModel> {
|
) -> Vec<WireModel> {
|
||||||
let contextual = crate::scene::annotative::entity_for_annotation_context(
|
let contextual = crate::scene::annotative::entity_for_annotation_context(
|
||||||
document,
|
document,
|
||||||
@@ -980,6 +983,7 @@ fn tessellate_entity_inner(
|
|||||||
pslt_factor,
|
pslt_factor,
|
||||||
BlockObjectOptions {
|
BlockObjectOptions {
|
||||||
suppress_root_points: block_use.suppress_root_points,
|
suppress_root_points: block_use.suppress_root_points,
|
||||||
|
scale_policy: block_use.scale_policy,
|
||||||
..BlockObjectOptions::default()
|
..BlockObjectOptions::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1087,7 +1091,10 @@ fn tessellate_entity_inner(
|
|||||||
view_aabb,
|
view_aabb,
|
||||||
world_per_pixel,
|
world_per_pixel,
|
||||||
pslt_factor,
|
pslt_factor,
|
||||||
BlockObjectOptions::default(),
|
BlockObjectOptions {
|
||||||
|
scale_policy: block_use.scale_policy,
|
||||||
|
..BlockObjectOptions::default()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.wires;
|
.wires;
|
||||||
if !wires.is_empty() {
|
if !wires.is_empty() {
|
||||||
@@ -1119,19 +1126,27 @@ fn tessellate_entity_inner(
|
|||||||
.filter(|block_use| {
|
.filter(|block_use| {
|
||||||
block_use.role == crate::scene::render_graph::BlockRole::TableCell
|
block_use.role == crate::scene::render_graph::BlockRole::TableCell
|
||||||
}) {
|
}) {
|
||||||
wires.extend(tessellate_entity(
|
wires.extend(
|
||||||
document,
|
expand_block_object(
|
||||||
selected,
|
document,
|
||||||
active_viewport,
|
&block_use.insert,
|
||||||
bg_color,
|
h,
|
||||||
1.0,
|
sel,
|
||||||
None,
|
active_viewport,
|
||||||
&EntityType::Insert(block_use.insert),
|
bg_color,
|
||||||
block_cache,
|
table_anno,
|
||||||
view_aabb,
|
None,
|
||||||
world_per_pixel,
|
block_cache,
|
||||||
paper_space,
|
view_aabb,
|
||||||
));
|
world_per_pixel,
|
||||||
|
pslt_factor,
|
||||||
|
BlockObjectOptions {
|
||||||
|
scale_policy: block_use.scale_policy,
|
||||||
|
..BlockObjectOptions::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.wires,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if !wires.is_empty() {
|
if !wires.is_empty() {
|
||||||
let aabb = entity_aabb(e);
|
let aabb = entity_aabb(e);
|
||||||
|
|||||||
@@ -2100,8 +2100,8 @@ fn custom_arrow_from_block(
|
|||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
let mut fill = Vec::new();
|
let mut fill = Vec::new();
|
||||||
let mut deferred_hatch = false;
|
let mut deferred_hatch = false;
|
||||||
graph.walk_insert(
|
graph.walk_block_use(
|
||||||
&block_use.insert,
|
&block_use,
|
||||||
record.handle,
|
record.handle,
|
||||||
|_, _| true,
|
|_, _| true,
|
||||||
|entity, context| {
|
|entity, context| {
|
||||||
|
|||||||
+1
-1
@@ -1183,7 +1183,7 @@ impl Scene {
|
|||||||
fn layer_plottable_in_context(
|
fn layer_plottable_in_context(
|
||||||
&self,
|
&self,
|
||||||
entity: &EntityType,
|
entity: &EntityType,
|
||||||
context: &crate::scene::render_graph::RenderContext,
|
context: &crate::scene::render_graph::InstanceContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let common = entity.common();
|
let common = entity.common();
|
||||||
let layer = if crate::scene::view::render::is_effective_layer_zero(&common.layer) {
|
let layer = if crate::scene::view::render::is_effective_layer_zero(&common.layer) {
|
||||||
|
|||||||
+146
-16
@@ -17,6 +17,13 @@ pub(crate) mod render_graph;
|
|||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||||
|
pub enum BlockScalePolicy {
|
||||||
|
#[default]
|
||||||
|
FromInsert,
|
||||||
|
Applied,
|
||||||
|
}
|
||||||
|
|
||||||
// Topic submodules split out of this root (each contributes `impl Scene`
|
// Topic submodules split out of this root (each contributes `impl Scene`
|
||||||
// blocks and/or free functions). Pure text-move from the original mod.rs.
|
// blocks and/or free functions). Pure text-move from the original mod.rs.
|
||||||
mod boundary;
|
mod boundary;
|
||||||
@@ -66,6 +73,13 @@ struct DependencyTargets {
|
|||||||
touches_block_definition: bool,
|
touches_block_definition: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum DependencyKind {
|
||||||
|
Layer,
|
||||||
|
TextStyle,
|
||||||
|
DimStyle,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct SceneDependencyIndex {
|
struct SceneDependencyIndex {
|
||||||
layers: HashMap<String, DependencyTargets>,
|
layers: HashMap<String, DependencyTargets>,
|
||||||
@@ -75,6 +89,7 @@ struct SceneDependencyIndex {
|
|||||||
points: DependencyTargets,
|
points: DependencyTargets,
|
||||||
text_geometry: DependencyTargets,
|
text_geometry: DependencyTargets,
|
||||||
annotation_geometry: DependencyTargets,
|
annotation_geometry: DependencyTargets,
|
||||||
|
signatures: HashMap<Handle, u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> {
|
fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> {
|
||||||
@@ -164,6 +179,7 @@ use glam;
|
|||||||
use iced::time::Duration;
|
use iced::time::Duration;
|
||||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -2429,11 +2445,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !changes.is_empty() {
|
if !changes.is_empty() {
|
||||||
// A Modified delta can change layer/style/block references as well
|
self.refresh_dependency_index_for_changes(&changes);
|
||||||
// as coordinates. Rebuild the reverse dependency map lazily on its
|
|
||||||
// next use so later targeted global-property updates never follow
|
|
||||||
// stale ownership.
|
|
||||||
self.invalidate_dependency_index();
|
|
||||||
}
|
}
|
||||||
let cached_light_changed = self.lighting_cache.borrow().values().any(|lights| {
|
let cached_light_changed = self.lighting_cache.borrow().values().any(|lights| {
|
||||||
changes
|
changes
|
||||||
@@ -2533,6 +2545,7 @@ impl Scene {
|
|||||||
// Structural change — drop both per-entity tessellation memos.
|
// Structural change — drop both per-entity tessellation memos.
|
||||||
self.tess_memo.borrow_mut().clear();
|
self.tess_memo.borrow_mut().clear();
|
||||||
self.resident_tess_memo.borrow_mut().clear();
|
self.resident_tess_memo.borrow_mut().clear();
|
||||||
|
self.invalidate_dependency_index();
|
||||||
// Can't name the changed handles → force every journal consumer to rebuild.
|
// Can't name the changed handles → force every journal consumer to rebuild.
|
||||||
self.push_geometry_delta(epoch, Vec::new(), true);
|
self.push_geometry_delta(epoch, Vec::new(), true);
|
||||||
}
|
}
|
||||||
@@ -2616,6 +2629,7 @@ impl Scene {
|
|||||||
let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||||
self.geometry_epoch = epoch;
|
self.geometry_epoch = epoch;
|
||||||
self.lighting_cache.borrow_mut().clear();
|
self.lighting_cache.borrow_mut().clear();
|
||||||
|
self.invalidate_dependency_index();
|
||||||
self.push_geometry_delta(epoch, Vec::new(), true);
|
self.push_geometry_delta(epoch, Vec::new(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7132,8 +7146,8 @@ impl Scene {
|
|||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
graph.walk_insert(
|
graph.walk_block_use(
|
||||||
&block_use.insert,
|
&block_use,
|
||||||
common.handle,
|
common.handle,
|
||||||
|_, _| true,
|
|_, _| true,
|
||||||
|entity, context| {
|
|entity, context| {
|
||||||
@@ -7387,7 +7401,10 @@ impl Scene {
|
|||||||
let changed_live: Vec<Handle> = changes
|
let changed_live: Vec<Handle> = changes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(handle, kind)| {
|
.filter_map(|(handle, kind)| {
|
||||||
(!matches!(kind, ChangeKind::Removed)).then_some(*handle)
|
(!matches!(kind, ChangeKind::Removed)
|
||||||
|
&& !self.entity_temporarily_hidden(*handle)
|
||||||
|
&& self.document.get_entity(*handle).is_some())
|
||||||
|
.then_some(*handle)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let wires = Arc::new(self.wire_models_for(&changed_live));
|
let wires = Arc::new(self.wire_models_for(&changed_live));
|
||||||
@@ -8847,6 +8864,113 @@ impl Scene {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn entity_dependency_signature(&self, entity: &EntityType) -> u64 {
|
||||||
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
let common = entity.common();
|
||||||
|
common.handle.hash(&mut hasher);
|
||||||
|
common.owner_handle.hash(&mut hasher);
|
||||||
|
normalize_name(&common.layer).hash(&mut hasher);
|
||||||
|
|
||||||
|
let category = match entity {
|
||||||
|
EntityType::Point(_) => 1_u8,
|
||||||
|
EntityType::Text(_)
|
||||||
|
| EntityType::MText(_)
|
||||||
|
| EntityType::AttributeDefinition(_)
|
||||||
|
| EntityType::AttributeEntity(_)
|
||||||
|
| EntityType::Dimension(_)
|
||||||
|
| EntityType::Leader(_)
|
||||||
|
| EntityType::Tolerance(_)
|
||||||
|
| EntityType::MultiLeader(_)
|
||||||
|
| EntityType::Table(_)
|
||||||
|
| EntityType::Shape(_) => 2,
|
||||||
|
EntityType::Hatch(_) => 3,
|
||||||
|
EntityType::Insert(insert) if !insert.attributes.is_empty() => 4,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
category.hash(&mut hasher);
|
||||||
|
crate::scene::annotative::is_annotative(&self.document, entity).hash(&mut hasher);
|
||||||
|
|
||||||
|
for block_use in render_graph::entity_block_uses(&self.document, entity, 1.0) {
|
||||||
|
block_use.block.hash(&mut hasher);
|
||||||
|
normalize_name(&block_use.insert.block_name).hash(&mut hasher);
|
||||||
|
block_use.role.hash(&mut hasher);
|
||||||
|
block_use.scale_policy.hash(&mut hasher);
|
||||||
|
block_use.active.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
|
||||||
|
match entity {
|
||||||
|
EntityType::Text(text) => normalize_name(&text.style).hash(&mut hasher),
|
||||||
|
EntityType::MText(text) => normalize_name(&text.style).hash(&mut hasher),
|
||||||
|
EntityType::AttributeDefinition(attribute) => {
|
||||||
|
normalize_name(&attribute.text_style).hash(&mut hasher)
|
||||||
|
}
|
||||||
|
EntityType::AttributeEntity(attribute) => {
|
||||||
|
normalize_name(&attribute.text_style).hash(&mut hasher)
|
||||||
|
}
|
||||||
|
EntityType::Insert(insert) => {
|
||||||
|
for attribute in &insert.attributes {
|
||||||
|
normalize_name(&attribute.common.layer).hash(&mut hasher);
|
||||||
|
normalize_name(&attribute.text_style).hash(&mut hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntityType::Dimension(dimension) => {
|
||||||
|
normalize_name(&dimension.base().style_name).hash(&mut hasher)
|
||||||
|
}
|
||||||
|
EntityType::Leader(leader) => {
|
||||||
|
normalize_name(&leader.dimension_style).hash(&mut hasher)
|
||||||
|
}
|
||||||
|
EntityType::Tolerance(tolerance) => {
|
||||||
|
normalize_name(&tolerance.dimension_style_name).hash(&mut hasher)
|
||||||
|
}
|
||||||
|
EntityType::Table(table) => {
|
||||||
|
table.table_style_handle.hash(&mut hasher);
|
||||||
|
for row in &table.rows {
|
||||||
|
row.style
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|style| style.text_style_handle)
|
||||||
|
.hash(&mut hasher);
|
||||||
|
for cell in &row.cells {
|
||||||
|
cell.style
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|style| style.text_style_handle)
|
||||||
|
.hash(&mut hasher);
|
||||||
|
for content in &cell.contents {
|
||||||
|
content.text_style_handle.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntityType::MultiLeader(leader) => {
|
||||||
|
leader.style_handle.hash(&mut hasher);
|
||||||
|
leader.text_style_handle.hash(&mut hasher);
|
||||||
|
leader.context.text_style_handle.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
EntityType::MLine(line) => line.style_handle.hash(&mut hasher),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_dependency_index_for_changes(&self, changes: &[(Handle, ChangeKind)]) {
|
||||||
|
let mut cache = self.dependency_index_cache.borrow_mut();
|
||||||
|
let Some(index) = cache.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let unchanged = changes.iter().all(|(handle, kind)| {
|
||||||
|
if !matches!(kind, ChangeKind::Modified) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(entity) = self.document.get_entity(*handle) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
index.signatures.get(handle).copied()
|
||||||
|
== Some(self.entity_dependency_signature(entity))
|
||||||
|
});
|
||||||
|
if !unchanged {
|
||||||
|
cache.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn rebuild_dependency_index(&self) -> SceneDependencyIndex {
|
fn rebuild_dependency_index(&self) -> SceneDependencyIndex {
|
||||||
let layout_blocks: HashSet<Handle> = self
|
let layout_blocks: HashSet<Handle> = self
|
||||||
.document
|
.document
|
||||||
@@ -8928,6 +9052,9 @@ impl Scene {
|
|||||||
let mut index = SceneDependencyIndex::default();
|
let mut index = SceneDependencyIndex::default();
|
||||||
for entity in self.document.entities() {
|
for entity in self.document.entities() {
|
||||||
let common = entity.common();
|
let common = entity.common();
|
||||||
|
index
|
||||||
|
.signatures
|
||||||
|
.insert(common.handle, self.entity_dependency_signature(entity));
|
||||||
let owner = if common.owner_handle.is_null() {
|
let owner = if common.owner_handle.is_null() {
|
||||||
membership
|
membership
|
||||||
.get(&common.handle)
|
.get(&common.handle)
|
||||||
@@ -9102,17 +9229,20 @@ impl Scene {
|
|||||||
index
|
index
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dependency_targets(&self, kind: &str, names: &[String]) -> DependencyTargets {
|
fn dependency_targets(
|
||||||
|
&self,
|
||||||
|
kind: DependencyKind,
|
||||||
|
names: &[String],
|
||||||
|
) -> DependencyTargets {
|
||||||
if self.dependency_index_cache.borrow().is_none() {
|
if self.dependency_index_cache.borrow().is_none() {
|
||||||
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
|
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
|
||||||
}
|
}
|
||||||
let cache = self.dependency_index_cache.borrow();
|
let cache = self.dependency_index_cache.borrow();
|
||||||
let index = cache.as_ref().unwrap();
|
let index = cache.as_ref().unwrap();
|
||||||
let map = match kind {
|
let map = match kind {
|
||||||
"layer" => &index.layers,
|
DependencyKind::Layer => &index.layers,
|
||||||
"text" => &index.text_styles,
|
DependencyKind::TextStyle => &index.text_styles,
|
||||||
"dim" => &index.dim_styles,
|
DependencyKind::DimStyle => &index.dim_styles,
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
};
|
||||||
let mut combined = DependencyTargets::default();
|
let mut combined = DependencyTargets::default();
|
||||||
for name in names {
|
for name in names {
|
||||||
@@ -9148,7 +9278,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalidate_layer_dependencies(&mut self, names: &[String]) {
|
pub fn invalidate_layer_dependencies(&mut self, names: &[String]) {
|
||||||
let targets = self.dependency_targets("layer", names);
|
let targets = self.dependency_targets(DependencyKind::Layer, names);
|
||||||
self.invalidate_dependency_targets(targets);
|
self.invalidate_dependency_targets(targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9157,7 +9287,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalidate_text_style_dependencies_many(&mut self, names: &[String]) {
|
pub fn invalidate_text_style_dependencies_many(&mut self, names: &[String]) {
|
||||||
let targets = self.dependency_targets("text", names);
|
let targets = self.dependency_targets(DependencyKind::TextStyle, names);
|
||||||
self.invalidate_dependency_targets(targets);
|
self.invalidate_dependency_targets(targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9166,7 +9296,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalidate_dim_style_dependencies_many(&mut self, names: &[String]) {
|
pub fn invalidate_dim_style_dependencies_many(&mut self, names: &[String]) {
|
||||||
let targets = self.dependency_targets("dim", names);
|
let targets = self.dependency_targets(DependencyKind::DimStyle, names);
|
||||||
self.invalidate_dependency_targets(targets);
|
self.invalidate_dependency_targets(targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ pub const GRIP_INNER_RADIUS: usize = 10_006;
|
|||||||
pub const GRIP_SIDES: usize = 10_007;
|
pub const GRIP_SIDES: usize = 10_007;
|
||||||
pub const GRIP_MAJOR_RADIUS: usize = 10_008;
|
pub const GRIP_MAJOR_RADIUS: usize = 10_008;
|
||||||
pub const GRIP_MINOR_RADIUS: usize = 10_009;
|
pub const GRIP_MINOR_RADIUS: usize = 10_009;
|
||||||
pub const GRIP_POSITION: usize = 10_010;
|
pub const GRIP_TOP_RADIUS: usize = 10_010;
|
||||||
|
pub const GRIP_POSITION: usize = 10_011;
|
||||||
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
|
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
|
||||||
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
|
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
|
||||||
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
||||||
@@ -54,6 +55,7 @@ pub const PROP_ROTATION: &str = "solid_history_rotation";
|
|||||||
pub const PROP_PROFILE_ROTATION: &str = "solid_history_profile_rotation";
|
pub const PROP_PROFILE_ROTATION: &str = "solid_history_profile_rotation";
|
||||||
pub const PROP_TWIST_ALONG_PATH: &str = "solid_history_twist_along_path";
|
pub const PROP_TWIST_ALONG_PATH: &str = "solid_history_twist_along_path";
|
||||||
pub const PROP_SCALE_ALONG_PATH: &str = "solid_history_scale_along_path";
|
pub const PROP_SCALE_ALONG_PATH: &str = "solid_history_scale_along_path";
|
||||||
|
pub const PROP_PYRAMID_TYPE: &str = "solid_history_pyramid_type";
|
||||||
pub const PROP_HISTORY: &str = "solid_history_record";
|
pub const PROP_HISTORY: &str = "solid_history_record";
|
||||||
pub const PROP_SHOW_HISTORY: &str = "solid_history_show";
|
pub const PROP_SHOW_HISTORY: &str = "solid_history_show";
|
||||||
|
|
||||||
@@ -103,6 +105,8 @@ pub fn has_specialized_primitive_properties(
|
|||||||
| SolidHistoryOperation::Sphere(_)
|
| SolidHistoryOperation::Sphere(_)
|
||||||
| SolidHistoryOperation::Cone(_)
|
| SolidHistoryOperation::Cone(_)
|
||||||
| SolidHistoryOperation::Cylinder(_)
|
| SolidHistoryOperation::Cylinder(_)
|
||||||
|
| SolidHistoryOperation::Torus(_)
|
||||||
|
| SolidHistoryOperation::Pyramid(_)
|
||||||
| SolidHistoryOperation::Sweep(_)
|
| SolidHistoryOperation::Sweep(_)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -127,6 +131,8 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
|
|||||||
value.reference_point.z,
|
value.reference_point.z,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
SolidHistoryOperation::Torus(value) => world_point(value.base.transform, [0.0; 3]),
|
||||||
|
SolidHistoryOperation::Pyramid(value) => world_point(value.base.transform, [0.0; 3]),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +251,75 @@ fn sweep_path_length(value: &SolidHistorySweep) -> Option<f64> {
|
|||||||
(length.is_finite() && length >= 0.0).then_some(length)
|
(length.is_finite() && length >= 0.0).then_some(length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn torus_properties(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
handle: acadrust::Handle,
|
||||||
|
value: &SolidHistoryTorus,
|
||||||
|
) -> Vec<PropSection> {
|
||||||
|
let Some(position) = world_point(value.base.transform, [0.0; 3]) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let (record_history, object_show_history, show_history_mode) =
|
||||||
|
history_flags(document, handle).unwrap_or((false, false, 1));
|
||||||
|
let (show_history, _) = displayed_history_state(object_show_history, show_history_mode);
|
||||||
|
vec![
|
||||||
|
PropSection {
|
||||||
|
title: t!("Geometry").into_owned(),
|
||||||
|
props: vec![
|
||||||
|
Property {
|
||||||
|
label: t!("Solid type").into_owned(),
|
||||||
|
field: "solid_history_type",
|
||||||
|
value: PropValue::ReadOnly(t!("Torus").into_owned()),
|
||||||
|
},
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position X").as_ref(),
|
||||||
|
PROP_POSITION_X,
|
||||||
|
position.x,
|
||||||
|
),
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position Y").as_ref(),
|
||||||
|
PROP_POSITION_Y,
|
||||||
|
position.y,
|
||||||
|
),
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position Z").as_ref(),
|
||||||
|
PROP_POSITION_Z,
|
||||||
|
position.z,
|
||||||
|
),
|
||||||
|
history_prop(
|
||||||
|
t!("Torus radius").as_ref(),
|
||||||
|
PROP_MAJOR_RADIUS,
|
||||||
|
value.major_radius,
|
||||||
|
),
|
||||||
|
history_prop(
|
||||||
|
t!("Tube radius").as_ref(),
|
||||||
|
PROP_MINOR_RADIUS,
|
||||||
|
value.minor_radius,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
PropSection {
|
||||||
|
title: t!("Solid History").into_owned(),
|
||||||
|
props: vec![
|
||||||
|
Property {
|
||||||
|
label: t!("History").into_owned(),
|
||||||
|
field: PROP_HISTORY,
|
||||||
|
value: PropValue::ReadOnly(
|
||||||
|
if record_history { "Record" } else { "None" }.to_string(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
label: t!("Show History").into_owned(),
|
||||||
|
field: PROP_SHOW_HISTORY,
|
||||||
|
value: PropValue::ReadOnly(
|
||||||
|
if show_history { "Yes" } else { "No" }.to_string(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn cone_properties(
|
fn cone_properties(
|
||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
handle: acadrust::Handle,
|
handle: acadrust::Handle,
|
||||||
@@ -491,6 +566,132 @@ fn cylinder_properties(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pyramid_is_inscribed(value: &SolidHistoryPyramid) -> bool {
|
||||||
|
value.operation_minor == -1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pyramid_apothem_factor(sides: i32) -> f64 {
|
||||||
|
(std::f64::consts::PI / sides.clamp(3, 32) as f64).cos()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pyramid_display_radius(value: &SolidHistoryPyramid, radius: f64) -> f64 {
|
||||||
|
if pyramid_is_inscribed(value) {
|
||||||
|
radius
|
||||||
|
} else {
|
||||||
|
radius * pyramid_apothem_factor(value.sides)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pyramid_display_rotation(value: &SolidHistoryPyramid) -> Option<f64> {
|
||||||
|
let vertex_rotation = matrix(value.base.transform)
|
||||||
|
.map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x))?;
|
||||||
|
if pyramid_is_inscribed(value) {
|
||||||
|
Some(vertex_rotation)
|
||||||
|
} else {
|
||||||
|
Some(vertex_rotation + std::f64::consts::PI / value.sides.clamp(3, 32) as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pyramid_properties(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
handle: acadrust::Handle,
|
||||||
|
value: &SolidHistoryPyramid,
|
||||||
|
) -> Vec<PropSection> {
|
||||||
|
let Some(position) = world_point(value.base.transform, [0.0; 3]) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let rotation = pyramid_display_rotation(value).unwrap_or(0.0);
|
||||||
|
let (record_history, object_show_history, show_history_mode) =
|
||||||
|
history_flags(document, handle).unwrap_or((false, false, 1));
|
||||||
|
let (show_history, show_history_editable) =
|
||||||
|
displayed_history_state(object_show_history, show_history_mode);
|
||||||
|
let show_value = if show_history { "Yes" } else { "No" };
|
||||||
|
vec![
|
||||||
|
PropSection {
|
||||||
|
title: t!("Geometry").into_owned(),
|
||||||
|
props: vec![
|
||||||
|
Property {
|
||||||
|
label: t!("Solid type").into_owned(),
|
||||||
|
field: "solid_history_type",
|
||||||
|
value: PropValue::ReadOnly(t!("Pyramid").into_owned()),
|
||||||
|
},
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position X").as_ref(),
|
||||||
|
PROP_POSITION_X,
|
||||||
|
position.x,
|
||||||
|
),
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position Y").as_ref(),
|
||||||
|
PROP_POSITION_Y,
|
||||||
|
position.y,
|
||||||
|
),
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position Z").as_ref(),
|
||||||
|
PROP_POSITION_Z,
|
||||||
|
position.z,
|
||||||
|
),
|
||||||
|
Property {
|
||||||
|
label: t!("Type").into_owned(),
|
||||||
|
field: PROP_PYRAMID_TYPE,
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: if pyramid_is_inscribed(value) {
|
||||||
|
"Inscribed"
|
||||||
|
} else {
|
||||||
|
"Circumscribed"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
options: vec!["Circumscribed".to_string(), "Inscribed".to_string()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
history_prop(
|
||||||
|
t!("Base radius").as_ref(),
|
||||||
|
PROP_BASE_RADIUS,
|
||||||
|
pyramid_display_radius(value, value.radius),
|
||||||
|
),
|
||||||
|
history_prop(
|
||||||
|
t!("Top radius").as_ref(),
|
||||||
|
PROP_TOP_RADIUS,
|
||||||
|
pyramid_display_radius(value, value.top_radius),
|
||||||
|
),
|
||||||
|
history_prop(t!("Sides").as_ref(), PROP_SIDES, value.sides),
|
||||||
|
Property {
|
||||||
|
label: t!("Rotation").into_owned(),
|
||||||
|
field: PROP_ROTATION,
|
||||||
|
value: PropValue::EditText(
|
||||||
|
crate::entities::common::format_direction(rotation),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
PropSection {
|
||||||
|
title: t!("Solid History").into_owned(),
|
||||||
|
props: vec![
|
||||||
|
Property {
|
||||||
|
label: t!("History").into_owned(),
|
||||||
|
field: PROP_HISTORY,
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: if record_history { "Record" } else { "None" }.to_string(),
|
||||||
|
options: vec!["None".to_string(), "Record".to_string()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
label: t!("Show History").into_owned(),
|
||||||
|
field: PROP_SHOW_HISTORY,
|
||||||
|
value: if show_history_editable {
|
||||||
|
PropValue::Choice {
|
||||||
|
selected: show_value.to_string(),
|
||||||
|
options: vec!["No".to_string(), "Yes".to_string()],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PropValue::ReadOnly(show_value.to_string())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn sphere_properties(
|
fn sphere_properties(
|
||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
handle: acadrust::Handle,
|
handle: acadrust::Handle,
|
||||||
@@ -671,46 +872,21 @@ pub fn primitive_properties(
|
|||||||
let Some(operation) = document.solid_history_operation(handle) else {
|
let Some(operation) = document.solid_history_operation(handle) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let props = match operation {
|
match operation {
|
||||||
SolidHistoryOperation::Box(value) => {
|
SolidHistoryOperation::Box(value) => {
|
||||||
return rectangular_properties(document, handle, value, "Box")
|
rectangular_properties(document, handle, value, "Box")
|
||||||
}
|
}
|
||||||
SolidHistoryOperation::Wedge(value) => {
|
SolidHistoryOperation::Wedge(value) => {
|
||||||
return rectangular_properties(document, handle, value, "Wedge")
|
rectangular_properties(document, handle, value, "Wedge")
|
||||||
}
|
}
|
||||||
SolidHistoryOperation::Sphere(value) => {
|
SolidHistoryOperation::Sphere(value) => sphere_properties(document, handle, value),
|
||||||
return sphere_properties(document, handle, value)
|
SolidHistoryOperation::Cone(value) => cone_properties(document, handle, value),
|
||||||
}
|
SolidHistoryOperation::Cylinder(value) => cylinder_properties(document, handle, value),
|
||||||
SolidHistoryOperation::Cone(value) => return cone_properties(document, handle, value),
|
SolidHistoryOperation::Pyramid(value) => pyramid_properties(document, handle, value),
|
||||||
SolidHistoryOperation::Cylinder(value) => {
|
SolidHistoryOperation::Torus(value) => torus_properties(document, handle, value),
|
||||||
return cylinder_properties(document, handle, value)
|
SolidHistoryOperation::Sweep(value) => sweep_properties(document, handle, value),
|
||||||
}
|
_ => Vec::new(),
|
||||||
SolidHistoryOperation::Sweep(value) => {
|
}
|
||||||
return sweep_properties(document, handle, value)
|
|
||||||
}
|
|
||||||
SolidHistoryOperation::Torus(value) => vec![
|
|
||||||
history_prop(
|
|
||||||
t!("Outer Radius").as_ref(),
|
|
||||||
PROP_OUTER_RADIUS,
|
|
||||||
value.major_radius + value.minor_radius,
|
|
||||||
),
|
|
||||||
history_prop(
|
|
||||||
t!("Inner Radius").as_ref(),
|
|
||||||
PROP_INNER_RADIUS,
|
|
||||||
(value.major_radius - value.minor_radius).max(0.0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
SolidHistoryOperation::Pyramid(value) => vec![
|
|
||||||
history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.radius),
|
|
||||||
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
|
|
||||||
history_prop(t!("Sides").as_ref(), PROP_SIDES, value.sides),
|
|
||||||
],
|
|
||||||
_ => return Vec::new(),
|
|
||||||
};
|
|
||||||
vec![PropSection {
|
|
||||||
title: t!("Primitive").into_owned(),
|
|
||||||
props,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_primitive_property(field: &str) -> bool {
|
pub fn is_primitive_property(field: &str) -> bool {
|
||||||
@@ -739,6 +915,7 @@ pub fn is_primitive_property(field: &str) -> bool {
|
|||||||
| PROP_PROFILE_ROTATION
|
| PROP_PROFILE_ROTATION
|
||||||
| PROP_TWIST_ALONG_PATH
|
| PROP_TWIST_ALONG_PATH
|
||||||
| PROP_SCALE_ALONG_PATH
|
| PROP_SCALE_ALONG_PATH
|
||||||
|
| PROP_PYRAMID_TYPE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,6 +1159,36 @@ fn apply_sweep_geometry_property(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_torus_position_property(
|
||||||
|
value: &mut SolidHistoryTorus,
|
||||||
|
field: &str,
|
||||||
|
text: &str,
|
||||||
|
) -> Option<bool> {
|
||||||
|
let axis = match field {
|
||||||
|
PROP_POSITION_X => 0,
|
||||||
|
PROP_POSITION_Y => 1,
|
||||||
|
PROP_POSITION_Z => 2,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let target = crate::entities::common::parse_length(text)?;
|
||||||
|
if !target.is_finite() {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let current = matrix(value.base.transform)?;
|
||||||
|
let origin = current.transform_point3(glam::DVec3::ZERO);
|
||||||
|
if !origin.is_finite() || (target - origin[axis]).abs() <= 1e-12 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let mut delta = glam::DVec3::ZERO;
|
||||||
|
delta[axis] = target - origin[axis];
|
||||||
|
let updated = glam::DMat4::from_translation(delta) * current;
|
||||||
|
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
value.base.transform = updated.to_cols_array();
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
|
||||||
fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool {
|
fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool {
|
||||||
if value.minor_radius <= value.major_radius {
|
if value.minor_radius <= value.major_radius {
|
||||||
value.x_radius = value.major_radius;
|
value.x_radius = value.major_radius;
|
||||||
@@ -997,6 +1204,120 @@ fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_pyramid_geometry_property(
|
||||||
|
value: &mut SolidHistoryPyramid,
|
||||||
|
field: &str,
|
||||||
|
text: &str,
|
||||||
|
) -> Option<bool> {
|
||||||
|
if field == PROP_PYRAMID_TYPE {
|
||||||
|
let next_inscribed = if text.eq_ignore_ascii_case("Inscribed") {
|
||||||
|
true
|
||||||
|
} else if text.eq_ignore_ascii_case("Circumscribed") {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
return Some(false);
|
||||||
|
};
|
||||||
|
let old_inscribed = pyramid_is_inscribed(value);
|
||||||
|
if next_inscribed == old_inscribed {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let factor = pyramid_apothem_factor(value.sides);
|
||||||
|
if factor <= 1e-9 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let base_display = if old_inscribed { value.radius } else { value.radius * factor };
|
||||||
|
let top_display = if old_inscribed {
|
||||||
|
value.top_radius
|
||||||
|
} else {
|
||||||
|
value.top_radius * factor
|
||||||
|
};
|
||||||
|
let Some(current) = matrix(value.base.transform) else {
|
||||||
|
return Some(false);
|
||||||
|
};
|
||||||
|
value.radius = if next_inscribed { base_display } else { base_display / factor };
|
||||||
|
value.top_radius = if next_inscribed { top_display } else { top_display / factor };
|
||||||
|
let half = std::f64::consts::PI / value.sides.clamp(3, 32) as f64;
|
||||||
|
let delta = if next_inscribed { half } else { -half };
|
||||||
|
value.base.transform =
|
||||||
|
(current * glam::DMat4::from_rotation_z(delta)).to_cols_array();
|
||||||
|
value.operation_minor = if next_inscribed { -1 } else { 0 };
|
||||||
|
return Some(true);
|
||||||
|
}
|
||||||
|
if field == PROP_ROTATION {
|
||||||
|
let target = crate::entities::common::parse_direction(text)?;
|
||||||
|
if !target.is_finite() {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let current = matrix(value.base.transform)?;
|
||||||
|
let projected = current.x_axis.truncate();
|
||||||
|
if projected.length_squared() <= 1e-12 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let current_angle = projected.y.atan2(projected.x);
|
||||||
|
let target_vertex_angle = if pyramid_is_inscribed(value) {
|
||||||
|
target
|
||||||
|
} else {
|
||||||
|
target - std::f64::consts::PI / value.sides.clamp(3, 32) as f64
|
||||||
|
};
|
||||||
|
let delta = (target_vertex_angle - current_angle + std::f64::consts::PI)
|
||||||
|
.rem_euclid(std::f64::consts::TAU)
|
||||||
|
- std::f64::consts::PI;
|
||||||
|
if delta.abs() <= 1e-12 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let updated = current * glam::DMat4::from_rotation_z(delta);
|
||||||
|
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
value.base.transform = updated.to_cols_array();
|
||||||
|
return Some(true);
|
||||||
|
}
|
||||||
|
let axis = match field {
|
||||||
|
PROP_POSITION_X => 0,
|
||||||
|
PROP_POSITION_Y => 1,
|
||||||
|
PROP_POSITION_Z => 2,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let target = crate::entities::common::parse_length(text)?;
|
||||||
|
if !target.is_finite() {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let current = matrix(value.base.transform)?;
|
||||||
|
let origin = current.transform_point3(glam::DVec3::ZERO);
|
||||||
|
if !origin.is_finite() || (target - origin[axis]).abs() <= 1e-12 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let mut delta = glam::DVec3::ZERO;
|
||||||
|
delta[axis] = target - origin[axis];
|
||||||
|
let updated = glam::DMat4::from_translation(delta) * current;
|
||||||
|
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
value.base.transform = updated.to_cols_array();
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_pyramid_sides(value: &mut SolidHistoryPyramid, sides: i32) -> bool {
|
||||||
|
if !(3..=32).contains(&sides) || sides == value.sides {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !pyramid_is_inscribed(value) {
|
||||||
|
let Some(current) = matrix(value.base.transform) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let old_factor = pyramid_apothem_factor(value.sides);
|
||||||
|
let new_factor = pyramid_apothem_factor(sides);
|
||||||
|
value.radius = value.radius * old_factor / new_factor;
|
||||||
|
value.top_radius = value.top_radius * old_factor / new_factor;
|
||||||
|
let old_half = std::f64::consts::PI / value.sides.clamp(3, 32) as f64;
|
||||||
|
let new_half = std::f64::consts::PI / sides as f64;
|
||||||
|
value.base.transform =
|
||||||
|
(current * glam::DMat4::from_rotation_z(old_half - new_half)).to_cols_array();
|
||||||
|
}
|
||||||
|
value.sides = sides;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_primitive_property(
|
pub fn apply_primitive_property(
|
||||||
operation: &mut SolidHistoryOperation,
|
operation: &mut SolidHistoryOperation,
|
||||||
field: &str,
|
field: &str,
|
||||||
@@ -1031,6 +1352,16 @@ pub fn apply_primitive_property(
|
|||||||
return applied;
|
return applied;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let SolidHistoryOperation::Torus(torus_value) = operation {
|
||||||
|
if let Some(applied) = apply_torus_position_property(torus_value, field, value) {
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let SolidHistoryOperation::Pyramid(pyramid_value) = operation {
|
||||||
|
if let Some(applied) = apply_pyramid_geometry_property(pyramid_value, field, value) {
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
}
|
||||||
let Some(number) = (if field == PROP_SIDES {
|
let Some(number) = (if field == PROP_SIDES {
|
||||||
value.trim().parse::<f64>().ok()
|
value.trim().parse::<f64>().ok()
|
||||||
} else {
|
} else {
|
||||||
@@ -1155,46 +1486,55 @@ pub fn apply_primitive_property(
|
|||||||
}
|
}
|
||||||
value.radius = radius;
|
value.radius = radius;
|
||||||
}
|
}
|
||||||
SolidHistoryOperation::Torus(value) => {
|
SolidHistoryOperation::Torus(value) => match field {
|
||||||
let outer = value.major_radius + value.minor_radius;
|
PROP_MAJOR_RADIUS => {
|
||||||
let inner = (value.major_radius - value.minor_radius).max(0.0);
|
let Some(radius) = positive() else {
|
||||||
match field {
|
return false;
|
||||||
PROP_OUTER_RADIUS => {
|
};
|
||||||
let Some(next_outer) = positive() else {
|
if (radius - value.major_radius).abs() <= 1e-12 {
|
||||||
return false;
|
return false;
|
||||||
};
|
|
||||||
if next_outer <= inner {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
value.major_radius = (next_outer + inner) * 0.5;
|
|
||||||
value.minor_radius = (next_outer - inner) * 0.5;
|
|
||||||
}
|
}
|
||||||
PROP_INNER_RADIUS => {
|
value.major_radius = radius;
|
||||||
let Some(next_inner) = positive() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if next_inner >= outer {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
value.major_radius = (outer + next_inner) * 0.5;
|
|
||||||
value.minor_radius = (outer - next_inner) * 0.5;
|
|
||||||
}
|
|
||||||
_ => return false,
|
|
||||||
}
|
}
|
||||||
}
|
PROP_MINOR_RADIUS => {
|
||||||
|
let Some(radius) = positive() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (radius - value.minor_radius).abs() <= 1e-12 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value.minor_radius = radius;
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
},
|
||||||
SolidHistoryOperation::Pyramid(value) => match field {
|
SolidHistoryOperation::Pyramid(value) => match field {
|
||||||
PROP_RADIUS => value.radius = positive().unwrap_or(value.radius),
|
PROP_BASE_RADIUS => {
|
||||||
|
let Some(displayed) = positive() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
value.radius = if pyramid_is_inscribed(value) {
|
||||||
|
displayed
|
||||||
|
} else {
|
||||||
|
displayed / pyramid_apothem_factor(value.sides)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
PROP_TOP_RADIUS => {
|
||||||
|
if number < 0.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value.top_radius = if pyramid_is_inscribed(value) {
|
||||||
|
number
|
||||||
|
} else {
|
||||||
|
number / pyramid_apothem_factor(value.sides)
|
||||||
|
};
|
||||||
|
}
|
||||||
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
|
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
|
||||||
PROP_SIDES => {
|
PROP_SIDES => {
|
||||||
let rounded = number.round();
|
let rounded = number.round();
|
||||||
if (number - rounded).abs() > 1e-9 {
|
if (number - rounded).abs() > 1e-9 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let sides = rounded as i32;
|
return set_pyramid_sides(value, rounded as i32);
|
||||||
if !(3..=71).contains(&sides) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
value.sides = sides;
|
|
||||||
}
|
}
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
@@ -1467,22 +1807,18 @@ pub fn primitive_grips(
|
|||||||
),
|
),
|
||||||
SolidHistoryOperation::Torus(value) => {
|
SolidHistoryOperation::Torus(value) => {
|
||||||
add(
|
add(
|
||||||
GRIP_OUTER_RADIUS,
|
GRIP_MAJOR_RADIUS,
|
||||||
value.base.transform,
|
value.base.transform,
|
||||||
[value.major_radius + value.minor_radius, 0.0, 0.0],
|
[value.major_radius, 0.0, 0.0],
|
||||||
GripShape::Square,
|
GripShape::Square,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
GRIP_INNER_RADIUS,
|
GRIP_MINOR_RADIUS,
|
||||||
value.base.transform,
|
value.base.transform,
|
||||||
[
|
[value.major_radius, 0.0, value.minor_radius],
|
||||||
(value.major_radius - value.minor_radius).max(0.0),
|
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
],
|
|
||||||
GripShape::Square,
|
GripShape::Square,
|
||||||
None,
|
Some([0.0, 0.0, 1.0]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
SolidHistoryOperation::Pyramid(value) => {
|
SolidHistoryOperation::Pyramid(value) => {
|
||||||
@@ -1493,6 +1829,13 @@ pub fn primitive_grips(
|
|||||||
GripShape::Square,
|
GripShape::Square,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
add(
|
||||||
|
GRIP_TOP_RADIUS,
|
||||||
|
value.base.transform,
|
||||||
|
[value.top_radius, 0.0, value.height],
|
||||||
|
GripShape::Square,
|
||||||
|
None,
|
||||||
|
);
|
||||||
add(
|
add(
|
||||||
GRIP_HEIGHT,
|
GRIP_HEIGHT,
|
||||||
value.base.transform,
|
value.base.transform,
|
||||||
@@ -1500,7 +1843,7 @@ pub fn primitive_grips(
|
|||||||
GripShape::Square,
|
GripShape::Square,
|
||||||
Some([0.0, 0.0, 1.0]),
|
Some([0.0, 0.0, 1.0]),
|
||||||
);
|
);
|
||||||
let angle = (value.sides.clamp(3, 71) as f64 * 5.0).to_radians();
|
let angle = (value.sides.clamp(3, 32) as f64 * 5.0).to_radians();
|
||||||
add(
|
add(
|
||||||
GRIP_SIDES,
|
GRIP_SIDES,
|
||||||
value.base.transform,
|
value.base.transform,
|
||||||
@@ -1644,33 +1987,22 @@ pub fn apply_primitive_grip(
|
|||||||
value.radius = local.length().max(1e-6);
|
value.radius = local.length().max(1e-6);
|
||||||
}
|
}
|
||||||
SolidHistoryOperation::Torus(value) => match grip_id {
|
SolidHistoryOperation::Torus(value) => match grip_id {
|
||||||
GRIP_OUTER_RADIUS => {
|
GRIP_MAJOR_RADIUS => value.major_radius = local.x.hypot(local.y).max(1e-6),
|
||||||
let outer = local.x.hypot(local.y).max(1e-6);
|
GRIP_MINOR_RADIUS => value.minor_radius = local.z.abs().max(1e-6),
|
||||||
let inner = (value.major_radius - value.minor_radius).max(1e-6);
|
|
||||||
if outer <= inner {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
value.major_radius = (outer + inner) * 0.5;
|
|
||||||
value.minor_radius = (outer - inner) * 0.5;
|
|
||||||
}
|
|
||||||
GRIP_INNER_RADIUS => {
|
|
||||||
let inner = local.x.hypot(local.y).max(1e-6);
|
|
||||||
let outer = value.major_radius + value.minor_radius;
|
|
||||||
if inner >= outer {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
value.major_radius = (outer + inner) * 0.5;
|
|
||||||
value.minor_radius = (outer - inner) * 0.5;
|
|
||||||
}
|
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
SolidHistoryOperation::Pyramid(value) => match grip_id {
|
SolidHistoryOperation::Pyramid(value) => match grip_id {
|
||||||
GRIP_RADIUS => value.radius = local.x.hypot(local.y).max(1e-6),
|
GRIP_RADIUS => value.radius = local.x.hypot(local.y).max(1e-6),
|
||||||
|
GRIP_TOP_RADIUS => {
|
||||||
|
value.top_radius = local.x.hypot(local.y).max(0.0);
|
||||||
|
}
|
||||||
GRIP_HEIGHT => value.height = local.z.max(1e-6),
|
GRIP_HEIGHT => value.height = local.z.max(1e-6),
|
||||||
GRIP_SIDES => {
|
GRIP_SIDES => {
|
||||||
let angle = local.y.atan2(local.x).rem_euclid(std::f64::consts::TAU);
|
let angle = local.y.atan2(local.x).rem_euclid(std::f64::consts::TAU);
|
||||||
value.sides = (angle.to_degrees() / 5.0).round() as i32;
|
let sides = ((angle.to_degrees() / 5.0).round() as i32).clamp(3, 32);
|
||||||
value.sides = value.sides.clamp(3, 71);
|
if !set_pyramid_sides(value, sides) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
@@ -1810,15 +2142,19 @@ pub fn torus_op(
|
|||||||
pub fn pyramid_op(
|
pub fn pyramid_op(
|
||||||
transform: [f64; 16],
|
transform: [f64; 16],
|
||||||
radius: f64,
|
radius: f64,
|
||||||
|
top_radius: f64,
|
||||||
height: f64,
|
height: f64,
|
||||||
sides: usize,
|
sides: usize,
|
||||||
|
inscribed: bool,
|
||||||
) -> SolidHistoryOperation {
|
) -> SolidHistoryOperation {
|
||||||
SolidHistoryOperation::Pyramid(SolidHistoryPyramid {
|
SolidHistoryOperation::Pyramid(SolidHistoryPyramid {
|
||||||
base: base(transform),
|
base: base(transform),
|
||||||
operation_major: 1,
|
operation_major: 1,
|
||||||
|
operation_minor: if inscribed { -1 } else { 0 },
|
||||||
height,
|
height,
|
||||||
sides: sides as i32,
|
sides: sides as i32,
|
||||||
radius,
|
radius,
|
||||||
|
top_radius,
|
||||||
..SolidHistoryPyramid::default()
|
..SolidHistoryPyramid::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,17 @@ pub fn pyramid_solid(center: [f64; 3], radius: f64, height: f64, sides: usize) -
|
|||||||
brep::make::pyramid(center, radius, height, sides)
|
brep::make::pyramid(center, radius, height, sides)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Solid pyramid or polygonal frustum on a regular polygon of `sides` corners.
|
||||||
|
pub fn pyramid_frustum_solid(
|
||||||
|
center: [f64; 3],
|
||||||
|
base_radius: f64,
|
||||||
|
top_radius: f64,
|
||||||
|
height: f64,
|
||||||
|
sides: usize,
|
||||||
|
) -> Option<Body> {
|
||||||
|
brep::make::pyramid_frustum(center, base_radius, top_radius, height, sides)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Placement ───────────────────────────────────────────────────────────────
|
// ── Placement ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Moves a body by a rigid transform, given as three axes and an origin.
|
/// Moves a body by a rigid transform, given as three axes and an origin.
|
||||||
|
|||||||
@@ -181,29 +181,7 @@ fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
|
|||||||
EntityType::Circle(value) => Some(EmbeddedEntity::Circle(value.clone())),
|
EntityType::Circle(value) => Some(EmbeddedEntity::Circle(value.clone())),
|
||||||
EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())),
|
EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())),
|
||||||
EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())),
|
EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())),
|
||||||
EntityType::Spline(_) => {
|
EntityType::Spline(value) => Some(EmbeddedEntity::Spline(value.clone())),
|
||||||
let planar = entity_curve(entity)?;
|
|
||||||
let samples = planar.curve.tessellate_within(1e-4);
|
|
||||||
if samples.len() < 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut path = LwPolyline::new();
|
|
||||||
path.vertices = samples
|
|
||||||
.into_iter()
|
|
||||||
.map(|point| LwVertex::new(Vector2::new(point[0], point[1])))
|
|
||||||
.collect();
|
|
||||||
path.is_closed = planar.curve.is_closed();
|
|
||||||
let plane = crate::command::WorkingPlane::new(
|
|
||||||
glam::DVec3::from_array(planar.plane.origin),
|
|
||||||
glam::DVec3::from_array(planar.plane.x_axis),
|
|
||||||
glam::DVec3::from_array(planar.plane.y_axis),
|
|
||||||
);
|
|
||||||
let EntityType::LwPolyline(path) = plane.place_entity(EntityType::LwPolyline(path))
|
|
||||||
else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
Some(EmbeddedEntity::LwPolyline(path))
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +209,7 @@ pub fn polysolid(
|
|||||||
+ Vec3::from(planar.plane.y_axis) * tangent_local.y)
|
+ Vec3::from(planar.plane.y_axis) * tangent_local.y)
|
||||||
.normalize()?;
|
.normalize()?;
|
||||||
let normal = Vec3::from(planar.plane.normal()?).normalize()?;
|
let normal = Vec3::from(planar.plane.normal()?).normalize()?;
|
||||||
let side = tangent.cross(normal).normalize()?;
|
let side = normal.cross(tangent).normalize()?;
|
||||||
let offset = match justification {
|
let offset = match justification {
|
||||||
PolysolidJustification::Left => 0.0,
|
PolysolidJustification::Left => 0.0,
|
||||||
PolysolidJustification::Center => -width * 0.5,
|
PolysolidJustification::Center => -width * 0.5,
|
||||||
|
|||||||
@@ -343,11 +343,11 @@ pub struct MeshBatchChunk {
|
|||||||
/// erase — the geometry behind them.
|
/// erase — the geometry behind them.
|
||||||
pub transp_index_buffer: wgpu::Buffer,
|
pub transp_index_buffer: wgpu::Buffer,
|
||||||
pub transp_index_count: u32,
|
pub transp_index_count: u32,
|
||||||
/// Triangle-edge line list for plain meshes that carry no B-rep edges.
|
/// Expanded triangle-edge line list for plain meshes that carry no B-rep
|
||||||
/// The dedicated vertex buffer matches the edge pipeline layout.
|
/// edges. Keeping the edge endpoints together avoids coupling this buffer
|
||||||
|
/// to the surface mesh's indexing and vertex representation.
|
||||||
pub wire_vertex_buffer: wgpu::Buffer,
|
pub wire_vertex_buffer: wgpu::Buffer,
|
||||||
pub wire_index_buffer: wgpu::Buffer,
|
pub wire_vertex_count: u32,
|
||||||
pub wire_index_count: u32,
|
|
||||||
/// B-rep feature edges of ACIS solids, as a standalone LineList vertex
|
/// B-rep feature edges of ACIS solids, as a standalone LineList vertex
|
||||||
/// buffer (pairs of endpoints), drawn non-indexed. Empty for plain meshes.
|
/// buffer (pairs of endpoints), drawn non-indexed. Empty for plain meshes.
|
||||||
pub edge_vertex_buffer: wgpu::Buffer,
|
pub edge_vertex_buffer: wgpu::Buffer,
|
||||||
@@ -438,7 +438,7 @@ fn make_chunk(
|
|||||||
queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data));
|
queue.write_buffer(&buffer, 0, bytemuck::cast_slice(data));
|
||||||
buffer
|
buffer
|
||||||
};
|
};
|
||||||
let compact_vertices = !material_has_textures(material) && wire_indices.is_empty();
|
let compact_vertices = !material_has_textures(material);
|
||||||
let mk_plain_vertex = |data: &[MeshVertex]| {
|
let mk_plain_vertex = |data: &[MeshVertex]| {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
return stubs.vertex.clone();
|
return stubs.vertex.clone();
|
||||||
@@ -479,18 +479,22 @@ fn make_chunk(
|
|||||||
} else {
|
} else {
|
||||||
instances
|
instances
|
||||||
};
|
};
|
||||||
let wire_vertices: Vec<MeshEdgeVertex> = if wire_indices.is_empty() {
|
let mut wire_vertices = Vec::with_capacity(wire_indices.len());
|
||||||
Vec::new()
|
for line in wire_indices.chunks_exact(2) {
|
||||||
} else {
|
let (Some(start), Some(end)) = (
|
||||||
verts
|
verts.get(line[0] as usize),
|
||||||
.iter()
|
verts.get(line[1] as usize),
|
||||||
.map(|vertex| MeshEdgeVertex {
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for vertex in [start, end] {
|
||||||
|
wire_vertices.push(MeshEdgeVertex {
|
||||||
position: vertex.position,
|
position: vertex.position,
|
||||||
color: face_color,
|
color: face_color,
|
||||||
position_low: vertex.position_low,
|
position_low: vertex.position_low,
|
||||||
})
|
});
|
||||||
.collect()
|
}
|
||||||
};
|
}
|
||||||
MeshBatchChunk {
|
MeshBatchChunk {
|
||||||
vertex_buffer: vertex_buffer_override.unwrap_or_else(|| {
|
vertex_buffer: vertex_buffer_override.unwrap_or_else(|| {
|
||||||
if compact_vertices {
|
if compact_vertices {
|
||||||
@@ -505,8 +509,7 @@ fn make_chunk(
|
|||||||
transp_index_buffer: mk_index(transp_indices, "mesh.batch.transp_ibuf"),
|
transp_index_buffer: mk_index(transp_indices, "mesh.batch.transp_ibuf"),
|
||||||
transp_index_count: transp_indices.len() as u32,
|
transp_index_count: transp_indices.len() as u32,
|
||||||
wire_vertex_buffer: mk_edge_vertex(&wire_vertices, "mesh.batch.wire_vbuf"),
|
wire_vertex_buffer: mk_edge_vertex(&wire_vertices, "mesh.batch.wire_vbuf"),
|
||||||
wire_index_buffer: mk_index(wire_indices, "mesh.batch.wire_ibuf"),
|
wire_vertex_count: wire_vertices.len() as u32,
|
||||||
wire_index_count: wire_indices.len() as u32,
|
|
||||||
edge_vertex_buffer: edge_buffer_override
|
edge_vertex_buffer: edge_buffer_override
|
||||||
.unwrap_or_else(|| mk_edge_vertex(edge_verts, "mesh.batch.edge_vbuf")),
|
.unwrap_or_else(|| mk_edge_vertex(edge_verts, "mesh.batch.edge_vbuf")),
|
||||||
edge_vertex_count: edge_verts.len() as u32,
|
edge_vertex_count: edge_verts.len() as u32,
|
||||||
@@ -1169,8 +1172,7 @@ fn build_instanced_chunk(
|
|||||||
let material = first.material;
|
let material = first.material;
|
||||||
let color = first.color;
|
let color = first.color;
|
||||||
let source_handle = source.handle.value();
|
let source_handle = source.handle.value();
|
||||||
let compact_vertices = !material_has_textures(material)
|
let compact_vertices = !material_has_textures(material);
|
||||||
&& (!first.include_edges || !source.edge_verts.is_empty());
|
|
||||||
let material_identity = if material_has_textures(material) {
|
let material_identity = if material_has_textures(material) {
|
||||||
material.map_or(0, |material| {
|
material.map_or(0, |material| {
|
||||||
material
|
material
|
||||||
@@ -1582,7 +1584,10 @@ pub fn build_mesh_batch_filtered(
|
|||||||
let budget = hard_budget.min(32 * 1024 * 1024);
|
let budget = hard_budget.min(32 * 1024 * 1024);
|
||||||
let vsize = std::mem::size_of::<MeshVertex>();
|
let vsize = std::mem::size_of::<MeshVertex>();
|
||||||
let max_verts = (budget / vsize).max(3);
|
let max_verts = (budget / vsize).max(3);
|
||||||
let max_tris = (budget / (6 * 4)).max(1); // wire-index buffer: 6 u32 per tri
|
// A triangle contributes three line segments (six standalone edge
|
||||||
|
// vertices). Bound chunks by the largest buffer produced for wire meshes.
|
||||||
|
let max_tris =
|
||||||
|
(budget / (6 * std::mem::size_of::<MeshEdgeVertex>())).max(1);
|
||||||
let stubs = MeshBatchStubs::new(device);
|
let stubs = MeshBatchStubs::new(device);
|
||||||
|
|
||||||
let mut chunks = Vec::new();
|
let mut chunks = Vec::new();
|
||||||
@@ -1796,18 +1801,27 @@ pub fn build_mesh_batch_filtered(
|
|||||||
.len()
|
.len()
|
||||||
.saturating_mul(std::mem::size_of::<MeshVertex>())
|
.saturating_mul(std::mem::size_of::<MeshVertex>())
|
||||||
<= hard_budget
|
<= hard_budget
|
||||||
&& part.indices.len().saturating_mul(2 * std::mem::size_of::<u32>())
|
&& part.indices.len().saturating_mul(std::mem::size_of::<u32>())
|
||||||
<= hard_budget
|
<= hard_budget
|
||||||
&& part
|
&& part
|
||||||
.set
|
.set
|
||||||
.instance_source
|
.instance_source
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|source| {
|
.is_some_and(|source| {
|
||||||
source
|
let feature_edges_fit = source
|
||||||
.edge_verts
|
.edge_verts
|
||||||
.len()
|
.len()
|
||||||
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
|
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
|
||||||
<= hard_budget
|
<= hard_budget;
|
||||||
|
let triangle_edges_fit = !part.include_edges
|
||||||
|
|| !source.edge_verts.is_empty()
|
||||||
|
|| part
|
||||||
|
.indices
|
||||||
|
.len()
|
||||||
|
.saturating_mul(2)
|
||||||
|
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
|
||||||
|
<= hard_budget;
|
||||||
|
feature_edges_fit && triangle_edges_fit
|
||||||
})
|
})
|
||||||
&& part
|
&& part
|
||||||
.indices
|
.indices
|
||||||
@@ -2152,8 +2166,8 @@ pub fn build_mesh_batch_filtered(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush when adding this mesh would overflow either the vertex buffer
|
// Flush when adding this mesh would overflow either the surface vertex
|
||||||
// or the wire-index buffer.
|
// buffer or the expanded wire-vertex buffer.
|
||||||
if !verts.is_empty()
|
if !verts.is_empty()
|
||||||
&& (verts.len() + mesh.verts.len() > max_verts
|
&& (verts.len() + mesh.verts.len() > max_verts
|
||||||
|| wire_indices.len() / 6 + mesh_tris > max_tris)
|
|| wire_indices.len() / 6 + mesh_tris > max_tris)
|
||||||
|
|||||||
@@ -3750,7 +3750,7 @@ impl Pipeline {
|
|||||||
// Four draw paths share this pass:
|
// Four draw paths share this pass:
|
||||||
// - Solid: `mesh_pipeline` + triangle index buf.
|
// - Solid: `mesh_pipeline` + triangle index buf.
|
||||||
// - Wireframe: `mesh_wireframe_pipeline` + the
|
// - Wireframe: `mesh_wireframe_pipeline` + the
|
||||||
// pre-built `wire_index_buffer`.
|
// pre-built expanded wire vertex buffer.
|
||||||
// - HiddenLine: depth prepass (`mesh_depth_pipeline`,
|
// - HiddenLine: depth prepass (`mesh_depth_pipeline`,
|
||||||
// writes Z, no colour) → wire overlay.
|
// writes Z, no colour) → wire overlay.
|
||||||
// - Solid+Edges: `mesh_pipeline` shaded fill → wire
|
// - Solid+Edges: `mesh_pipeline` shaded fill → wire
|
||||||
@@ -3809,17 +3809,9 @@ impl Pipeline {
|
|||||||
);
|
);
|
||||||
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
|
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
|
||||||
// Plain-mesh triangulation edges.
|
// Plain-mesh triangulation edges.
|
||||||
if c.wire_index_count != 0 {
|
if c.wire_vertex_count != 0 {
|
||||||
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
|
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
|
||||||
pass.set_index_buffer(
|
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
|
||||||
c.wire_index_buffer.slice(..),
|
|
||||||
wgpu::IndexFormat::Uint32,
|
|
||||||
);
|
|
||||||
pass.draw_indexed(
|
|
||||||
0..c.wire_index_count,
|
|
||||||
0,
|
|
||||||
0..c.instance_count,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// ACIS solid B-rep feature edges (LineList, non-indexed).
|
// ACIS solid B-rep feature edges (LineList, non-indexed).
|
||||||
if c.edge_vertex_count != 0 {
|
if c.edge_vertex_count != 0 {
|
||||||
@@ -3846,17 +3838,9 @@ impl Pipeline {
|
|||||||
&[],
|
&[],
|
||||||
);
|
);
|
||||||
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
|
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
|
||||||
if c.wire_index_count != 0 {
|
if c.wire_vertex_count != 0 {
|
||||||
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
|
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
|
||||||
pass.set_index_buffer(
|
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
|
||||||
c.wire_index_buffer.slice(..),
|
|
||||||
wgpu::IndexFormat::Uint32,
|
|
||||||
);
|
|
||||||
pass.draw_indexed(
|
|
||||||
0..c.wire_index_count,
|
|
||||||
0,
|
|
||||||
0..c.instance_count,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if c.edge_vertex_count != 0 {
|
if c.edge_vertex_count != 0 {
|
||||||
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
|
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
|
||||||
@@ -4002,17 +3986,9 @@ impl Pipeline {
|
|||||||
&[],
|
&[],
|
||||||
);
|
);
|
||||||
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
|
pass.set_vertex_buffer(1, c.instance_buffer.slice(..));
|
||||||
if c.wire_index_count != 0 {
|
if c.wire_vertex_count != 0 {
|
||||||
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
|
pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..));
|
||||||
pass.set_index_buffer(
|
pass.draw(0..c.wire_vertex_count, 0..c.instance_count);
|
||||||
c.wire_index_buffer.slice(..),
|
|
||||||
wgpu::IndexFormat::Uint32,
|
|
||||||
);
|
|
||||||
pass.draw_indexed(
|
|
||||||
0..c.wire_index_count,
|
|
||||||
0,
|
|
||||||
0..c.instance_count,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if c.edge_vertex_count != 0 {
|
if c.edge_vertex_count != 0 {
|
||||||
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
|
pass.set_vertex_buffer(0, c.edge_vertex_buffer.slice(..));
|
||||||
|
|||||||
+165
-36
@@ -15,6 +15,7 @@ use crate::scene::view::render::{
|
|||||||
has_resolved_book_color, is_effective_layer_zero, layer_render_style_viewport,
|
has_resolved_book_color, is_effective_layer_zero, layer_render_style_viewport,
|
||||||
render_style_for_block_sub_viewport, render_style_for_viewport, InheritStyle,
|
render_style_for_block_sub_viewport, render_style_for_viewport, InheritStyle,
|
||||||
};
|
};
|
||||||
|
use crate::scene::BlockScalePolicy;
|
||||||
|
|
||||||
pub type ResolvedStyle = ([f32; 4], f32, [f32; 8], f32, u8);
|
pub type ResolvedStyle = ([f32; 4], f32, [f32; 8], f32, u8);
|
||||||
|
|
||||||
@@ -216,7 +217,7 @@ fn layer_aci(document: &CadDocument, layer: &str) -> u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RenderContext {
|
pub struct InstanceContext {
|
||||||
pub transform: Transform,
|
pub transform: Transform,
|
||||||
pub root_handle: Handle,
|
pub root_handle: Handle,
|
||||||
pub parent_insert: Handle,
|
pub parent_insert: Handle,
|
||||||
@@ -227,9 +228,10 @@ pub struct RenderContext {
|
|||||||
pub depth_scale: f32,
|
pub depth_scale: f32,
|
||||||
pub nesting_depth: usize,
|
pub nesting_depth: usize,
|
||||||
pub viewport: Option<Handle>,
|
pub viewport: Option<Handle>,
|
||||||
|
pub scale_policy: BlockScalePolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderContext {
|
impl InstanceContext {
|
||||||
fn direct(depth_base: f32, viewport: Option<Handle>) -> Self {
|
fn direct(depth_base: f32, viewport: Option<Handle>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
transform: Transform::identity(),
|
transform: Transform::identity(),
|
||||||
@@ -242,6 +244,7 @@ impl RenderContext {
|
|||||||
depth_scale: 1.0,
|
depth_scale: 1.0,
|
||||||
nesting_depth: 0,
|
nesting_depth: 0,
|
||||||
viewport,
|
viewport,
|
||||||
|
scale_policy: BlockScalePolicy::FromInsert,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,8 +317,8 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
/// false for an Insert removes its whole subtree.
|
/// false for an Insert removes its whole subtree.
|
||||||
pub fn walk_root<V, F>(&self, root: SceneRoot, mut visible: V, mut leaf: F)
|
pub fn walk_root<V, F>(&self, root: SceneRoot, mut visible: V, mut leaf: F)
|
||||||
where
|
where
|
||||||
V: FnMut(&EntityType, &RenderContext) -> bool,
|
V: FnMut(&EntityType, &InstanceContext) -> bool,
|
||||||
F: FnMut(&EntityType, &RenderContext),
|
F: FnMut(&EntityType, &InstanceContext),
|
||||||
{
|
{
|
||||||
let block = root.content_block();
|
let block = root.content_block();
|
||||||
let viewport = match root {
|
let viewport = match root {
|
||||||
@@ -344,12 +347,18 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
.depths
|
.depths
|
||||||
.get(&handle.value())
|
.get(&handle.value())
|
||||||
.map_or(0.0, |depth| depth[0]);
|
.map_or(0.0, |depth| depth[0]);
|
||||||
let context = RenderContext::direct(direct_depth, viewport);
|
let context = InstanceContext::direct(direct_depth, viewport);
|
||||||
if !self.document_visible(entity) || !visible(entity, &context) {
|
if !self.document_visible(entity) || !visible(entity, &context) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let EntityType::Insert(insert) = entity {
|
if let EntityType::Insert(insert) = entity {
|
||||||
self.walk_insert_instances(insert, &context, &mut visible, &mut leaf);
|
self.walk_insert_instances(
|
||||||
|
insert,
|
||||||
|
BlockScalePolicy::FromInsert,
|
||||||
|
&context,
|
||||||
|
&mut visible,
|
||||||
|
&mut leaf,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
leaf(entity, &context);
|
leaf(entity, &context);
|
||||||
self.walk_owned_content(entity, &context, &mut visible, &mut leaf, &mut Vec::new());
|
self.walk_owned_content(entity, &context, &mut visible, &mut leaf, &mut Vec::new());
|
||||||
@@ -359,15 +368,36 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
|
|
||||||
/// Walk one synthetic or document-owned Insert. Used by entity renderers
|
/// Walk one synthetic or document-owned Insert. Used by entity renderers
|
||||||
/// whose content is itself a block reference.
|
/// whose content is itself a block reference.
|
||||||
pub fn walk_insert<V, F>(
|
pub fn walk_block_use<V, F>(
|
||||||
&self,
|
&self,
|
||||||
insert: &Insert,
|
block_use: &BlockUse,
|
||||||
root_handle: Handle,
|
root_handle: Handle,
|
||||||
mut visible: V,
|
mut visible: V,
|
||||||
mut leaf: F,
|
mut leaf: F,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
V: FnMut(&EntityType, &InstanceContext) -> bool,
|
||||||
|
F: FnMut(&EntityType, &InstanceContext),
|
||||||
|
{
|
||||||
|
self.walk_insert_with_policy(
|
||||||
|
&block_use.insert,
|
||||||
|
root_handle,
|
||||||
|
block_use.scale_policy,
|
||||||
|
&mut visible,
|
||||||
|
&mut leaf,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_insert_with_policy<V, F>(
|
||||||
|
&self,
|
||||||
|
insert: &Insert,
|
||||||
|
root_handle: Handle,
|
||||||
|
scale_policy: BlockScalePolicy,
|
||||||
|
visible: &mut V,
|
||||||
|
leaf: &mut F,
|
||||||
) where
|
) where
|
||||||
V: FnMut(&EntityType, &RenderContext) -> bool,
|
V: FnMut(&EntityType, &InstanceContext) -> bool,
|
||||||
F: FnMut(&EntityType, &RenderContext),
|
F: FnMut(&EntityType, &InstanceContext),
|
||||||
{
|
{
|
||||||
let mut root_insert = insert.clone();
|
let mut root_insert = insert.clone();
|
||||||
root_insert.common.handle = root_handle;
|
root_insert.common.handle = root_handle;
|
||||||
@@ -376,9 +406,15 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
.depths
|
.depths
|
||||||
.get(&root_handle.value())
|
.get(&root_handle.value())
|
||||||
.map_or(0.0, |depth| depth[0]);
|
.map_or(0.0, |depth| depth[0]);
|
||||||
let context = RenderContext::direct(depth_base, self.viewport);
|
let context = InstanceContext::direct(depth_base, self.viewport);
|
||||||
if self.document_visible(&entity) && visible(&entity, &context) {
|
if self.document_visible(&entity) && visible(&entity, &context) {
|
||||||
self.walk_insert_instances(&root_insert, &context, &mut visible, &mut leaf);
|
self.walk_insert_instances(
|
||||||
|
&root_insert,
|
||||||
|
scale_policy,
|
||||||
|
&context,
|
||||||
|
visible,
|
||||||
|
leaf,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,12 +422,13 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
fn walk_insert_instances<V, F>(
|
fn walk_insert_instances<V, F>(
|
||||||
&self,
|
&self,
|
||||||
insert: &Insert,
|
insert: &Insert,
|
||||||
parent: &RenderContext,
|
scale_policy: BlockScalePolicy,
|
||||||
|
parent: &InstanceContext,
|
||||||
visible: &mut V,
|
visible: &mut V,
|
||||||
leaf: &mut F,
|
leaf: &mut F,
|
||||||
) where
|
) where
|
||||||
V: FnMut(&EntityType, &RenderContext) -> bool,
|
V: FnMut(&EntityType, &InstanceContext) -> bool,
|
||||||
F: FnMut(&EntityType, &RenderContext),
|
F: FnMut(&EntityType, &InstanceContext),
|
||||||
{
|
{
|
||||||
let insert_entity = EntityType::Insert(insert.clone());
|
let insert_entity = EntityType::Insert(insert.clone());
|
||||||
let block_style = parent
|
let block_style = parent
|
||||||
@@ -424,6 +461,7 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
insert,
|
insert,
|
||||||
offset,
|
offset,
|
||||||
self.annotation_scale,
|
self.annotation_scale,
|
||||||
|
scale_policy,
|
||||||
);
|
);
|
||||||
let transform = local.then(&parent.transform);
|
let transform = local.then(&parent.transform);
|
||||||
let mut context = parent.clone();
|
let mut context = parent.clone();
|
||||||
@@ -435,6 +473,7 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
context.depth_base = depth_base;
|
context.depth_base = depth_base;
|
||||||
context.depth_scale = depth_scale;
|
context.depth_scale = depth_scale;
|
||||||
context.nesting_depth += 1;
|
context.nesting_depth += 1;
|
||||||
|
context.scale_policy = scale_policy;
|
||||||
if let Some(filter) = crate::scene::pick::xclip::insert_spatial_filter(
|
if let Some(filter) = crate::scene::pick::xclip::insert_spatial_filter(
|
||||||
self.document,
|
self.document,
|
||||||
insert,
|
insert,
|
||||||
@@ -465,13 +504,13 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
fn walk_block<V, F>(
|
fn walk_block<V, F>(
|
||||||
&self,
|
&self,
|
||||||
block_name: &str,
|
block_name: &str,
|
||||||
context: &RenderContext,
|
context: &InstanceContext,
|
||||||
visible: &mut V,
|
visible: &mut V,
|
||||||
leaf: &mut F,
|
leaf: &mut F,
|
||||||
stack: &mut Vec<String>,
|
stack: &mut Vec<String>,
|
||||||
) where
|
) where
|
||||||
V: FnMut(&EntityType, &RenderContext) -> bool,
|
V: FnMut(&EntityType, &InstanceContext) -> bool,
|
||||||
F: FnMut(&EntityType, &RenderContext),
|
F: FnMut(&EntityType, &InstanceContext),
|
||||||
{
|
{
|
||||||
if context.nesting_depth > 32 {
|
if context.nesting_depth > 32 {
|
||||||
return;
|
return;
|
||||||
@@ -504,7 +543,13 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
stack.push(nested.block_name.clone());
|
stack.push(nested.block_name.clone());
|
||||||
self.walk_insert_instances(nested, context, visible, leaf);
|
self.walk_insert_instances(
|
||||||
|
nested,
|
||||||
|
BlockScalePolicy::FromInsert,
|
||||||
|
context,
|
||||||
|
visible,
|
||||||
|
leaf,
|
||||||
|
);
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -518,13 +563,13 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
fn walk_owned_content<V, F>(
|
fn walk_owned_content<V, F>(
|
||||||
&self,
|
&self,
|
||||||
entity: &EntityType,
|
entity: &EntityType,
|
||||||
context: &RenderContext,
|
context: &InstanceContext,
|
||||||
visible: &mut V,
|
visible: &mut V,
|
||||||
leaf: &mut F,
|
leaf: &mut F,
|
||||||
stack: &mut Vec<String>,
|
stack: &mut Vec<String>,
|
||||||
) where
|
) where
|
||||||
V: FnMut(&EntityType, &RenderContext) -> bool,
|
V: FnMut(&EntityType, &InstanceContext) -> bool,
|
||||||
F: FnMut(&EntityType, &RenderContext),
|
F: FnMut(&EntityType, &InstanceContext),
|
||||||
{
|
{
|
||||||
for block_use in entity_render_block_uses(self.document, entity, self.annotation_scale)
|
for block_use in entity_render_block_uses(self.document, entity, self.annotation_scale)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -537,7 +582,13 @@ impl<'a> RenderSceneGraph<'a> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
stack.push(block_use.insert.block_name.clone());
|
stack.push(block_use.insert.block_name.clone());
|
||||||
self.walk_insert_instances(&block_use.insert, context, visible, leaf);
|
self.walk_insert_instances(
|
||||||
|
&block_use.insert,
|
||||||
|
block_use.scale_policy,
|
||||||
|
context,
|
||||||
|
visible,
|
||||||
|
leaf,
|
||||||
|
);
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -595,9 +646,24 @@ pub fn insert_transform_at_scale(
|
|||||||
document: &CadDocument,
|
document: &CadDocument,
|
||||||
insert: &Insert,
|
insert: &Insert,
|
||||||
annotation_scale: f32,
|
annotation_scale: f32,
|
||||||
|
) -> Transform {
|
||||||
|
insert_transform_with_policy(
|
||||||
|
document,
|
||||||
|
insert,
|
||||||
|
annotation_scale,
|
||||||
|
BlockScalePolicy::FromInsert,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_transform_with_policy(
|
||||||
|
document: &CadDocument,
|
||||||
|
insert: &Insert,
|
||||||
|
annotation_scale: f32,
|
||||||
|
scale_policy: BlockScalePolicy,
|
||||||
) -> Transform {
|
) -> Transform {
|
||||||
let mut transform = insert_transform(document, insert);
|
let mut transform = insert_transform(document, insert);
|
||||||
if (annotation_scale - 1.0).abs() > 1.0e-6
|
if scale_policy == BlockScalePolicy::FromInsert
|
||||||
|
&& (annotation_scale - 1.0).abs() > 1.0e-6
|
||||||
&& insert
|
&& insert
|
||||||
.common
|
.common
|
||||||
.extended_data
|
.extended_data
|
||||||
@@ -615,7 +681,7 @@ pub fn insert_transform_at_scale(
|
|||||||
transform
|
transform
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum BlockRole {
|
pub enum BlockRole {
|
||||||
Insert,
|
Insert,
|
||||||
DimensionPicture,
|
DimensionPicture,
|
||||||
@@ -631,6 +697,7 @@ pub struct BlockUse {
|
|||||||
pub block: Handle,
|
pub block: Handle,
|
||||||
pub insert: Insert,
|
pub insert: Insert,
|
||||||
pub role: BlockRole,
|
pub role: BlockRole,
|
||||||
|
pub scale_policy: BlockScalePolicy,
|
||||||
/// Whether this edge belongs to the active render representation. Inactive
|
/// Whether this edge belongs to the active render representation. Inactive
|
||||||
/// edges remain dependencies so cache invalidation and purge stay complete.
|
/// edges remain dependencies so cache invalidation and purge stay complete.
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
@@ -654,6 +721,7 @@ fn block_use(
|
|||||||
document: &CadDocument,
|
document: &CadDocument,
|
||||||
mut insert: Insert,
|
mut insert: Insert,
|
||||||
role: BlockRole,
|
role: BlockRole,
|
||||||
|
scale_policy: BlockScalePolicy,
|
||||||
active: bool,
|
active: bool,
|
||||||
replaces_host_wire: bool,
|
replaces_host_wire: bool,
|
||||||
suppress_root_points: bool,
|
suppress_root_points: bool,
|
||||||
@@ -667,6 +735,7 @@ fn block_use(
|
|||||||
block,
|
block,
|
||||||
insert,
|
insert,
|
||||||
role,
|
role,
|
||||||
|
scale_policy,
|
||||||
active,
|
active,
|
||||||
replaces_host_wire,
|
replaces_host_wire,
|
||||||
suppress_root_points,
|
suppress_root_points,
|
||||||
@@ -687,6 +756,7 @@ pub fn block_use_from_handle(
|
|||||||
document,
|
document,
|
||||||
Insert::new(record.name.clone(), insertion),
|
Insert::new(record.name.clone(), insertion),
|
||||||
role,
|
role,
|
||||||
|
BlockScalePolicy::Applied,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -727,6 +797,7 @@ fn entity_owned_block_uses(
|
|||||||
document,
|
document,
|
||||||
insert.clone(),
|
insert.clone(),
|
||||||
BlockRole::Insert,
|
BlockRole::Insert,
|
||||||
|
BlockScalePolicy::FromInsert,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
@@ -743,6 +814,7 @@ fn entity_owned_block_uses(
|
|||||||
document,
|
document,
|
||||||
insert,
|
insert,
|
||||||
BlockRole::DimensionPicture,
|
BlockRole::DimensionPicture,
|
||||||
|
BlockScalePolicy::FromInsert,
|
||||||
picture,
|
picture,
|
||||||
picture,
|
picture,
|
||||||
true,
|
true,
|
||||||
@@ -771,6 +843,7 @@ fn entity_owned_block_uses(
|
|||||||
document,
|
document,
|
||||||
insert,
|
insert,
|
||||||
BlockRole::TablePicture,
|
BlockRole::TablePicture,
|
||||||
|
BlockScalePolicy::FromInsert,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
@@ -780,15 +853,12 @@ fn entity_owned_block_uses(
|
|||||||
uses.extend(
|
uses.extend(
|
||||||
crate::entities::table::block_cell_inserts(table, document, annotation_scale)
|
crate::entities::table::block_cell_inserts(table, document, annotation_scale)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mut insert| {
|
.map(|insert| {
|
||||||
insert
|
|
||||||
.common
|
|
||||||
.extended_data
|
|
||||||
.remove_record("AcAnnotativeData");
|
|
||||||
block_use(
|
block_use(
|
||||||
document,
|
document,
|
||||||
insert,
|
insert,
|
||||||
BlockRole::TableCell,
|
BlockRole::TableCell,
|
||||||
|
BlockScalePolicy::Applied,
|
||||||
cells_active,
|
cells_active,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -799,15 +869,12 @@ fn entity_owned_block_uses(
|
|||||||
}
|
}
|
||||||
EntityType::MultiLeader(multileader) => {
|
EntityType::MultiLeader(multileader) => {
|
||||||
crate::entities::multileader::block_content_insert(document, multileader)
|
crate::entities::multileader::block_content_insert(document, multileader)
|
||||||
.map(|mut insert| {
|
.map(|insert| {
|
||||||
insert
|
|
||||||
.common
|
|
||||||
.extended_data
|
|
||||||
.remove_record("AcAnnotativeData");
|
|
||||||
vec![block_use(
|
vec![block_use(
|
||||||
document,
|
document,
|
||||||
insert,
|
insert,
|
||||||
BlockRole::MultiLeaderContent,
|
BlockRole::MultiLeaderContent,
|
||||||
|
BlockScalePolicy::Applied,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -997,8 +1064,9 @@ pub fn insert_instance_transform(
|
|||||||
insert: &Insert,
|
insert: &Insert,
|
||||||
offset: [f64; 3],
|
offset: [f64; 3],
|
||||||
annotation_scale: f32,
|
annotation_scale: f32,
|
||||||
|
scale_policy: BlockScalePolicy,
|
||||||
) -> Transform {
|
) -> Transform {
|
||||||
let transform = insert_transform_at_scale(document, insert, annotation_scale);
|
let transform = insert_transform_with_policy(document, insert, annotation_scale, scale_policy);
|
||||||
if offset == [0.0; 3] {
|
if offset == [0.0; 3] {
|
||||||
transform
|
transform
|
||||||
} else {
|
} else {
|
||||||
@@ -1007,6 +1075,32 @@ pub fn insert_instance_transform(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_instance_translation_delta(
|
||||||
|
document: &CadDocument,
|
||||||
|
insert: &Insert,
|
||||||
|
offset: [f64; 3],
|
||||||
|
annotation_scale: f32,
|
||||||
|
scale_policy: BlockScalePolicy,
|
||||||
|
) -> Vector3 {
|
||||||
|
let base = insert_instance_transform(
|
||||||
|
document,
|
||||||
|
insert,
|
||||||
|
[0.0; 3],
|
||||||
|
annotation_scale,
|
||||||
|
scale_policy,
|
||||||
|
)
|
||||||
|
.apply(Vector3::ZERO);
|
||||||
|
let instance = insert_instance_transform(
|
||||||
|
document,
|
||||||
|
insert,
|
||||||
|
offset,
|
||||||
|
annotation_scale,
|
||||||
|
scale_policy,
|
||||||
|
)
|
||||||
|
.apply(Vector3::ZERO);
|
||||||
|
instance - base
|
||||||
|
}
|
||||||
|
|
||||||
pub fn block_contains_hatch(
|
pub fn block_contains_hatch(
|
||||||
document: &CadDocument,
|
document: &CadDocument,
|
||||||
block_name: &str,
|
block_name: &str,
|
||||||
@@ -1035,3 +1129,38 @@ pub fn block_contains_hatch(
|
|||||||
memo.insert(key, contains);
|
memo.insert(key, contains);
|
||||||
contains
|
contains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use acadrust::xdata::ExtendedDataRecord;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn applied_block_scale_skips_annotative_rescaling() {
|
||||||
|
let document = CadDocument::new();
|
||||||
|
let mut insert = Insert::new("BLOCK", Vector3::new(10.0, 20.0, 0.0));
|
||||||
|
insert
|
||||||
|
.common
|
||||||
|
.extended_data
|
||||||
|
.add_record(ExtendedDataRecord::new("AcAnnotativeData"));
|
||||||
|
let point = Vector3::new(1.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
let from_insert = insert_transform_with_policy(
|
||||||
|
&document,
|
||||||
|
&insert,
|
||||||
|
2.0,
|
||||||
|
BlockScalePolicy::FromInsert,
|
||||||
|
)
|
||||||
|
.apply(point);
|
||||||
|
let applied = insert_transform_with_policy(
|
||||||
|
&document,
|
||||||
|
&insert,
|
||||||
|
2.0,
|
||||||
|
BlockScalePolicy::Applied,
|
||||||
|
)
|
||||||
|
.apply(point);
|
||||||
|
|
||||||
|
assert!((from_insert.x - 12.0).abs() < 1.0e-9);
|
||||||
|
assert!((applied.x - 11.0).abs() < 1.0e-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -531,6 +531,17 @@ pub fn themed_success<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a,
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render an icon with the foreground chosen for a success-coloured surface.
|
||||||
|
pub fn themed_success_text<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
|
||||||
|
svg(themed_handle(bytes))
|
||||||
|
.width(size)
|
||||||
|
.height(size)
|
||||||
|
.style(|theme: &Theme, _| svg::Style {
|
||||||
|
color: Some(theme.palette().success.base.text),
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
/// Render a warning-state chrome icon from the active Iced theme.
|
/// Render a warning-state chrome icon from the active Iced theme.
|
||||||
pub fn themed_warning<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
|
pub fn themed_warning<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
|
||||||
svg(themed_handle(bytes))
|
svg(themed_handle(bytes))
|
||||||
|
|||||||
+281
-28
@@ -19,13 +19,54 @@ fn muted_style(theme: &Theme) -> iced::widget::text::Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_window(
|
fn danger_text_style(theme: &Theme) -> iced::widget::text::Style {
|
||||||
rows: &[(String, String)],
|
iced::widget::text::Style {
|
||||||
|
color: Some(theme.palette().danger.base.color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Text-input variant flagging an invalid value: danger border and value
|
||||||
|
/// color on the theme's danger weak background.
|
||||||
|
fn danger_input_style(
|
||||||
|
theme: &Theme,
|
||||||
|
status: iced::widget::text_input::Status,
|
||||||
|
) -> iced::widget::text_input::Style {
|
||||||
|
let danger = theme.palette().danger.base;
|
||||||
|
iced::widget::text_input::Style {
|
||||||
|
background: Background::Color(theme.palette().danger.weak.color),
|
||||||
|
border: iced::border::rounded(4)
|
||||||
|
.color(danger.color)
|
||||||
|
.width(
|
||||||
|
if matches!(
|
||||||
|
status,
|
||||||
|
iced::widget::text_input::Status::Focused { is_hovered: _ }
|
||||||
|
) {
|
||||||
|
1.5
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
},
|
||||||
|
),
|
||||||
|
icon: danger.color,
|
||||||
|
placeholder: danger.color.scale_alpha(0.7),
|
||||||
|
value: danger.text,
|
||||||
|
selection: danger.color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_window<'a>(
|
||||||
|
rows: &'a [(String, String)],
|
||||||
|
capture_row: Option<usize>,
|
||||||
|
pending_add: bool,
|
||||||
|
reset_confirm: bool,
|
||||||
|
duplicate_keys: &rustc_hash::FxHashSet<String>,
|
||||||
|
duplicate_conflicts: &[(String, String)],
|
||||||
|
unknown_commands: &rustc_hash::FxHashSet<String>,
|
||||||
|
close_confirm: bool,
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
) -> Element<'_, Message> {
|
) -> Element<'a, Message> {
|
||||||
let title = text(t!("Keyboard Shortcuts")).size(15);
|
let title = text(t!("Keyboard Shortcuts")).size(15);
|
||||||
let hint = text(t!(
|
let hint = text(t!(
|
||||||
"Type SHORTCUTS SET <key> <cmd> to add custom shortcuts."
|
"Click + Add, press a key combination, type the command, then Apply. Esc cancels the pending row."
|
||||||
))
|
))
|
||||||
.size(11)
|
.size(11)
|
||||||
.style(muted_style);
|
.style(muted_style);
|
||||||
@@ -49,15 +90,42 @@ pub fn view_window(
|
|||||||
|
|
||||||
let mut list = column![].spacing(3);
|
let mut list = column![].spacing(3);
|
||||||
for (idx, (key, command)) in rows.iter().enumerate() {
|
for (idx, (key, command)) in rows.iter().enumerate() {
|
||||||
let key_box = text_input("Ctrl+Key", key)
|
// Key cell: click to arm capture, then press the combination. The
|
||||||
.on_input(move |value| Message::ShortcutEditorInput {
|
// armed cell shows what is expected and highlights until a key
|
||||||
idx,
|
// arrives; clicking it again cancels. A key that already exists in
|
||||||
field: ShortcutField::Key,
|
// another row is shown red. Manual key entry is available via
|
||||||
value,
|
// SHORTCUTS SET.
|
||||||
})
|
let armed = capture_row == Some(idx);
|
||||||
.size(13)
|
let duplicate = !key.is_empty() && duplicate_keys.contains(key.as_str());
|
||||||
.padding([3, 6])
|
let key_label = if armed {
|
||||||
.width(Length::Fixed(180.0));
|
t!("Press a key combination...").into_owned()
|
||||||
|
} else if key.is_empty() {
|
||||||
|
t!("Click, then press keys...").into_owned()
|
||||||
|
} else {
|
||||||
|
key.clone()
|
||||||
|
};
|
||||||
|
let key_box = if armed {
|
||||||
|
button(text(key_label).size(13))
|
||||||
|
.on_press(Message::ShortcutCaptureClear)
|
||||||
|
.padding([3, 6])
|
||||||
|
.width(Length::Fixed(180.0))
|
||||||
|
.style(button::primary)
|
||||||
|
} else if duplicate {
|
||||||
|
// The whole field turns red, not just the text.
|
||||||
|
button(text(key_label).size(13))
|
||||||
|
.on_press(Message::ShortcutCaptureStart(idx))
|
||||||
|
.padding([3, 6])
|
||||||
|
.width(Length::Fixed(180.0))
|
||||||
|
.style(button::danger)
|
||||||
|
} else {
|
||||||
|
button(text(key_label).size(13).style(muted_style))
|
||||||
|
.on_press(Message::ShortcutCaptureStart(idx))
|
||||||
|
.padding([3, 6])
|
||||||
|
.width(Length::Fixed(180.0))
|
||||||
|
.style(button::secondary)
|
||||||
|
};
|
||||||
|
let unknown_command =
|
||||||
|
!command.is_empty() && unknown_commands.contains(command.trim());
|
||||||
let command_box = text_input(t!("command").as_ref(), command)
|
let command_box = text_input(t!("command").as_ref(), command)
|
||||||
.on_input(move |value| Message::ShortcutEditorInput {
|
.on_input(move |value| Message::ShortcutEditorInput {
|
||||||
idx,
|
idx,
|
||||||
@@ -67,13 +135,50 @@ pub fn view_window(
|
|||||||
.size(13)
|
.size(13)
|
||||||
.padding([3, 6])
|
.padding([3, 6])
|
||||||
.width(sizing.width);
|
.width(sizing.width);
|
||||||
let remove = button(crate::ui::icons::themed_danger_text(
|
// An unrunnable command turns the whole field red, mirroring the
|
||||||
crate::ui::icons::CLOSE,
|
// duplicate key cell.
|
||||||
12.0,
|
let command_box = if unknown_command {
|
||||||
))
|
command_box.style(danger_input_style)
|
||||||
.on_press(Message::ShortcutEditorRemove(idx))
|
} else {
|
||||||
.padding([2, 6])
|
command_box
|
||||||
.style(button::danger);
|
};
|
||||||
|
// Draft rows get a check (finish the addition, without applying) and
|
||||||
|
// a cancel ✕; committed rows get the trash bin.
|
||||||
|
let remove = if pending_add && idx == 0 {
|
||||||
|
// Done is only clickable for a valid row: key and command filled,
|
||||||
|
// key not already used, command actually runnable.
|
||||||
|
let done_ok = !key.is_empty()
|
||||||
|
&& !command.is_empty()
|
||||||
|
&& !duplicate
|
||||||
|
&& !unknown_commands.contains(command.trim());
|
||||||
|
row![
|
||||||
|
button(crate::ui::icons::themed_success_text(
|
||||||
|
crate::ui::icons::CHECK,
|
||||||
|
12.0,
|
||||||
|
))
|
||||||
|
.on_press_maybe(done_ok.then_some(Message::ShortcutEditorDraftAccept))
|
||||||
|
.padding([2, 6])
|
||||||
|
.style(button::success),
|
||||||
|
button(crate::ui::icons::themed_danger_text(
|
||||||
|
crate::ui::icons::CLOSE,
|
||||||
|
12.0,
|
||||||
|
))
|
||||||
|
.on_press(Message::ShortcutCaptureCancel)
|
||||||
|
.padding([2, 6])
|
||||||
|
.style(button::danger),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
} else {
|
||||||
|
row![button(crate::ui::icons::themed_danger_text(
|
||||||
|
crate::ui::icons::TRASH,
|
||||||
|
12.0,
|
||||||
|
))
|
||||||
|
.on_press(Message::ShortcutEditorRemove(idx))
|
||||||
|
.padding([2, 6])
|
||||||
|
.style(button::danger)]
|
||||||
|
.align_y(iced::Center)
|
||||||
|
};
|
||||||
list = list.push(
|
list = list.push(
|
||||||
row![key_box, command_box, remove]
|
row![key_box, command_box, remove]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
@@ -81,16 +186,100 @@ pub fn view_window(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let add = button(text(format!("+ {}", t!("Add"))).size(12))
|
// While a draft row is pending, the add button doubles as the visible
|
||||||
.on_press(Message::ShortcutEditorAdd)
|
// cancel affordance (Esc works too).
|
||||||
.padding([4, 10])
|
let add = if pending_add {
|
||||||
.style(button::secondary);
|
button(text(t!("Cancel add (Esc)")).size(12))
|
||||||
|
.on_press(Message::ShortcutCaptureCancel)
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(button::danger)
|
||||||
|
} else {
|
||||||
|
button(text(format!("+ {}", t!("Add"))).size(12))
|
||||||
|
.on_press(Message::ShortcutEditorAdd)
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(button::secondary)
|
||||||
|
};
|
||||||
|
// The reset confirmation replaces the shortcut count so the warning
|
||||||
|
// gets the whole left side of the bar.
|
||||||
|
let stats = if reset_confirm {
|
||||||
|
row![Space::new().width(Length::Fixed(0.0))]
|
||||||
|
} else {
|
||||||
|
row![text(format!("{}: {}", t!("Number of shortcuts"), rows.len()))
|
||||||
|
.size(12)
|
||||||
|
.style(muted_style)]
|
||||||
|
};
|
||||||
|
// Reset asks for confirmation in place: the add/reset buttons are
|
||||||
|
// replaced by the question with Yes / No.
|
||||||
|
let (reset_area, add_area) = if reset_confirm {
|
||||||
|
(
|
||||||
|
row![
|
||||||
|
text(t!(
|
||||||
|
"Are you sure you want to reset? You will lose all your current shortcuts!"
|
||||||
|
))
|
||||||
|
.size(12)
|
||||||
|
.style(danger_text_style),
|
||||||
|
button(text(t!("Yes, reset")).size(12))
|
||||||
|
.on_press(Message::ShortcutEditorResetConfirm)
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(button::danger),
|
||||||
|
button(text(t!("No")).size(12))
|
||||||
|
.on_press(Message::ShortcutEditorResetDeny)
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(button::secondary),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![Space::new().width(Length::Fixed(0.0))],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
row![button(text(t!("Reset to default")).size(12))
|
||||||
|
.on_press(Message::ShortcutEditorResetAsk)
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(button::secondary)]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![add],
|
||||||
|
)
|
||||||
|
};
|
||||||
let apply = button(text(t!("Apply")).size(12))
|
let apply = button(text(t!("Apply")).size(12))
|
||||||
.on_press(Message::ShortcutEditorApply)
|
.on_press(Message::ShortcutEditorApply)
|
||||||
.padding([4, 16])
|
.padding([4, 16])
|
||||||
.style(button::primary);
|
.style(button::primary);
|
||||||
|
let apply_exit = button(text(t!("Apply && Exit")).size(12))
|
||||||
|
.on_press(Message::ShortcutEditorApplyExit)
|
||||||
|
.padding([4, 16])
|
||||||
|
.style(button::primary);
|
||||||
|
// The reset confirmation takes over the whole action bar.
|
||||||
|
let apply_area = if reset_confirm {
|
||||||
|
row![Space::new().width(Length::Fixed(0.0))]
|
||||||
|
} else {
|
||||||
|
row![apply, apply_exit].spacing(8).align_y(iced::Center)
|
||||||
|
};
|
||||||
|
|
||||||
container(
|
// Persistent duplicate warning: shown until every conflicting key is
|
||||||
|
// resolved (re-captured, changed, or a row removed).
|
||||||
|
let mut conflict_banner = column![].spacing(2);
|
||||||
|
for (key, command) in duplicate_conflicts {
|
||||||
|
conflict_banner = conflict_banner.push(
|
||||||
|
text(crate::tf!(
|
||||||
|
"Shortcut already used for command: {} → {}",
|
||||||
|
key,
|
||||||
|
command
|
||||||
|
))
|
||||||
|
.size(12)
|
||||||
|
.style(danger_text_style),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for command in unknown_commands {
|
||||||
|
conflict_banner = conflict_banner.push(
|
||||||
|
text(crate::tf!("Unknown command: {}", command))
|
||||||
|
.size(12)
|
||||||
|
.style(danger_text_style),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = container(
|
||||||
column![
|
column![
|
||||||
title,
|
title,
|
||||||
hint,
|
hint,
|
||||||
@@ -98,7 +287,11 @@ pub fn view_window(
|
|||||||
head,
|
head,
|
||||||
scrollable(container(list).padding(gutter)).height(sizing.height),
|
scrollable(container(list).padding(gutter)).height(sizing.height),
|
||||||
Space::new().height(6),
|
Space::new().height(6),
|
||||||
row![add, Space::new().width(sizing.width), apply].align_y(iced::Center),
|
conflict_banner,
|
||||||
|
Space::new().height(4),
|
||||||
|
row![stats, add_area, reset_area, Space::new().width(sizing.width), apply_area]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
]
|
]
|
||||||
.spacing(6)
|
.spacing(6)
|
||||||
.width(sizing.width)
|
.width(sizing.width)
|
||||||
@@ -112,6 +305,66 @@ pub fn view_window(
|
|||||||
theme.palette().background.base.color,
|
theme.palette().background.base.color,
|
||||||
)),
|
)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
});
|
||||||
.into()
|
|
||||||
|
// Unsaved-changes guard: closing with un-applied rows stacks a dimmed
|
||||||
|
// shield and a confirmation panel on top of the editor.
|
||||||
|
if !close_confirm {
|
||||||
|
return content.into();
|
||||||
|
}
|
||||||
|
let shield = iced::widget::mouse_area(
|
||||||
|
container(Space::new())
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
theme
|
||||||
|
.palette()
|
||||||
|
.background
|
||||||
|
.strongest
|
||||||
|
.color
|
||||||
|
.scale_alpha(0.55),
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_press(Message::ShortcutEditorCloseKeep)
|
||||||
|
.interaction(iced::mouse::Interaction::Idle);
|
||||||
|
let panel = container(
|
||||||
|
column![
|
||||||
|
text(t!("Unsaved changes will be discarded.")).size(14),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
button(text(t!("Discard && close")).size(12))
|
||||||
|
.on_press(Message::ShortcutEditorCloseDiscard)
|
||||||
|
.padding([4, 12])
|
||||||
|
.style(button::danger),
|
||||||
|
button(text(t!("Keep editing")).size(12))
|
||||||
|
.on_press(Message::ShortcutEditorCloseKeep)
|
||||||
|
.padding([4, 12])
|
||||||
|
.style(button::secondary),
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
]
|
||||||
|
.spacing(4),
|
||||||
|
)
|
||||||
|
.padding(16)
|
||||||
|
.width(Length::Fixed(320.0))
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
theme.palette().background.base.color,
|
||||||
|
)),
|
||||||
|
border: iced::Border {
|
||||||
|
color: theme.palette().background.neutral.color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 6.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let centered = container(panel)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.align_x(iced::alignment::Horizontal::Center)
|
||||||
|
.align_y(iced::alignment::Vertical::Center);
|
||||||
|
iced::widget::stack![content, iced::widget::opaque(shield), centered].into()
|
||||||
}
|
}
|
||||||
@@ -59,6 +59,7 @@ fn expand_block_mtext(
|
|||||||
&Default::default(),
|
&Default::default(),
|
||||||
);
|
);
|
||||||
expand_insert(
|
expand_insert(
|
||||||
|
&doc,
|
||||||
&cache,
|
&cache,
|
||||||
&ins,
|
&ins,
|
||||||
Handle::new(999),
|
Handle::new(999),
|
||||||
@@ -86,6 +87,8 @@ fn expand_block_mtext(
|
|||||||
// Annotation scale: 1.0 = no annotative scaling, matching the
|
// Annotation scale: 1.0 = no annotative scaling, matching the
|
||||||
// `BlockCache::build(&doc, 1.0, ...)` call above.
|
// `BlockCache::build(&doc, 1.0, ...)` call above.
|
||||||
1.0,
|
1.0,
|
||||||
|
OpenCADStudio::scene::BlockScalePolicy::FromInsert,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.expect("block defn is cached")
|
.expect("block defn is cached")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user