refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
//! `command` arms and helpers, split out of the original `update.rs` (#mechanical decomposition).
|
|
|
|
|
|
|
|
|
|
|
|
#![allow(unused_imports)]
|
|
|
|
|
|
use super::util::*;
|
2026-06-27 11:50:11 +03:00
|
|
|
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
use crate::app::helpers::{
|
|
|
|
|
|
ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
|
|
|
|
|
CoordKind,
|
|
|
|
|
|
};
|
|
|
|
|
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
|
|
|
|
|
use crate::modules::ModuleEvent;
|
|
|
|
|
|
use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit};
|
|
|
|
|
|
use crate::scene::model::object::GripApply;
|
|
|
|
|
|
use crate::scene::{
|
2026-06-27 11:50:11 +03:00
|
|
|
|
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
};
|
|
|
|
|
|
use crate::ui::PropertiesPanel;
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
use crate::ui::window::attribute_editor::{AttrRow, AttrTab};
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
use acadrust::types::Color as AcadColor;
|
|
|
|
|
|
use acadrust::{EntityType as AcadEntityType, Handle};
|
|
|
|
|
|
use iced::time::Instant;
|
|
|
|
|
|
use iced::{mouse, Point, Task};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl OpenCADStudio {
|
|
|
|
|
|
pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|
|
|
|
|
// Start tab is fixed — close requests on it are no-ops.
|
|
|
|
|
|
if self.tabs.get(idx).map_or(false, |t| t.is_start) {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-01 08:05:25 +03:00
|
|
|
|
// Closing a tab shifts indices / the active tab. The attribute
|
|
|
|
|
|
// editor holds a document-local handle into one tab, so drop it
|
|
|
|
|
|
// now rather than risk it applying to a different tab's document.
|
|
|
|
|
|
self.cancel_attr_editor();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if self.tabs.get(idx).map_or(false, |t| t.dirty) {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
|
|
|
|
|
return self.open_unsaved_dialog_window();
|
|
|
|
|
|
}
|
2026-06-29 16:39:42 +03:00
|
|
|
|
// This tab is closing for good — drop its autosave recovery copy.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
let _ = std::fs::remove_file(self.autosave_target(idx));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Only-tab case: when the lone non-start tab closes, fall
|
|
|
|
|
|
// back to the Start tab if it exists; otherwise spawn a
|
|
|
|
|
|
// fresh blank drawing (legacy behaviour).
|
|
|
|
|
|
if self.tabs.len() == 1 {
|
|
|
|
|
|
self.tab_counter += 1;
|
|
|
|
|
|
self.tabs[0] = crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
|
|
|
|
|
self.active_tab = 0;
|
|
|
|
|
|
self.apply_bg_default(0);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs.remove(idx);
|
|
|
|
|
|
if self.active_tab >= self.tabs.len() {
|
|
|
|
|
|
self.active_tab = self.tabs.len() - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// The active tab is now either a brand-new blank or a
|
|
|
|
|
|
// different existing tab; in both cases the ribbon needs
|
|
|
|
|
|
// to track that doc's defaults / selection. #21.
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.sync_ribbon_styles();
|
|
|
|
|
|
self.sync_ribbon_from_selection();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_command_append_char(&mut self, s: String) -> Task<Message> {
|
|
|
|
|
|
// While the MText preview is up, typed glyphs edit it directly.
|
|
|
|
|
|
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
|
|
|
|
|
if s.chars().all(|c| !c.is_control()) {
|
|
|
|
|
|
self.mtext_type(&s);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Filter out control characters — only push the typed
|
|
|
|
|
|
// glyph(s). `Tab`, etc. arrive as Named keys, not here.
|
|
|
|
|
|
if s.chars().all(|c| !c.is_control()) {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// `,` is the coordinate separator in dynamic input,
|
|
|
|
|
|
// not a decimal point: typing it locks the current
|
|
|
|
|
|
// field's buffer and advances to the next coordinate,
|
|
|
|
|
|
// reshaping the field set when going polar → cartesian
|
|
|
|
|
|
// (Distance → X, Y) or 2-D → 3-D (X, Y → X, Y, Z).
|
|
|
|
|
|
// See #35.
|
|
|
|
|
|
if s == "," && self.dyn_input && !self.tabs[i].dyn_fields.is_empty() {
|
|
|
|
|
|
self.dyn_comma_advance();
|
|
|
|
|
|
self.command_line.autocomplete_cursor = None;
|
|
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
|
|
|
|
|
// While dynamic input is showing fields, numeric and
|
|
|
|
|
|
// expression glyphs edit the focused field instead of
|
|
|
|
|
|
// the command line. Letters still go to the command line
|
|
|
|
|
|
// so command-option keywords keep working.
|
|
|
|
|
|
let dyn_field_char = !s.is_empty()
|
|
|
|
|
|
&& s.chars().all(|c| {
|
|
|
|
|
|
c.is_ascii_digit()
|
|
|
|
|
|
|| matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')')
|
|
|
|
|
|
});
|
|
|
|
|
|
if dyn_field_char && self.dyn_input && !self.tabs[i].dyn_fields.is_empty() {
|
|
|
|
|
|
let a = self.tabs[i]
|
|
|
|
|
|
.dyn_active
|
|
|
|
|
|
.min(self.tabs[i].dyn_fields.len() - 1);
|
|
|
|
|
|
self.tabs[i].dyn_fields[a]
|
|
|
|
|
|
.buffer
|
|
|
|
|
|
.get_or_insert_with(String::new)
|
|
|
|
|
|
.push_str(&s);
|
|
|
|
|
|
} else {
|
2026-07-09 13:18:49 +03:00
|
|
|
|
// Command-line entry is shown uppercase.
|
|
|
|
|
|
self.command_line.input.push_str(&s.to_uppercase());
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.autocomplete_cursor = None;
|
|
|
|
|
|
self.focus_cmd_input()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_command_backspace(&mut self) -> Task<Message> {
|
|
|
|
|
|
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
|
|
|
|
|
self.mtext_backspace();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// Backspace edits the focused dynamic-input field first;
|
|
|
|
|
|
// emptying it unlocks the field (back to cursor tracking).
|
|
|
|
|
|
if self.dyn_input && !self.tabs[i].dyn_fields.is_empty() {
|
|
|
|
|
|
let a = self.tabs[i]
|
|
|
|
|
|
.dyn_active
|
|
|
|
|
|
.min(self.tabs[i].dyn_fields.len() - 1);
|
|
|
|
|
|
if let Some(buf) = self.tabs[i].dyn_fields[a].buffer.as_mut() {
|
|
|
|
|
|
buf.pop();
|
|
|
|
|
|
if buf.is_empty() {
|
|
|
|
|
|
self.tabs[i].dyn_fields[a].buffer = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.input.pop();
|
|
|
|
|
|
self.command_line.autocomplete_cursor = None;
|
|
|
|
|
|
self.focus_cmd_input()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_command_submit(&mut self) -> Task<Message> {
|
|
|
|
|
|
// Submitting a command implicitly dismisses the history
|
|
|
|
|
|
// dropdown so the dispatched command's new prompt is
|
|
|
|
|
|
// immediately visible on the overlay.
|
|
|
|
|
|
self.command_line.close_history();
|
2026-07-09 13:18:49 +03:00
|
|
|
|
// A leading `>` was only a "literal spaces" typing hint (see
|
|
|
|
|
|
// CommandSpace) — drop it before the input is interpreted.
|
|
|
|
|
|
if self.command_line.input.starts_with('>') {
|
|
|
|
|
|
self.command_line.input.remove(0);
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Grip-menu value prompt — consume the typed number and
|
|
|
|
|
|
// route it through `apply_grip_menu_value`.
|
|
|
|
|
|
if let Some(pending) = self.grip_pending.take() {
|
|
|
|
|
|
let raw = crate::app::expr_eval::eval_to_string(self.command_line.input.trim());
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
let Ok(v) = raw.parse::<f64>() else {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"{}: expected a number, got \"{raw}\"",
|
|
|
|
|
|
pending.label
|
|
|
|
|
|
));
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
use crate::entities::traits::EntityTypeOps;
|
|
|
|
|
|
self.push_undo_snapshot(i, pending.label);
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(pending.handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
entity.apply_grip_menu_value(pending.grip_id, pending.action, v);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Interactive VPORTS: the entry after a bare `VPORTS` is the
|
|
|
|
|
|
// tiled configuration. Empty input defaults to SINGLE.
|
|
|
|
|
|
if self.awaiting_vports {
|
|
|
|
|
|
self.awaiting_vports = false;
|
|
|
|
|
|
let cfg = self.command_line.input.trim().to_string();
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
let cfg = if cfg.is_empty() {
|
|
|
|
|
|
"SINGLE".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cfg
|
|
|
|
|
|
};
|
|
|
|
|
|
return self.dispatch_command(&format!("VPORTS {cfg}"));
|
|
|
|
|
|
}
|
2026-07-03 12:42:55 +03:00
|
|
|
|
// Interactive system-variable value: after a bare `MIRRTEXT`
|
|
|
|
|
|
// (etc.) the next entry is the new value; empty Enter keeps the
|
|
|
|
|
|
// current setting.
|
|
|
|
|
|
if let Some(name) = self.pending_setvar.take() {
|
|
|
|
|
|
let val = self.command_line.input.trim().to_string();
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
if val.is_empty() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
return self.dispatch_command(&format!("SETVAR {name} {val}"));
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// If the user navigated the autocomplete list with the
|
|
|
|
|
|
// arrow keys, Enter dispatches the highlighted command
|
|
|
|
|
|
// rather than the partial text actually in the buffer.
|
|
|
|
|
|
let i_tab = self.active_tab;
|
|
|
|
|
|
if self.tabs[i_tab].active_cmd.is_none() {
|
2026-07-04 23:01:53 +03:00
|
|
|
|
if let Some(cmd) = self.command_line.selected_suggestion() {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
self.command_line.autocomplete_cursor = None;
|
|
|
|
|
|
return self.dispatch_command(&cmd);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// A whole multi-token command line (`UCS Z 90`, `LINE 0,0
|
|
|
|
|
|
// 10,10`, `PDMODE 3`) — typable now that Space is literal — is
|
|
|
|
|
|
// processed as one unit: feed the tokens to a running command,
|
|
|
|
|
|
// or start a new one through the shared runner that the headless
|
|
|
|
|
|
// automation feeder uses too.
|
|
|
|
|
|
{
|
|
|
|
|
|
// Skip token-splitting when the active command collects
|
|
|
|
|
|
// free-form text with spaces (TEXT / MTEXT / a name) — it
|
|
|
|
|
|
// wants the whole line as one input.
|
|
|
|
|
|
let wants_spaces = self.tabs[i]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|c| c.wants_text_input() && c.wants_text_with_spaces())
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let raw = self.command_line.input.clone();
|
|
|
|
|
|
let toks: Vec<String> = raw.split_whitespace().map(String::from).collect();
|
|
|
|
|
|
if toks.len() > 1 && !wants_spaces {
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
for tok in &toks {
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_none() {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.feed_active_cmd(tok);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
return self.run_command_line(&raw);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// With the command line empty, a typed dynamic-input value
|
|
|
|
|
|
// commits as a point pick instead of an empty submit.
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() && self.command_line.input.trim().is_empty() {
|
|
|
|
|
|
if let Some(task) = self.try_dyn_commit() {
|
|
|
|
|
|
return task;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
let text = crate::app::expr_eval::eval_to_string(self.command_line.input.trim());
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
|
|
|
|
|
|
// Offer the typed text to the command's option handler
|
|
|
|
|
|
// first (keywords like PLINE's A/L/C, a radius, …). If it
|
|
|
|
|
|
// consumes the text we're done; if it returns None the
|
|
|
|
|
|
// text falls through to the Enter / coordinate handling
|
|
|
|
|
|
// below, so a bare Enter still terminates and typed points
|
|
|
|
|
|
// still work when dynamic input is off. See #97.
|
2026-07-21 11:01:51 +03:00
|
|
|
|
// Selection keywords (P / L) at a Select objects prompt
|
|
|
|
|
|
// consume the token before any other interpretation (#426).
|
|
|
|
|
|
if let Some(task) = self.try_selection_keyword(&text) {
|
|
|
|
|
|
return task;
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if self.tabs[i]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|c| c.wants_text_input())
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
{
|
|
|
|
|
|
self.push_ucs_to_cmd(i);
|
|
|
|
|
|
if let Some(result) = self.tabs[i]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_mut()
|
|
|
|
|
|
.and_then(|c| c.on_text_input(&text))
|
|
|
|
|
|
{
|
|
|
|
|
|
return self.apply_cmd_result(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if text.is_empty() {
|
|
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter());
|
|
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
return self.apply_cmd_result(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// OTRACK: while aligned to a tracking ray, a bare distance
|
|
|
|
|
|
// places the point along the ray from the tracking point
|
|
|
|
|
|
// (issue #69).
|
|
|
|
|
|
if let Some((base, dir)) = self.otrack_active {
|
|
|
|
|
|
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
// Typed distance along the tracking ray: keep the
|
|
|
|
|
|
// magnitude exact in f64 (ray origin/dir are the
|
|
|
|
|
|
// screen-space snapper's f32).
|
|
|
|
|
|
let pt = base.as_dvec3() + dir.as_dvec3() * dist;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.last_point = Some(pt);
|
|
|
|
|
|
self.dyn_user_reshaped = false;
|
|
|
|
|
|
self.sync_dyn_fields();
|
|
|
|
|
|
self.reset_tracking_after_point();
|
|
|
|
|
|
self.push_ucs_to_cmd(i);
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
let task = self.apply_cmd_result(r);
|
|
|
|
|
|
self.refresh_active_cmd_preview(i);
|
|
|
|
|
|
return task;
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some((coord, kind)) = parse_coord(&text) {
|
|
|
|
|
|
// Command-line coordinates are absolute by default,
|
|
|
|
|
|
// independent of DYN: `@` forces relative, `#` forces
|
|
|
|
|
|
// absolute, and a bare value is absolute. (Relative-by-
|
|
|
|
|
|
// default lives in the DYN tooltip path — see
|
|
|
|
|
|
// `dyn_resolve_point` — matching AutoCAD, where the
|
|
|
|
|
|
// command line stays absolute regardless of DYN.)
|
|
|
|
|
|
let want_relative = matches!(kind, CoordKind::Relative);
|
|
|
|
|
|
let ucs = self.tabs[i].active_ucs.clone();
|
|
|
|
|
|
let wcs_pt = match (want_relative, self.last_point) {
|
|
|
|
|
|
(true, Some(base)) => {
|
|
|
|
|
|
// Offset from the last point, rotated by the
|
|
|
|
|
|
// UCS axes (no origin translation).
|
|
|
|
|
|
let offset = match &ucs {
|
|
|
|
|
|
Some(u) => ucs_rotate_vec(coord, u),
|
|
|
|
|
|
None => coord,
|
|
|
|
|
|
};
|
|
|
|
|
|
base + offset
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
// Absolute: typed coordinates are in active UCS.
|
|
|
|
|
|
match &ucs {
|
|
|
|
|
|
Some(u) => ucs_to_wcs(coord, u),
|
|
|
|
|
|
None => coord,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
self.last_point = Some(wcs_pt);
|
|
|
|
|
|
self.dyn_user_reshaped = false;
|
|
|
|
|
|
self.sync_dyn_fields();
|
|
|
|
|
|
self.reset_tracking_after_point();
|
|
|
|
|
|
self.push_ucs_to_cmd(i);
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
let task = self.apply_cmd_result(r);
|
|
|
|
|
|
// The rubber-band preview that the command
|
|
|
|
|
|
// last published reflects the *previous*
|
|
|
|
|
|
// last_point — a typed coordinate doesn't
|
|
|
|
|
|
// fire a mouse-move, so re-run the preview
|
|
|
|
|
|
// hook now using the current cursor world
|
|
|
|
|
|
// pos so the next segment immediately starts
|
|
|
|
|
|
// from the just-committed point. See #32.
|
|
|
|
|
|
self.refresh_active_cmd_preview(i);
|
|
|
|
|
|
return task;
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.push_ucs_to_cmd(i);
|
|
|
|
|
|
if let Some(result) = self.tabs[i]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_mut()
|
|
|
|
|
|
.and_then(|c| c.on_text_input(&text))
|
|
|
|
|
|
{
|
|
|
|
|
|
return self.apply_cmd_result(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"Expected coordinates (x,y) or a number, got: \"{text}\""
|
|
|
|
|
|
));
|
|
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(cmd) = self.command_line.submit() {
|
2026-06-27 14:45:51 +03:00
|
|
|
|
return self.dispatch_command_or_suggest(&cmd);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
// Empty Enter / Space with no active command repeats the
|
|
|
|
|
|
// last dispatched command — same shortcut `CommandFinalize`
|
|
|
|
|
|
// already implements, mirrored here so the trailing-space
|
|
|
|
|
|
// submit path goes through it too.
|
|
|
|
|
|
if let Some(cmd) = self.tabs[i].last_cmd.clone() {
|
|
|
|
|
|
return self.dispatch_command(&cmd);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_command_finalize(&mut self) -> Task<Message> {
|
|
|
|
|
|
// In the MText preview, Enter inserts a line break.
|
|
|
|
|
|
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
|
|
|
|
|
self.mtext_type("\n");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Grip popup open → Enter commits the highlighted item.
|
|
|
|
|
|
if self.grip_popup.is_some() {
|
|
|
|
|
|
let idx = self.grip_popup.as_ref().map(|p| p.selected).unwrap_or(0);
|
|
|
|
|
|
return Task::done(Message::GripMenuPick(idx));
|
|
|
|
|
|
}
|
|
|
|
|
|
// Any typed command-line text must be submitted rather than
|
|
|
|
|
|
// finalising. The focused-input Enter routes through
|
|
|
|
|
|
// CommandSubmit, but when the field isn't focused — e.g. at
|
|
|
|
|
|
// startup before the window grabs focus, or a command started
|
|
|
|
|
|
// from the ribbon — its Enter arrives here. Forward a non-empty
|
|
|
|
|
|
// buffer to the same submit path so typing a command name (or
|
|
|
|
|
|
// an option keyword like "R") and pressing Enter works without
|
|
|
|
|
|
// first clicking into the command line (issue #99).
|
|
|
|
|
|
if !self.command_line.input.trim().is_empty() {
|
|
|
|
|
|
return self.update(Message::CommandSubmit);
|
|
|
|
|
|
}
|
|
|
|
|
|
// A typed dynamic-input value commits as a point pick
|
|
|
|
|
|
// before the plain-Enter (on_enter) path runs.
|
|
|
|
|
|
if let Some(task) = self.try_dyn_commit() {
|
|
|
|
|
|
return task;
|
|
|
|
|
|
}
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter());
|
|
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
return self.apply_cmd_result(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
} else if let Some(cmd) = self.tabs[i].last_cmd.clone() {
|
|
|
|
|
|
self.dispatch_command(&cmd)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_command_escape(&mut self) -> Task<Message> {
|
|
|
|
|
|
// Open MText editor swallows Escape (cancel without committing).
|
|
|
|
|
|
if self.mtext_editor.is_some() {
|
|
|
|
|
|
self.mtext_cancel();
|
|
|
|
|
|
return self.post_editor_closed(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
// The in-place TEXT editor likewise cancels on Escape.
|
|
|
|
|
|
if self.text_inline.is_some() {
|
|
|
|
|
|
self.text_inline_cancel();
|
|
|
|
|
|
return self.post_editor_closed(false);
|
|
|
|
|
|
}
|
2026-06-27 11:50:11 +03:00
|
|
|
|
// Esc cancels an armed pane move.
|
|
|
|
|
|
if self.pane_move_from.take().is_some() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-03 12:42:55 +03:00
|
|
|
|
// Esc cancels a pending system-variable value prompt.
|
|
|
|
|
|
if self.pending_setvar.take().is_some() {
|
|
|
|
|
|
self.command_line.push_info("*Cancel*");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-06-27 01:12:45 +03:00
|
|
|
|
// UCS icon: Esc ends any grip drag and clears the selection
|
|
|
|
|
|
// (only when no command owns Escape).
|
|
|
|
|
|
if self.tabs[self.active_tab].active_cmd.is_none()
|
|
|
|
|
|
&& (self.ucs_grip_drag.is_some() || self.ucs_icon_selected)
|
|
|
|
|
|
{
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.ucs_grip_drag = None;
|
|
|
|
|
|
self.ucs_icon_selected = false;
|
|
|
|
|
|
self.ucs_icon_hover = false;
|
|
|
|
|
|
self.tabs[i].snap_result = None;
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Leave interactive PAN mode (and end any in-flight pan drag).
|
|
|
|
|
|
if self.tabs[self.active_tab].pan_mode {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].pan_mode = false;
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.middle_down = false;
|
|
|
|
|
|
sel.middle_last_pos = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.push_output("PAN ended.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Grip popup intercepts Escape — dismisses the menu
|
|
|
|
|
|
// without doing anything else.
|
|
|
|
|
|
if self.grip_popup.take().is_some() {
|
|
|
|
|
|
self.grip_hover = None;
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.visibility_popup.take().is_some() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.grip_pending.take().is_some() {
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// A hot grip (click-move-click placement in progress) ends on
|
|
|
|
|
|
// Escape, leaving the entity at its last previewed position.
|
|
|
|
|
|
if self.tabs[self.active_tab].active_grip.take().is_some() {
|
|
|
|
|
|
// An Add-Leader arrow being placed: Esc removes it again.
|
|
|
|
|
|
if let Some((h, gid)) = self.grip_add_provisional.take() {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
use crate::entities::traits::EntityTypeOps;
|
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(h) {
|
|
|
|
|
|
e.apply_grip_menu(
|
|
|
|
|
|
gid,
|
|
|
|
|
|
crate::scene::model::object::GripMenuAction::RemoveLeader,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Cancel an in-progress grip drag: restore the edited
|
|
|
|
|
|
// entity from its pre-drag backup, un-hide it, re-tessellate
|
|
|
|
|
|
// once, and drop the preview.
|
|
|
|
|
|
if let Some(h) = self.grip_preview_handle.take() {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-10 10:39:44 +03:00
|
|
|
|
self.grip_text_verts = Vec::new();
|
|
|
|
|
|
self.grip_text_slide = false;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(orig) = self.grip_original.take() {
|
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(h) {
|
|
|
|
|
|
*e = orig;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.hidden.remove(&h);
|
|
|
|
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
|
|
|
|
// Geometry restored to the backup — re-tessellate just it.
|
|
|
|
|
|
self.tabs[i].scene.mark_entity_dirty(h);
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[self.active_tab].snap_result = None;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Cancel layout rename / context menus first, then fall through.
|
|
|
|
|
|
let i_e = self.active_tab;
|
|
|
|
|
|
if self.qselect.take().is_some() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut sel = self.tabs[i_e].scene.selection.borrow_mut();
|
|
|
|
|
|
if sel.context_menu.is_some() {
|
|
|
|
|
|
sel.context_menu = None;
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.layout_rename_state.take().is_some()
|
|
|
|
|
|
|| self.layout_context_menu.take().is_some()
|
|
|
|
|
|
{
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Typed text on the command line cancels first — one
|
|
|
|
|
|
// Esc empties the buffer, a second Esc then escalates
|
|
|
|
|
|
// to whatever the current mode would otherwise do
|
|
|
|
|
|
// (cancel command / exit viewport / deselect).
|
|
|
|
|
|
if !self.command_line.input.is_empty() {
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
self.command_line.autocomplete_cursor = None;
|
|
|
|
|
|
self.command_line.close_history();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_escape());
|
|
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
return self.apply_cmd_result(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if self.tabs[i].scene.active_viewport.is_some() {
|
|
|
|
|
|
// ESC while in MSPACE → exit back to paper space.
|
|
|
|
|
|
return Task::done(Message::ExitViewport);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.box_anchor = None;
|
2026-07-21 09:32:44 +03:00
|
|
|
|
// Also drop the world-space anchor, or the next pan/zoom
|
|
|
|
|
|
// re-projects it back into `box_anchor` and the cancelled
|
|
|
|
|
|
// marquee springs back to life (reproject_box_anchor).
|
|
|
|
|
|
sel.box_anchor_world = None;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
sel.box_current = None;
|
|
|
|
|
|
sel.box_crossing = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_layer_toggle_vp_freeze(&mut self, layer_idx: usize, vp_col_idx: usize) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let vp_handle = self.tabs[i]
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.vp_cols
|
|
|
|
|
|
.get(vp_col_idx)
|
|
|
|
|
|
.map(|c| c.handle);
|
|
|
|
|
|
let layer_name = self.tabs[i]
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.get(layer_idx)
|
|
|
|
|
|
.map(|l| l.name.clone());
|
|
|
|
|
|
|
|
|
|
|
|
if let (Some(vp_handle), Some(layer_name)) = (vp_handle, layer_name) {
|
|
|
|
|
|
// Get the layer handle from the document
|
|
|
|
|
|
if let Some(doc_layer) = self.tabs[i].scene.document.layers.get(&layer_name) {
|
|
|
|
|
|
let layer_handle = doc_layer.handle;
|
|
|
|
|
|
self.push_undo_snapshot(i, "VPLAYER");
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle frozen_layers on the viewport entity
|
|
|
|
|
|
for e in self.tabs[i].scene.document.entities_mut() {
|
|
|
|
|
|
if let acadrust::EntityType::Viewport(vp) = e {
|
|
|
|
|
|
if vp.common.handle == vp_handle {
|
|
|
|
|
|
if vp.frozen_layers.contains(&layer_handle) {
|
|
|
|
|
|
vp.frozen_layers.retain(|h| h != &layer_handle);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vp.frozen_layers.push(layer_handle);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Re-sync layer panel with updated VP info
|
|
|
|
|
|
let vp_info = self.tabs[i].scene.viewport_list();
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.sync_with_viewports(&doc_layers, vp_info);
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_layer_new(&mut self) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut n = 1;
|
|
|
|
|
|
let new_name = loop {
|
|
|
|
|
|
let candidate = format!("Layer{}", n);
|
|
|
|
|
|
if !self.tabs[i].scene.document.layers.contains(&candidate) {
|
|
|
|
|
|
break candidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
n += 1;
|
|
|
|
|
|
};
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER NEW");
|
|
|
|
|
|
use acadrust::tables::layer::Layer as DocLayer;
|
|
|
|
|
|
// A layer needs a real handle or it is dropped on a DWG save
|
|
|
|
|
|
// (the format is handle-based; issue #67).
|
|
|
|
|
|
let mut dl = DocLayer::new(&new_name);
|
|
|
|
|
|
// `allocate_handle` advances the seed so the layer gets a
|
|
|
|
|
|
// unique handle; the non-advancing `next_handle` getter hands
|
|
|
|
|
|
// out the same value twice and the later object overwrites it.
|
|
|
|
|
|
dl.handle = self.tabs[i].scene.document.allocate_handle();
|
|
|
|
|
|
let _ = self.tabs[i].scene.document.layers.add(dl);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
let vp_info = self.tabs[i].scene.viewport_list();
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.sync_with_viewports(&doc_layers, vp_info);
|
|
|
|
|
|
let new_idx = self.tabs[i]
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.position(|l| l.name == new_name);
|
|
|
|
|
|
if let Some(idx) = new_idx {
|
|
|
|
|
|
self.tabs[i].layers.selected = Some(idx);
|
2026-07-01 17:44:19 +03:00
|
|
|
|
self.tabs[i].layers.selected_multi = vec![idx];
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.tabs[i].layers.editing = Some(idx);
|
|
|
|
|
|
self.tabs[i].layers.edit_buf = new_name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.sync_ribbon_layers();
|
2026-07-04 22:28:07 +03:00
|
|
|
|
// With alphabetical ordering (#270) the new layer can land
|
|
|
|
|
|
// anywhere in a long list; scroll its row into view so the
|
|
|
|
|
|
// rename prompt is visible (#271). Offset is layout-independent
|
|
|
|
|
|
// (idx × fixed row height), so it holds even before the freshly
|
|
|
|
|
|
// added row is measured.
|
|
|
|
|
|
match new_idx {
|
|
|
|
|
|
Some(idx) => iced::widget::operation::scroll_to(
|
|
|
|
|
|
iced::advanced::widget::Id::new(
|
|
|
|
|
|
crate::ui::window::layers::LAYER_TABLE_SCROLL_ID,
|
|
|
|
|
|
),
|
|
|
|
|
|
iced::widget::scrollable::AbsoluteOffset {
|
|
|
|
|
|
x: 0.0,
|
|
|
|
|
|
y: idx as f32 * crate::ui::ROW_H,
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
None => Task::none(),
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_layer_delete(&mut self) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-01 17:44:19 +03:00
|
|
|
|
// Every selected layer (multi-select), or the anchor if none.
|
|
|
|
|
|
let mut names = self.selected_layer_names(i);
|
|
|
|
|
|
if names.is_empty() {
|
2026-07-01 17:23:32 +03:00
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
// Layer "0" and the current layer can't be deleted — drop them
|
|
|
|
|
|
// from the batch and note it.
|
|
|
|
|
|
let current = self.tabs[i].scene.document.header.current_layer_name.clone();
|
|
|
|
|
|
let before = names.len();
|
|
|
|
|
|
names.retain(|n| n != "0" && *n != current);
|
|
|
|
|
|
if names.len() < before {
|
2026-07-01 17:23:32 +03:00
|
|
|
|
self.command_line
|
2026-07-01 17:44:19 +03:00
|
|
|
|
.push_info("Layer \"0\" and the current layer can't be deleted — skipped.");
|
2026-07-01 17:23:32 +03:00
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
if names.is_empty() {
|
2026-07-01 17:23:32 +03:00
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
// Total objects across the layers. Empty → delete straight away;
|
2026-07-01 17:23:32 +03:00
|
|
|
|
// non-empty → warn first (deleting also removes those objects).
|
|
|
|
|
|
let count = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.entities()
|
2026-07-01 17:44:19 +03:00
|
|
|
|
.filter(|e| names.contains(&e.common().layer))
|
2026-07-01 17:23:32 +03:00
|
|
|
|
.count();
|
|
|
|
|
|
if count == 0 {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.push_undo_snapshot(i, "LAYER DELETE");
|
2026-07-01 17:44:19 +03:00
|
|
|
|
for name in &names {
|
|
|
|
|
|
self.tabs[i].scene.document.layers.remove(name);
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.tabs[i].dirty = true;
|
2026-07-01 17:23:32 +03:00
|
|
|
|
self.sync_layer_panel(i);
|
|
|
|
|
|
} else {
|
2026-07-01 17:44:19 +03:00
|
|
|
|
self.layer_delete_pending = Some((names, count));
|
2026-07-01 17:23:32 +03:00
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::LayerDeleteWarning);
|
|
|
|
|
|
self.modal_offset = iced::Vector::ZERO;
|
2026-07-19 15:47:09 +03:00
|
|
|
|
self.modal_resize = iced::Vector::ZERO;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:44:19 +03:00
|
|
|
|
/// User confirmed deleting non-empty layer(s): erase every object on them,
|
|
|
|
|
|
/// then remove the layer records.
|
2026-07-01 17:23:32 +03:00
|
|
|
|
pub(super) fn on_layer_delete_confirm(&mut self) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.active_modal = None;
|
|
|
|
|
|
self.modal_offset = iced::Vector::ZERO;
|
2026-07-19 15:47:09 +03:00
|
|
|
|
self.modal_resize = iced::Vector::ZERO;
|
2026-07-01 17:44:19 +03:00
|
|
|
|
let Some((names, _)) = self.layer_delete_pending.take() else {
|
2026-07-01 17:23:32 +03:00
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER DELETE");
|
|
|
|
|
|
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.entities()
|
2026-07-01 17:44:19 +03:00
|
|
|
|
.filter(|e| names.contains(&e.common().layer))
|
2026-07-01 17:23:32 +03:00
|
|
|
|
.map(|e| e.common().handle)
|
|
|
|
|
|
.collect();
|
2026-07-01 17:44:19 +03:00
|
|
|
|
// Remove the layer records first so the lock guard in `erase_entities`
|
2026-07-01 17:23:32 +03:00
|
|
|
|
// (which looks the layer up by name) can't block the objects.
|
2026-07-01 17:44:19 +03:00
|
|
|
|
for name in &names {
|
|
|
|
|
|
self.tabs[i].scene.document.layers.remove(name);
|
|
|
|
|
|
}
|
2026-07-01 17:23:32 +03:00
|
|
|
|
self.tabs[i].scene.erase_entities(&handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_layer_panel(i);
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 16:06:10 +03:00
|
|
|
|
/// Layers a Layer-manager row toggle (visibility / lock / freeze /
|
|
|
|
|
|
/// transparency) should affect: the whole multi-selection when the clicked
|
|
|
|
|
|
/// row is part of it, otherwise just the clicked row — so those toggles work
|
|
|
|
|
|
/// in bulk like the color / linetype / lineweight edits already do (#236).
|
|
|
|
|
|
pub(super) fn layer_row_action_targets(&self, i: usize, idx: usize) -> Vec<String> {
|
|
|
|
|
|
let Some(clicked) = self.tabs[i].layers.layers.get(idx).map(|l| l.name.clone()) else {
|
|
|
|
|
|
return Vec::new();
|
|
|
|
|
|
};
|
|
|
|
|
|
let selected = self.selected_layer_names(i);
|
|
|
|
|
|
if selected.iter().any(|n| n == &clicked) {
|
|
|
|
|
|
selected
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vec![clicked]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:44:19 +03:00
|
|
|
|
/// Layer names for the current Layer-manager selection — every row in the
|
|
|
|
|
|
/// multi-selection, or the anchor row when the multi-set is empty. Bulk
|
|
|
|
|
|
/// property edits and deletion act on these.
|
|
|
|
|
|
pub(super) fn selected_layer_names(&self, i: usize) -> Vec<String> {
|
|
|
|
|
|
let panel = &self.tabs[i].layers;
|
|
|
|
|
|
let idxs: Vec<usize> = if panel.selected_multi.is_empty() {
|
|
|
|
|
|
panel.selected.into_iter().collect()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
panel.selected_multi.clone()
|
|
|
|
|
|
};
|
|
|
|
|
|
idxs.iter()
|
|
|
|
|
|
.filter_map(|&x| panel.layers.get(x).map(|l| l.name.clone()))
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:23:32 +03:00
|
|
|
|
/// Rebuild the Layer-manager panel + ribbon after a layer-table change.
|
|
|
|
|
|
fn sync_layer_panel(&mut self, i: usize) {
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
let vp_info = self.tabs[i].scene.viewport_list();
|
|
|
|
|
|
self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info);
|
|
|
|
|
|
self.tabs[i].layers.selected = None;
|
2026-07-01 17:44:19 +03:00
|
|
|
|
self.tabs[i].layers.selected_multi.clear();
|
2026-07-01 17:23:32 +03:00
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
pub(super) fn on_layer_set_current(&mut self) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|
|
|
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|
|
|
|
|
let name = layer.name.clone();
|
|
|
|
|
|
// Mirror the change into the document header (CLAYER) too,
|
|
|
|
|
|
// not just the per-tab default. Otherwise the no-selection
|
|
|
|
|
|
// ribbon refresh (e.g. after Esc) re-reads the stale header
|
|
|
|
|
|
// layer and the dropdown snaps back to it. See #93.
|
|
|
|
|
|
let handle = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.get(&name)
|
|
|
|
|
|
.map(|l| l.handle)
|
|
|
|
|
|
.unwrap_or(acadrust::types::Handle::NULL);
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_layer_name = name.clone();
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_layer_handle = handle;
|
|
|
|
|
|
self.tabs[i].active_layer = name.clone();
|
|
|
|
|
|
self.tabs[i].layers.current_layer = name.clone();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.ribbon.active_layer = name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_layer_rename_commit(&mut self) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let editing_idx = self.tabs[i].layers.editing.take();
|
|
|
|
|
|
if let Some(idx) = editing_idx {
|
|
|
|
|
|
let new_name = self.tabs[i].layers.edit_buf.trim().to_string();
|
|
|
|
|
|
let old_name = self.tabs[i]
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.get(idx)
|
|
|
|
|
|
.map(|l| l.name.clone())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
if !new_name.is_empty()
|
|
|
|
|
|
&& new_name != old_name
|
|
|
|
|
|
&& !self.tabs[i].scene.document.layers.contains(&new_name)
|
|
|
|
|
|
{
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER RENAME");
|
|
|
|
|
|
// Keep the whole record (handle, color, linetype,
|
|
|
|
|
|
// lineweight, flags) and only change the name, so the
|
|
|
|
|
|
// renamed layer still has a valid handle and survives a
|
|
|
|
|
|
// DWG save (issue #67).
|
|
|
|
|
|
if let Some(mut nl) =
|
|
|
|
|
|
self.tabs[i].scene.document.layers.get(&old_name).cloned()
|
|
|
|
|
|
{
|
|
|
|
|
|
nl.name = new_name.clone();
|
|
|
|
|
|
if !nl.handle.is_valid() {
|
|
|
|
|
|
nl.handle = self.tabs[i].scene.document.allocate_handle();
|
|
|
|
|
|
}
|
|
|
|
|
|
let _ = self.tabs[i].scene.document.layers.add(nl);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.document.layers.remove(&old_name);
|
|
|
|
|
|
for e in self.tabs[i].scene.document.entities_mut() {
|
|
|
|
|
|
if e.as_entity().layer() == old_name {
|
|
|
|
|
|
e.as_entity_mut().set_layer(new_name.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
let vp_info = self.tabs[i].scene.viewport_list();
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.sync_with_viewports(&doc_layers, vp_info);
|
|
|
|
|
|
self.tabs[i].layers.edit_buf.clear();
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_grip_menu_pick(&mut self, idx: usize) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let Some(popup) = self.grip_popup.take() else {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
self.grip_hover = None;
|
|
|
|
|
|
let Some(item) = popup.items.get(idx).cloned() else {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
use crate::entities::traits::EntityTypeOps;
|
|
|
|
|
|
use crate::scene::model::object::GripMenuAction;
|
|
|
|
|
|
if matches!(
|
|
|
|
|
|
item.action,
|
|
|
|
|
|
GripMenuAction::Stretch
|
|
|
|
|
|
| GripMenuAction::MoveWithLeader
|
|
|
|
|
|
| GripMenuAction::MoveIndependent
|
|
|
|
|
|
) {
|
|
|
|
|
|
// Stretch / Move = grab this grip. Engage it so the next
|
|
|
|
|
|
// click places it (click-move-click) — same as picking the
|
|
|
|
|
|
// grip directly in the viewport. Without this the menu just
|
|
|
|
|
|
// closed and the grip never became hot (issue #48).
|
|
|
|
|
|
if let Some(g) = self.tabs[i]
|
|
|
|
|
|
.selected_grips
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|g| g.id == popup.grip_id)
|
|
|
|
|
|
{
|
|
|
|
|
|
// "Move with Leader" drags the whole multileader; the
|
|
|
|
|
|
// others move just the picked grip.
|
|
|
|
|
|
let (grip_id, is_translate) =
|
|
|
|
|
|
if matches!(item.action, GripMenuAction::MoveWithLeader) {
|
|
|
|
|
|
(crate::entities::multileader::MOVE_ALL_GRIP, true)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(popup.grip_id, g.is_midpoint)
|
|
|
|
|
|
};
|
|
|
|
|
|
self.tabs[i].active_grip = Some(GripEdit {
|
|
|
|
|
|
handle: popup.handle,
|
|
|
|
|
|
grip_id,
|
|
|
|
|
|
is_translate,
|
|
|
|
|
|
origin_world: g.world,
|
|
|
|
|
|
last_world: g.world,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Actions that need a follow-up number stash a pending
|
|
|
|
|
|
// state + prompt; the next typed value drives
|
|
|
|
|
|
// `apply_grip_menu_value`.
|
|
|
|
|
|
let prompt = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.get_entity(popup.handle)
|
|
|
|
|
|
.and_then(|e| e.grip_menu_value_prompt(popup.grip_id, item.action));
|
|
|
|
|
|
if let Some(label) = prompt {
|
|
|
|
|
|
self.grip_pending = Some(crate::app::GripPendingValue {
|
|
|
|
|
|
handle: popup.handle,
|
|
|
|
|
|
grip_id: popup.grip_id,
|
|
|
|
|
|
action: item.action,
|
|
|
|
|
|
label,
|
|
|
|
|
|
});
|
|
|
|
|
|
self.command_line.push_info(&format!("{label}:"));
|
|
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
|
|
|
|
|
// One-shot action — apply immediately.
|
|
|
|
|
|
self.push_undo_snapshot(i, item.label);
|
|
|
|
|
|
// For Add Leader, the new arrow becomes the last grip; remember
|
|
|
|
|
|
// its id so we can grab it for placement right after.
|
|
|
|
|
|
let add_leader_gid = if matches!(item.action, GripMenuAction::AddLeader) {
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.get_entity(popup.handle)
|
|
|
|
|
|
.and_then(|e| match e {
|
|
|
|
|
|
acadrust::EntityType::MultiLeader(ml) => Some(
|
|
|
|
|
|
ml.context
|
|
|
|
|
|
.leader_roots
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.flat_map(|r| r.lines.iter())
|
|
|
|
|
|
.map(|l| l.points.len())
|
|
|
|
|
|
.sum::<usize>(),
|
|
|
|
|
|
),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(popup.handle) {
|
|
|
|
|
|
entity.apply_grip_menu(popup.grip_id, item.action);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
// Grab the new arrow so it follows the cursor (click places it,
|
|
|
|
|
|
// Esc removes it).
|
|
|
|
|
|
if let Some(new_gid) = add_leader_gid {
|
|
|
|
|
|
if let Some(g) = self.tabs[i].selected_grips.iter().find(|g| g.id == new_gid) {
|
|
|
|
|
|
self.tabs[i].active_grip = Some(GripEdit {
|
|
|
|
|
|
handle: popup.handle,
|
|
|
|
|
|
grip_id: new_gid,
|
|
|
|
|
|
is_translate: false,
|
|
|
|
|
|
origin_world: g.world,
|
|
|
|
|
|
last_world: g.world,
|
|
|
|
|
|
});
|
|
|
|
|
|
self.grip_add_provisional = Some((popup.handle, new_gid));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_paste_shortcut(&mut self) -> Task<Message> {
|
|
|
|
|
|
if self.mtext_editor.is_some() {
|
|
|
|
|
|
// Web reads via the browser's async Clipboard API (iced's
|
|
|
|
|
|
// sync clipboard read returns nothing there); native uses
|
|
|
|
|
|
// iced's clipboard.
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
return Task::perform(
|
|
|
|
|
|
crate::sys::read_clipboard_text(),
|
|
|
|
|
|
Message::MTextPasteClip,
|
|
|
|
|
|
);
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
return iced::clipboard::read().map(Message::MTextPasteClip);
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.text_inline.is_some() {
|
|
|
|
|
|
// Web: the iced text_input can't reach the async clipboard,
|
|
|
|
|
|
// so paste it ourselves. Native: the focused text_input
|
|
|
|
|
|
// already handled Ctrl+V — doing it here would duplicate.
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
return Task::perform(
|
|
|
|
|
|
crate::sys::read_clipboard_text(),
|
|
|
|
|
|
Message::TextInlinePasteClip,
|
|
|
|
|
|
);
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::done(Message::Command("PASTECLIP".to_string()))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_qselect_open(&mut self) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
|
|
|
|
|
// Seed the type filter from the first selected entity so a
|
|
|
|
|
|
// right-click → Quick Select on a known object opens the
|
|
|
|
|
|
// panel pre-tuned to that entity's type. Property defaults
|
|
|
|
|
|
// to "(Any property)" so the user immediately picks what
|
|
|
|
|
|
// they want to compare.
|
|
|
|
|
|
let mut type_filter: Option<String> = None;
|
|
|
|
|
|
if let Some(&h) = self.tabs[i].scene.selected.iter().next() {
|
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity(h) {
|
|
|
|
|
|
use crate::entities::traits::entity_type_name;
|
|
|
|
|
|
type_filter = Some(entity_type_name(e).to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.qselect = Some(crate::app::QSelectState {
|
|
|
|
|
|
type_filter,
|
|
|
|
|
|
property: None,
|
|
|
|
|
|
operator: crate::app::QSelectOp::Eq,
|
|
|
|
|
|
value: String::new(),
|
|
|
|
|
|
append: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_ribbon_layer_changed(&mut self, layer: String) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
|
// No selection — change the creation default. Persist
|
|
|
|
|
|
// into the tab's header (CLAYER) so it survives a tab
|
|
|
|
|
|
// switch and rides the next save. #21.
|
|
|
|
|
|
let handle = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.get(&layer)
|
|
|
|
|
|
.map(|l| l.handle)
|
|
|
|
|
|
.unwrap_or(acadrust::types::Handle::NULL);
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_layer_name = layer.clone();
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_layer_handle = handle;
|
|
|
|
|
|
self.tabs[i].active_layer = layer.clone();
|
|
|
|
|
|
self.tabs[i].layers.current_layer = layer.clone();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.ribbon.active_layer = layer;
|
|
|
|
|
|
} else {
|
2026-07-01 19:14:35 +03:00
|
|
|
|
// Apply to the selection; leave the creation default alone
|
|
|
|
|
|
// ("Make current" is a separate action).
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
2026-07-01 19:14:35 +03:00
|
|
|
|
for &handle in &handles {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
crate::scene::view::dispatch::apply_common_prop(entity, "layer", &layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-01 19:14:35 +03:00
|
|
|
|
// Layer drives by-layer colour/linetype/lineweight, which are
|
|
|
|
|
|
// baked into the cached wire geometry — re-tessellate so the
|
|
|
|
|
|
// change shows immediately (issue #231 class).
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.ribbon.active_layer = layer;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_ribbon_color_changed(&mut self, color: AcadColor) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.ribbon.prop_color_palette_open = false;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
|
// Persist the new default into the tab's header so it
|
|
|
|
|
|
// round-trips through tab switches and writes back on
|
|
|
|
|
|
// save (CECOLOR). #21.
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_entity_color = color;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.ribbon.active_color = color;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
crate::scene::view::dispatch::apply_color(entity, color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.ribbon.active_color = color;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_ribbon_linetype_changed(&mut self, lt: String) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
|
// Persist into the tab's header (CELTYPE). Resolve to a
|
|
|
|
|
|
// handle when the name matches a line_types entry so the
|
|
|
|
|
|
// handle-based lookup stays in sync. #21.
|
|
|
|
|
|
let handle = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.line_types
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|x| x.name.eq_ignore_ascii_case(<))
|
|
|
|
|
|
.map(|x| x.handle)
|
|
|
|
|
|
.unwrap_or(acadrust::types::Handle::NULL);
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_linetype_name = lt.clone();
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_linetype_handle = handle;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.ribbon.active_linetype = lt;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
2026-07-01 19:14:35 +03:00
|
|
|
|
for &handle in &handles {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
crate::scene::view::dispatch::apply_common_prop(entity, "linetype", <);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-01 19:14:35 +03:00
|
|
|
|
// Linetype is baked into the cached wire geometry —
|
|
|
|
|
|
// re-tessellate so the dashed/solid look updates immediately
|
|
|
|
|
|
// (issue #231 class).
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.ribbon.active_linetype = lt;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_ribbon_style_changed(&mut self, key: crate::modules::StyleKey, name: String) -> Task<Message> {
|
|
|
|
|
|
use crate::modules::StyleKey;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
match key {
|
|
|
|
|
|
StyleKey::TextStyle => {
|
|
|
|
|
|
self.ribbon.active_text_style = name.clone();
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let found = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|s| s.name == name)
|
|
|
|
|
|
.map(|ts| ts.handle);
|
|
|
|
|
|
if let Some(h) = found {
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_text_style_handle = h;
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_text_style_name = name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
StyleKey::DimStyle => {
|
|
|
|
|
|
self.ribbon.active_dim_style = name.clone();
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let found = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.dim_styles
|
|
|
|
|
|
.get(&name)
|
|
|
|
|
|
.map(|ds| ds.handle);
|
|
|
|
|
|
if let Some(h) = found {
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_dimstyle_handle = h;
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_dimstyle_name = name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
StyleKey::MLeaderStyle => {
|
|
|
|
|
|
self.ribbon.active_mleader_style = name.clone();
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].active_mleader_style = name;
|
|
|
|
|
|
}
|
|
|
|
|
|
StyleKey::TableStyle => {
|
|
|
|
|
|
self.ribbon.active_table_style = name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_prop_hatch_pattern_changed(&mut self, name: String) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
use crate::scene::model::hatch_patterns;
|
|
|
|
|
|
if let Some(entry) = hatch_patterns::find(&name) {
|
|
|
|
|
|
self.push_undo_snapshot(i, "HATCHEDIT");
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Hatch(dxf)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
dxf.pattern = hatch_patterns::build_dxf_pattern(entry);
|
|
|
|
|
|
dxf.is_solid = matches!(
|
|
|
|
|
|
entry.gpu,
|
|
|
|
|
|
crate::scene::model::hatch_model::HatchPattern::Solid
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) {
|
|
|
|
|
|
model.pattern = entry.gpu.clone();
|
|
|
|
|
|
model.name = name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_prop_geom_choice_changed(&mut self, field: &'static str, value: String) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
if field == "vp_ucs_name" {
|
|
|
|
|
|
// Resolve UCS name → cloned data, then mutate viewports.
|
|
|
|
|
|
let ucs_data = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.ucss
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|u| u.name == value)
|
|
|
|
|
|
.cloned();
|
|
|
|
|
|
if let Some(ucs) = ucs_data {
|
|
|
|
|
|
for handle in &handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Viewport(vp)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(*handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
vp.ucs_handle = ucs.handle;
|
|
|
|
|
|
vp.ucs_origin = ucs.origin.clone();
|
|
|
|
|
|
vp.ucs_x_axis = ucs.x_axis.clone();
|
|
|
|
|
|
vp.ucs_y_axis = ucs.y_axis.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if field == "vp_named_view" {
|
|
|
|
|
|
// Assign a named view to viewport(s): copy camera parameters.
|
|
|
|
|
|
let view_data = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.views
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|v| v.name == value)
|
|
|
|
|
|
.cloned();
|
|
|
|
|
|
if let Some(view) = view_data {
|
|
|
|
|
|
for handle in &handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Viewport(vp)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(*handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
vp.view_target = view.target.clone();
|
|
|
|
|
|
vp.view_direction = view.direction.clone();
|
|
|
|
|
|
if view.height > 0.0 {
|
|
|
|
|
|
vp.view_height = view.height;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
}
|
2026-07-12 23:16:26 +03:00
|
|
|
|
} else if matches!(
|
|
|
|
|
|
field,
|
|
|
|
|
|
"mleader_style"
|
|
|
|
|
|
| "text_style_handle"
|
|
|
|
|
|
| "arrowhead_handle"
|
|
|
|
|
|
| "line_type_handle"
|
|
|
|
|
|
) {
|
|
|
|
|
|
// Resolve a picked name back to the handle the MLEADER
|
|
|
|
|
|
// stores. The style/text-style rows keep their existing
|
|
|
|
|
|
// handle on a failed lookup; the arrowhead/linetype rows
|
|
|
|
|
|
// take the resolved value directly (None = the default
|
|
|
|
|
|
// "Closed filled" / "ByBlock" option).
|
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
|
let resolved: Option<acadrust::Handle> = match field {
|
|
|
|
|
|
"mleader_style" => doc.objects.iter().find_map(|(h, o)| match o {
|
|
|
|
|
|
acadrust::objects::ObjectType::MultiLeaderStyle(s)
|
|
|
|
|
|
if s.name == value =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(*h)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}),
|
|
|
|
|
|
"text_style_handle" => doc
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|s| s.name == value)
|
|
|
|
|
|
.map(|s| s.handle),
|
|
|
|
|
|
"arrowhead_handle" => {
|
|
|
|
|
|
if value == "Closed filled" {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
doc.block_records
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|b| b.name == value)
|
|
|
|
|
|
.map(|b| b.handle)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"line_type_handle" => {
|
|
|
|
|
|
if value == "ByBlock" {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
doc.line_types
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|l| l.name == value)
|
|
|
|
|
|
.map(|l| l.handle)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(acadrust::EntityType::MultiLeader(ml)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"mleader_style" => {
|
|
|
|
|
|
if let Some(h) = resolved {
|
|
|
|
|
|
ml.style_handle = Some(h);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_style_handle" => {
|
|
|
|
|
|
if let Some(h) = resolved {
|
|
|
|
|
|
ml.text_style_handle = Some(h);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"arrowhead_handle" => ml.arrowhead_handle = resolved,
|
|
|
|
|
|
"line_type_handle" => ml.line_type_handle = resolved,
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
|
} else if matches!(field, "arrow_block" | "dim_line_lw" | "text_pos_vert") {
|
|
|
|
|
|
// Leader dim-var overrides picked from a dropdown. The
|
|
|
|
|
|
// arrow block resolves a friendly arrowhead name to a
|
|
|
|
|
|
// block handle ("Closed filled" clears the override →
|
|
|
|
|
|
// reverts to the style); the other two map a label to
|
|
|
|
|
|
// the DIMLWD / DIMTAD enum value.
|
|
|
|
|
|
use crate::entities::dim_override as dov;
|
|
|
|
|
|
use acadrust::xdata::XDataValue;
|
|
|
|
|
|
let arrow_h: Option<acadrust::Handle> =
|
|
|
|
|
|
if field == "arrow_block" && value != "Closed filled" {
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.block_records
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|b| {
|
|
|
|
|
|
crate::app::properties::arrowhead_label(&b.name) == value
|
|
|
|
|
|
})
|
|
|
|
|
|
.map(|b| b.handle)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Only leaders carry these rows; skip anything else so
|
|
|
|
|
|
// a multi-type selection can't get stray overrides.
|
|
|
|
|
|
if !matches!(
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(handle),
|
|
|
|
|
|
Some(acadrust::EntityType::Leader(_))
|
|
|
|
|
|
) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"dim_line_lw" => dov::set(
|
|
|
|
|
|
doc,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
dov::DIMLWD,
|
|
|
|
|
|
Some(XDataValue::Integer16(
|
|
|
|
|
|
crate::app::properties::dim_lineweight_from_label(&value),
|
|
|
|
|
|
)),
|
|
|
|
|
|
),
|
|
|
|
|
|
"text_pos_vert" => dov::set(
|
|
|
|
|
|
doc,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
dov::DIMTAD,
|
|
|
|
|
|
Some(XDataValue::Integer16(
|
|
|
|
|
|
crate::app::properties::dimtad_from_label(&value),
|
|
|
|
|
|
)),
|
|
|
|
|
|
),
|
|
|
|
|
|
"arrow_block" => {
|
|
|
|
|
|
// "Closed filled" is an explicit override to
|
|
|
|
|
|
// the null-handle default arrow (not a clear),
|
|
|
|
|
|
// so the pick sticks even when the style's
|
|
|
|
|
|
// arrow differs.
|
|
|
|
|
|
if value == "Closed filled" {
|
|
|
|
|
|
dov::set(
|
|
|
|
|
|
doc,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
dov::DIMLDRBLK,
|
|
|
|
|
|
Some(XDataValue::Handle(acadrust::Handle::NULL)),
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if let Some(h) = arrow_h {
|
|
|
|
|
|
dov::set(
|
|
|
|
|
|
doc,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
dov::DIMLDRBLK,
|
|
|
|
|
|
Some(XDataValue::Handle(h)),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-12 23:16:26 +03:00
|
|
|
|
} else if field == "plot_style" {
|
|
|
|
|
|
// Named plot-style pick: ByLayer / ByBlock clear the
|
|
|
|
|
|
// handle; a named style resolves through the drawing's
|
|
|
|
|
|
// ACAD_PLOTSTYLENAME dictionary to its placeholder handle.
|
|
|
|
|
|
let dict_h =
|
|
|
|
|
|
self.tabs[i].scene.document.header.acad_plotstylename_dict_handle;
|
|
|
|
|
|
let ph: Option<acadrust::Handle> =
|
|
|
|
|
|
crate::scene::annotative::as_dict(&self.tabs[i].scene.document, dict_h)
|
|
|
|
|
|
.and_then(|d| {
|
|
|
|
|
|
d.entries
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|(n, _)| *n == value)
|
|
|
|
|
|
.map(|(_, h)| *h)
|
|
|
|
|
|
});
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
let common = entity.common_mut();
|
|
|
|
|
|
match value.as_str() {
|
|
|
|
|
|
"ByLayer" => {
|
|
|
|
|
|
common.plotstyle_flags = 0;
|
|
|
|
|
|
common.plotstyle_handle = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
"ByBlock" => {
|
|
|
|
|
|
common.plotstyle_flags = 1;
|
|
|
|
|
|
common.plotstyle_handle = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
if let Some(h) = ph {
|
|
|
|
|
|
common.plotstyle_flags = 3;
|
|
|
|
|
|
common.plotstyle_handle = Some(h);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if field == "material" {
|
|
|
|
|
|
// Material source: ByLayer / ByBlock clear the handle; a
|
|
|
|
|
|
// named material sets flag 3 + its handle (resolved here
|
|
|
|
|
|
// because the update loop holds the document).
|
|
|
|
|
|
let mat_handle: Option<acadrust::Handle> = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find_map(|(h, o)| match o {
|
|
|
|
|
|
acadrust::objects::ObjectType::Material(m) if m.name == value => {
|
|
|
|
|
|
Some(*h)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
});
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
let common = entity.common_mut();
|
|
|
|
|
|
match value.as_str() {
|
|
|
|
|
|
"ByLayer" => {
|
|
|
|
|
|
common.material_flags = 0;
|
|
|
|
|
|
common.material_handle = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
"ByBlock" => {
|
|
|
|
|
|
common.material_flags = 1;
|
|
|
|
|
|
common.material_handle = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
if let Some(h) = mat_handle {
|
|
|
|
|
|
common.material_flags = 3;
|
|
|
|
|
|
common.material_handle = Some(h);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
crate::scene::view::dispatch::apply_geom_prop(entity, field, &value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_prop_geom_commit(&mut self, field: &'static str) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
if let Some(raw_val) = self.tabs[i].properties.edit_buf.remove(field) {
|
|
|
|
|
|
let val = crate::app::expr_eval::eval_to_string(&raw_val);
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
if field == "frozen_layers" {
|
|
|
|
|
|
// Resolve layer names → handles, then apply to viewports.
|
|
|
|
|
|
let layer_handles: Vec<acadrust::Handle> = val
|
|
|
|
|
|
.split(',')
|
|
|
|
|
|
.map(|s| s.trim())
|
|
|
|
|
|
.filter(|s| !s.is_empty())
|
|
|
|
|
|
.filter_map(|name| {
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|l| l.name.eq_ignore_ascii_case(name))
|
|
|
|
|
|
.map(|l| l.handle)
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Viewport(vp)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
vp.frozen_layers = layer_handles.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-07-08 17:53:05 +03:00
|
|
|
|
// Per-vertex geometry edits (vertex_x/y, widths) target
|
|
|
|
|
|
// the vertex the Current Vertex stepper is on. (polyline)
|
|
|
|
|
|
crate::scene::view::dispatch::set_prop_current_vertex(
|
|
|
|
|
|
self.tabs[i].properties.prop_vertex,
|
|
|
|
|
|
);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
for &handle in &handles {
|
2026-07-01 13:10:39 +03:00
|
|
|
|
// Skip objects on a locked layer.
|
|
|
|
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
|
match field {
|
|
|
|
|
|
"hyperlink" => {
|
|
|
|
|
|
// Stored in the standard PE_URL XDATA
|
|
|
|
|
|
// record; an empty value clears it.
|
|
|
|
|
|
let vals = if val.trim().is_empty() {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(vec![acadrust::xdata::XDataValue::String(
|
|
|
|
|
|
val.clone(),
|
|
|
|
|
|
)])
|
|
|
|
|
|
};
|
|
|
|
|
|
crate::scene::view::dispatch::set_entity_xdata(
|
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
"PE_URL",
|
|
|
|
|
|
vals,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
"arrow_size" | "text_offset" | "dim_scale_overall" => {
|
|
|
|
|
|
// Real-valued leader dim-var overrides
|
|
|
|
|
|
// (ACAD_DSTYLE). A blank value clears the
|
|
|
|
|
|
// override → reverts to the style; an
|
|
|
|
|
|
// unparseable entry is ignored so a typo
|
|
|
|
|
|
// can't silently wipe a stored override.
|
|
|
|
|
|
use crate::entities::dim_override as dov;
|
|
|
|
|
|
let code = match field {
|
|
|
|
|
|
"arrow_size" => dov::DIMASZ,
|
|
|
|
|
|
"text_offset" => dov::DIMGAP,
|
|
|
|
|
|
_ => dov::DIMSCALE,
|
|
|
|
|
|
};
|
|
|
|
|
|
let trimmed = val.trim();
|
|
|
|
|
|
if trimmed.is_empty() {
|
|
|
|
|
|
dov::set(
|
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
code,
|
|
|
|
|
|
None,
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if let Ok(n) = trimmed.parse::<f64>() {
|
|
|
|
|
|
dov::set(
|
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
code,
|
|
|
|
|
|
Some(acadrust::xdata::XDataValue::Real(n)),
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
|
}
|
2026-07-20 13:40:12 +03:00
|
|
|
|
"linetype_scale" | "transparency" | "thickness" => {
|
|
|
|
|
|
// Thickness (DXF 39 extrusion) is a General-
|
|
|
|
|
|
// group common prop, not a geometry-group one:
|
|
|
|
|
|
// route it to apply_common_prop (which handles
|
|
|
|
|
|
// it via set_entity_thickness). apply_geom_prop
|
|
|
|
|
|
// ignores it, so the edit would otherwise be a
|
|
|
|
|
|
// silent no-op and revert to the old value.
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
|
if let Some(entity) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
crate::scene::view::dispatch::apply_common_prop(
|
|
|
|
|
|
entity, field, &val,
|
2026-07-12 23:24:34 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
if let Some(entity) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
crate::scene::view::dispatch::apply_geom_prop(
|
|
|
|
|
|
entity, field, &val,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-01 07:42:50 +03:00
|
|
|
|
|
|
|
|
|
|
/// Commit an edited block-attribute value from the Properties panel.
|
|
|
|
|
|
/// Writes the new value into the matching attribute (by tag) of every
|
|
|
|
|
|
/// selected INSERT, then re-tessellates so the attribute text repaints.
|
|
|
|
|
|
/// The value is stored verbatim (attribute values are free-form text, so
|
|
|
|
|
|
/// it is not run through the expression evaluator like numeric fields).
|
|
|
|
|
|
pub(super) fn on_prop_attr_commit(&mut self, tag: String) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let key = crate::ui::properties::attr_edit_key(&tag);
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let Some(val) = self.tabs[i].properties.edit_buf.remove(&key) else {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
self.push_undo_snapshot(i, "ATTEDIT");
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Insert(ins)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(attr) = ins.attributes.iter_mut().find(|a| a.tag == tag) {
|
|
|
|
|
|
attr.set_value(val.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-01 08:05:25 +03:00
|
|
|
|
|
|
|
|
|
|
/// Open the attribute editor dialog for the given INSERT, loading a working
|
|
|
|
|
|
/// copy of its attribute values. No-op (with a hint) when the block has no
|
|
|
|
|
|
/// attributes. Entry points: double-clicking such a block, or ATTEDIT.
|
|
|
|
|
|
pub(crate) fn open_attribute_editor(&mut self, handle: acadrust::Handle) {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-01 08:23:54 +03:00
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
// Ok((block, rows)) to open; Err(msg) to report and stay closed. The
|
2026-07-01 08:23:54 +03:00
|
|
|
|
// borrow of `doc` ends with this match, before any `self` mutation.
|
|
|
|
|
|
let result = match doc.get_entity(handle) {
|
2026-07-01 08:05:25 +03:00
|
|
|
|
Some(acadrust::EntityType::Insert(ins)) if !ins.attributes.is_empty() => {
|
2026-07-01 08:23:54 +03:00
|
|
|
|
// The prompt text lives on the block's ATTDEFs, not on the
|
|
|
|
|
|
// attribute instances — map tag → prompt from the definition.
|
|
|
|
|
|
let prompts = block_attr_prompts(doc, &ins.block_name);
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
let rows = ins
|
2026-07-01 08:05:25 +03:00
|
|
|
|
.attributes
|
|
|
|
|
|
.iter()
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
.map(|a| attr_row_from_entity(a, &prompts))
|
2026-07-01 08:05:25 +03:00
|
|
|
|
.collect::<Vec<_>>();
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
Ok((ins.block_name.clone(), rows))
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
Some(acadrust::EntityType::Insert(_)) => {
|
2026-07-01 08:23:54 +03:00
|
|
|
|
Err("ATTEDIT This block has no attributes.")
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
2026-07-01 08:23:54 +03:00
|
|
|
|
_ => Err("ATTEDIT Select a block with attributes."),
|
2026-07-01 08:05:25 +03:00
|
|
|
|
};
|
2026-07-01 08:23:54 +03:00
|
|
|
|
match result {
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
Ok((block, rows)) => {
|
2026-07-01 08:23:54 +03:00
|
|
|
|
self.attr_editor_block = block;
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
self.attr_editor_rows = rows;
|
|
|
|
|
|
self.attr_editor_selected = 0;
|
|
|
|
|
|
self.attr_editor_tab = AttrTab::Attribute;
|
2026-07-01 08:23:54 +03:00
|
|
|
|
self.attr_editor_handle = Some(handle);
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::AttributeEditor);
|
|
|
|
|
|
self.modal_offset = iced::Vector::ZERO;
|
2026-07-19 15:47:09 +03:00
|
|
|
|
self.modal_resize = iced::Vector::ZERO;
|
2026-07-01 08:23:54 +03:00
|
|
|
|
}
|
|
|
|
|
|
Err(msg) => self.command_line.push_error(msg),
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
/// The currently-selected editor row, for the Text Options / Properties
|
|
|
|
|
|
/// edit handlers (which all act on that one attribute).
|
|
|
|
|
|
pub(super) fn attr_row_selected_mut(&mut self) -> Option<&mut AttrRow> {
|
|
|
|
|
|
let sel = self.attr_editor_selected;
|
|
|
|
|
|
self.attr_editor_rows.get_mut(sel)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:05:25 +03:00
|
|
|
|
/// Close the attribute editor without applying, if it is open. Used when
|
|
|
|
|
|
/// the tab it belongs to closes or the active tab switches — its handle is
|
|
|
|
|
|
/// document-local and must not outlive its tab.
|
|
|
|
|
|
pub(super) fn cancel_attr_editor(&mut self) {
|
|
|
|
|
|
if self.active_modal == Some(crate::app::ModalKind::AttributeEditor) {
|
|
|
|
|
|
self.active_modal = None;
|
|
|
|
|
|
self.modal_offset = iced::Vector::ZERO;
|
2026-07-19 15:47:09 +03:00
|
|
|
|
self.modal_resize = iced::Vector::ZERO;
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
self.attr_editor_handle = None;
|
|
|
|
|
|
self.attr_editor_block.clear();
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
self.attr_editor_rows.clear();
|
|
|
|
|
|
self.attr_editor_selected = 0;
|
|
|
|
|
|
self.attr_editor_tab = AttrTab::Attribute;
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
/// Commit every edited attribute (value, text options, common properties)
|
|
|
|
|
|
/// back to the block, keeping the dialog open (Apply). Closing is the frame
|
|
|
|
|
|
/// ✕ (`CloseModal`), which discards any un-applied edits — matching the
|
|
|
|
|
|
/// other modal windows. Applied positionally so blocks with duplicate tags
|
|
|
|
|
|
/// stay correct; guarded on block name + count so an edit can never land on
|
|
|
|
|
|
/// a different entity if the selection changed under the open dialog.
|
|
|
|
|
|
pub(super) fn on_attr_editor_apply(&mut self) -> Task<Message> {
|
2026-07-01 08:05:25 +03:00
|
|
|
|
let i = self.active_tab;
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
let Some(handle) = self.attr_editor_handle else {
|
2026-07-01 08:05:25 +03:00
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
2026-07-01 13:14:33 +03:00
|
|
|
|
// The block's layer may have been locked while the editor was open —
|
|
|
|
|
|
// refuse to write attributes to a locked-layer block.
|
|
|
|
|
|
if let Some(layer) = self.tabs[i].scene.locked_layer_name(handle) {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"Object is on locked layer \"{layer}\" — unlock the layer to edit its attributes."
|
|
|
|
|
|
));
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
// Snapshot the working copy so the document can be mutated while the
|
|
|
|
|
|
// dialog's rows stay put (it remains open for further edits).
|
|
|
|
|
|
let rows = self.attr_editor_rows.clone();
|
|
|
|
|
|
let block = self.attr_editor_block.clone();
|
2026-07-01 08:05:25 +03:00
|
|
|
|
|
|
|
|
|
|
self.push_undo_snapshot(i, "ATTEDIT");
|
|
|
|
|
|
let mut changed = false;
|
|
|
|
|
|
if let Some(acadrust::EntityType::Insert(ins)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
if ins.block_name == block && rows.len() == ins.attributes.len() {
|
|
|
|
|
|
for (attr, row) in ins.attributes.iter_mut().zip(rows.iter()) {
|
|
|
|
|
|
changed |= apply_attr_row(attr, row);
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if changed {
|
|
|
|
|
|
self.invalidate_property_targets(i, &[handle]);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Nothing changed — drop the snapshot pushed a moment ago.
|
|
|
|
|
|
let _ = self.tabs[i].history.undo_stack.pop();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
2026-07-01 08:23:54 +03:00
|
|
|
|
|
feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor:
- Attribute: the tag/prompt/value list; click a row to select it, edit
its value below.
- Text Options: the selected attribute's Text Style, Justification,
Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down.
- Properties: the selected attribute's Layer, Linetype, Color,
Lineweight (the colour list also surfaces a non-standard current
colour so it displays and round-trips).
Applying writes value, text-formatting and common-property edits back to
each attribute positionally (guarded on block name + count), with undo
and a repaint. OK/Cancel are replaced by a single Apply button in a top
toolbar that commits but keeps the dialog open, matching the other modal
windows — the frame ✕ closes and discards un-applied edits. The frame
adopts the shared style-window palette, tab styling and sizing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:06:03 +03:00
|
|
|
|
/// Build the editor's working copy of one attribute. Angles are shown in
|
|
|
|
|
|
/// degrees; numeric fields become strings so the user can type freely.
|
|
|
|
|
|
fn attr_row_from_entity(
|
|
|
|
|
|
a: &acadrust::entities::AttributeEntity,
|
|
|
|
|
|
prompts: &rustc_hash::FxHashMap<String, String>,
|
|
|
|
|
|
) -> AttrRow {
|
|
|
|
|
|
let fmt = |v: f64| format!("{v}");
|
|
|
|
|
|
AttrRow {
|
|
|
|
|
|
tag: a.tag.clone(),
|
|
|
|
|
|
prompt: prompts.get(&a.tag).cloned().unwrap_or_default(),
|
|
|
|
|
|
value: a.get_value().to_string(),
|
|
|
|
|
|
text_style: a.text_style.clone(),
|
|
|
|
|
|
height: fmt(a.height),
|
|
|
|
|
|
rotation: fmt(a.rotation.to_degrees()),
|
|
|
|
|
|
width_factor: fmt(a.width_factor),
|
|
|
|
|
|
oblique: fmt(a.oblique_angle.to_degrees()),
|
|
|
|
|
|
h_align: a.horizontal_alignment,
|
|
|
|
|
|
v_align: a.vertical_alignment,
|
|
|
|
|
|
backwards: a.text_generation_flags & 0x2 != 0,
|
|
|
|
|
|
upside_down: a.text_generation_flags & 0x4 != 0,
|
|
|
|
|
|
layer: a.common.layer.clone(),
|
|
|
|
|
|
color: a.common.color,
|
|
|
|
|
|
linetype: a.common.linetype.clone(),
|
|
|
|
|
|
line_weight: a.common.line_weight,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Write one edited row back onto its attribute; returns whether anything
|
|
|
|
|
|
/// changed. Numeric fields are parsed (angles from degrees); an unparseable
|
|
|
|
|
|
/// field is left as-is.
|
|
|
|
|
|
fn apply_attr_row(a: &mut acadrust::entities::AttributeEntity, row: &AttrRow) -> bool {
|
|
|
|
|
|
let mut ch = false;
|
|
|
|
|
|
if a.get_value() != row.value {
|
|
|
|
|
|
a.set_value(row.value.clone());
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.text_style != row.text_style {
|
|
|
|
|
|
a.text_style = row.text_style.clone();
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok(h) = row.height.trim().parse::<f64>() {
|
|
|
|
|
|
if a.height != h {
|
|
|
|
|
|
a.height = h;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok(deg) = row.rotation.trim().parse::<f64>() {
|
|
|
|
|
|
let rad = deg.to_radians();
|
|
|
|
|
|
if (a.rotation - rad).abs() > 1e-12 {
|
|
|
|
|
|
a.rotation = rad;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok(w) = row.width_factor.trim().parse::<f64>() {
|
|
|
|
|
|
if a.width_factor != w {
|
|
|
|
|
|
a.width_factor = w;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok(deg) = row.oblique.trim().parse::<f64>() {
|
|
|
|
|
|
let rad = deg.to_radians();
|
|
|
|
|
|
if (a.oblique_angle - rad).abs() > 1e-12 {
|
|
|
|
|
|
a.oblique_angle = rad;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.horizontal_alignment != row.h_align {
|
|
|
|
|
|
a.horizontal_alignment = row.h_align;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.vertical_alignment != row.v_align {
|
|
|
|
|
|
a.vertical_alignment = row.v_align;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Text generation flags: bit 0x2 = backwards, bit 0x4 = upside down.
|
|
|
|
|
|
let new_flags = (a.text_generation_flags & !0x6)
|
|
|
|
|
|
| if row.backwards { 0x2 } else { 0 }
|
|
|
|
|
|
| if row.upside_down { 0x4 } else { 0 };
|
|
|
|
|
|
if new_flags != a.text_generation_flags {
|
|
|
|
|
|
a.text_generation_flags = new_flags;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.common.layer != row.layer {
|
|
|
|
|
|
a.common.layer = row.layer.clone();
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.common.color != row.color {
|
|
|
|
|
|
a.common.color = row.color;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.common.linetype != row.linetype {
|
|
|
|
|
|
a.common.linetype = row.linetype.clone();
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.common.line_weight != row.line_weight {
|
|
|
|
|
|
a.common.line_weight = row.line_weight;
|
|
|
|
|
|
ch = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
ch
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:23:54 +03:00
|
|
|
|
/// Map each attribute tag to the prompt text declared on the block's matching
|
|
|
|
|
|
/// ATTDEF. Attribute instances (ATTRIB) carry only tag + value; the prompt is
|
|
|
|
|
|
/// defined once on the block definition. Tags with no definition are absent.
|
|
|
|
|
|
fn block_attr_prompts(
|
|
|
|
|
|
doc: &acadrust::CadDocument,
|
|
|
|
|
|
block_name: &str,
|
|
|
|
|
|
) -> rustc_hash::FxHashMap<String, String> {
|
|
|
|
|
|
let mut map = rustc_hash::FxHashMap::default();
|
|
|
|
|
|
if let Some(br) = doc.block_records.get(block_name) {
|
|
|
|
|
|
for &eh in &br.entity_handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::AttributeDefinition(ad)) = doc.get_entity(eh) {
|
|
|
|
|
|
map.insert(ad.tag.clone(), ad.prompt.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
map
|
|
|
|
|
|
}
|