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;
|
2026-08-04 16:27:03 +03:00
|
|
|
|
use crate::scene::pick::grip::{
|
|
|
|
|
|
find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripEditMode,
|
|
|
|
|
|
};
|
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::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};
|
|
|
|
|
|
|
2026-07-31 23:36:07 +03:00
|
|
|
|
/// Return `(new vertex grip id, old vertex count)` for polyline Add Vertex.
|
|
|
|
|
|
/// LWPolyline segment grips live after the vertex grips, so translate their id
|
|
|
|
|
|
/// back to the segment before selecting the inserted vertex.
|
|
|
|
|
|
fn polyline_add_vertex_target(entity: &AcadEntityType, grip_id: usize) -> Option<(usize, usize)> {
|
|
|
|
|
|
match entity {
|
|
|
|
|
|
AcadEntityType::LwPolyline(polyline) => {
|
|
|
|
|
|
let n = polyline.vertices.len();
|
|
|
|
|
|
if n == 0 {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else if grip_id < n {
|
|
|
|
|
|
Some((grip_id + 1, n))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let segment = grip_id - n;
|
|
|
|
|
|
let segment_count = if polyline.is_closed {
|
|
|
|
|
|
n
|
|
|
|
|
|
} else {
|
|
|
|
|
|
n.saturating_sub(1)
|
|
|
|
|
|
};
|
|
|
|
|
|
(segment < segment_count).then_some((segment + 1, n))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
AcadEntityType::Polyline(polyline) if grip_id < polyline.vertices.len() => {
|
|
|
|
|
|
Some((grip_id + 1, polyline.vertices.len()))
|
|
|
|
|
|
}
|
|
|
|
|
|
AcadEntityType::Polyline2D(polyline) if grip_id < polyline.vertices.len() => {
|
|
|
|
|
|
Some((grip_id + 1, polyline.vertices.len()))
|
|
|
|
|
|
}
|
|
|
|
|
|
AcadEntityType::Polyline3D(polyline) if grip_id < polyline.vertices.len() => {
|
|
|
|
|
|
Some((grip_id + 1, polyline.vertices.len()))
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn polyline_vertex_count(entity: &AcadEntityType) -> Option<usize> {
|
|
|
|
|
|
match entity {
|
|
|
|
|
|
AcadEntityType::LwPolyline(polyline) => Some(polyline.vertices.len()),
|
|
|
|
|
|
AcadEntityType::Polyline(polyline) => Some(polyline.vertices.len()),
|
|
|
|
|
|
AcadEntityType::Polyline2D(polyline) => Some(polyline.vertices.len()),
|
|
|
|
|
|
AcadEntityType::Polyline3D(polyline) => Some(polyline.vertices.len()),
|
|
|
|
|
|
_ => 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
|
|
|
|
|
|
|
|
|
|
impl OpenCADStudio {
|
2026-07-27 12:02:50 +03:00
|
|
|
|
pub(super) fn begin_tab_close_queue(&mut self, tab_ids: Vec<u64>) -> Task<Message> {
|
|
|
|
|
|
self.pending_tab_closes.clear();
|
|
|
|
|
|
self.pending_tab_closes.extend(tab_ids);
|
|
|
|
|
|
self.continue_tab_close_queue()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Close queued drawings until a dirty tab requires confirmation. Queue
|
|
|
|
|
|
/// entries use stable document ids because removing an earlier tab changes
|
|
|
|
|
|
/// every later vector index.
|
|
|
|
|
|
pub(super) fn continue_tab_close_queue(&mut self) -> Task<Message> {
|
|
|
|
|
|
let mut tasks = Vec::new();
|
|
|
|
|
|
while let Some(tab_id) = self.pending_tab_closes.pop_front() {
|
|
|
|
|
|
let Some(idx) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
};
|
|
|
|
|
|
if self.tabs[idx].is_start {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.tabs[idx].dirty {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
|
|
|
|
|
tasks.push(self.open_unsaved_dialog_window());
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
tasks.push(self.on_tab_close(idx));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::batch(tasks)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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_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;
|
2026-07-21 23:58:05 +03:00
|
|
|
|
self.refresh_active_cmd_preview(i);
|
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
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
2026-08-02 23:32:54 +03:00
|
|
|
|
// In dynamic point entry, `@` and `#` are coordinate-mode
|
|
|
|
|
|
// switches rather than command-line text. Consuming them
|
|
|
|
|
|
// here keeps the prefix out of the command buffer while
|
|
|
|
|
|
// the following digits continue into the dynamic fields.
|
|
|
|
|
|
if matches!(s.as_str(), "@" | "#")
|
|
|
|
|
|
&& self.command_line.input.is_empty()
|
|
|
|
|
|
&& self.dyn_input
|
|
|
|
|
|
&& self.dyn_has_coordinate_fields(i)
|
|
|
|
|
|
{
|
|
|
|
|
|
self.dyn_set_coordinate_mode(s == "#");
|
|
|
|
|
|
self.command_line.autocomplete_cursor = None;
|
|
|
|
|
|
self.refresh_active_cmd_preview(i);
|
|
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
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
|
|
|
|
// 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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
|
self.command_line.push_error(crate::tf!(
|
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
|
|
|
|
"{}: expected a number, got \"{raw}\"",
|
|
|
|
|
|
pending.label
|
2026-08-02 22:09:25 +03:00
|
|
|
|
).as_ref());
|
2026-08-04 16:27:03 +03:00
|
|
|
|
self.grip_pending = Some(pending);
|
|
|
|
|
|
return self.focus_cmd_input();
|
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
|
|
|
|
};
|
|
|
|
|
|
let i = self.active_tab;
|
2026-08-04 16:27:03 +03:00
|
|
|
|
let interactive_lengthen = self.tabs[i]
|
|
|
|
|
|
.active_grip
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.is_some_and(|grip| {
|
|
|
|
|
|
grip.mode == GripEditMode::Lengthen
|
|
|
|
|
|
&& grip.handle == pending.handle
|
|
|
|
|
|
&& grip.grip_id == pending.grip_id
|
|
|
|
|
|
});
|
|
|
|
|
|
if interactive_lengthen {
|
|
|
|
|
|
self.cancel_active_grip_edit();
|
|
|
|
|
|
}
|
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::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);
|
|
|
|
|
|
}
|
2026-07-21 19:56:23 +03:00
|
|
|
|
// A typed grip-menu value reshapes dimensions too — drop a
|
|
|
|
|
|
// stale baked *D block (no-op for non-dims). (#398)
|
2026-07-24 23:25:33 +03:00
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.invalidate_dim_block_recorded(pending.handle);
|
2026-07-28 17:16:27 +03:00
|
|
|
|
self.tabs[i].scene.bump_entities(&[(
|
|
|
|
|
|
pending.handle,
|
|
|
|
|
|
crate::scene::ChangeKind::Modified,
|
|
|
|
|
|
)]);
|
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.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() {
|
2026-08-01 19:09:19 +03:00
|
|
|
|
return self.feed_command(crate::command::StepInput::Enter);
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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()) {
|
2026-07-24 17:56:06 +03:00
|
|
|
|
let pt = base + dir * dist;
|
2026-07-27 16:15:28 +03:00
|
|
|
|
if !self.command_point_allowed(i, pt) {
|
|
|
|
|
|
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
|
|
|
|
self.last_point = Some(pt);
|
|
|
|
|
|
self.dyn_user_reshaped = false;
|
2026-08-02 23:32:54 +03:00
|
|
|
|
self.dyn_coord_absolute = 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
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-07-27 16:15:28 +03:00
|
|
|
|
if !self.command_point_allowed(i, wcs_pt) {
|
|
|
|
|
|
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
|
|
|
|
self.last_point = Some(wcs_pt);
|
|
|
|
|
|
self.dyn_user_reshaped = false;
|
2026-08-02 23:32:54 +03:00
|
|
|
|
self.dyn_coord_absolute = 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
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-02 22:09:25 +03:00
|
|
|
|
self.command_line.push_error(crate::tf!(
|
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
|
|
|
|
"Expected coordinates (x,y) or a number, got: \"{text}\""
|
2026-08-02 22:09:25 +03:00
|
|
|
|
).as_ref());
|
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
|
|
|
|
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() {
|
2026-08-01 19:09:19 +03:00
|
|
|
|
self.feed_command(crate::command::StepInput::Enter)
|
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 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> {
|
2026-07-22 12:27:27 +03:00
|
|
|
|
// Esc drops an unconsumed one-shot snap override and closes
|
|
|
|
|
|
// its menu (#337). Falls through — Esc keeps its usual effect.
|
|
|
|
|
|
self.snap_override_popup = None;
|
|
|
|
|
|
self.snapper.clear_override();
|
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
|
|
|
|
// 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() {
|
2026-08-02 22:09:25 +03:00
|
|
|
|
self.command_line.push_info(crate::t!("*Cancel*").as_ref());
|
2026-07-03 12:42:55 +03:00
|
|
|
|
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;
|
2026-07-30 19:50:33 +03:00
|
|
|
|
// A UCS grip drag is live-only until mouse release. Reload
|
|
|
|
|
|
// the persisted pane UCS so Escape really cancels it.
|
|
|
|
|
|
if self.ucs_grip_drag.take().is_some() {
|
|
|
|
|
|
self.tabs[i].refresh_active_ucs();
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
}
|
2026-06-27 01:12:45 +03:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-08-02 22:09:25 +03:00
|
|
|
|
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
|
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
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-08-04 16:27:03 +03:00
|
|
|
|
if self.tabs[self.active_tab].active_grip.is_some() {
|
|
|
|
|
|
self.grip_pending = None;
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
if self.cancel_active_grip_edit() {
|
|
|
|
|
|
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
|
|
|
|
if self.grip_pending.take().is_some() {
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-28 18:51:20 +03:00
|
|
|
|
// A hot grip (click-move-click placement in progress) rolls
|
|
|
|
|
|
// back to its pre-drag image on Escape.
|
|
|
|
|
|
if self.cancel_active_grip_edit() {
|
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
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-28 01:34:45 +03:00
|
|
|
|
// Cancel layout rename first, then fall through.
|
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
|
|
|
|
let i_e = self.active_tab;
|
|
|
|
|
|
if self.qselect.take().is_some() {
|
2026-08-01 18:24:05 +03:00
|
|
|
|
self.reset_modal_geometry();
|
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
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-28 01:34:45 +03:00
|
|
|
|
if self.layout_rename_state.take().is_some() {
|
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
|
|
|
|
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);
|
2026-07-28 17:16:27 +03:00
|
|
|
|
// Per-viewport layer visibility changes the resident
|
|
|
|
|
|
// assembly, not any entity's tessellated geometry.
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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-08-02 22:09:25 +03:00
|
|
|
|
.push_info(crate::t!("Layer \"0\" and the current layer can't be deleted — skipped.").as_ref());
|
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;
|
2026-08-01 18:24:05 +03:00
|
|
|
|
self.reset_modal_geometry();
|
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();
|
2026-08-04 17:37:19 +03:00
|
|
|
|
if name == self.tabs[i].layers.current_layer {
|
|
|
|
|
|
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
|
|
|
|
// 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).
|
2026-07-31 22:05:30 +03:00
|
|
|
|
if let Some((_, g)) = self.tabs[i]
|
|
|
|
|
|
.selected_grip_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
|
|
|
|
.iter()
|
2026-07-31 22:05:30 +03:00
|
|
|
|
.zip(self.tabs[i].selected_grips.iter())
|
|
|
|
|
|
.find(|(owner, g)| **owner == popup.handle && g.id == popup.grip_id)
|
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
|
|
|
|
{
|
|
|
|
|
|
// "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)
|
|
|
|
|
|
};
|
2026-07-31 22:05:30 +03:00
|
|
|
|
self.tabs[i].active_grip = Some(GripEdit::single(
|
|
|
|
|
|
popup.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
|
|
|
|
grip_id,
|
|
|
|
|
|
is_translate,
|
2026-07-31 22:05:30 +03:00
|
|
|
|
g.world,
|
|
|
|
|
|
));
|
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
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
|
});
|
2026-08-04 16:27:03 +03:00
|
|
|
|
if matches!(item.action, GripMenuAction::Lengthen) {
|
|
|
|
|
|
if let Some((_, grip)) = self.tabs[i]
|
|
|
|
|
|
.selected_grip_handles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.zip(self.tabs[i].selected_grips.iter())
|
|
|
|
|
|
.find(|(owner, grip)| {
|
|
|
|
|
|
**owner == popup.handle && grip.id == popup.grip_id
|
|
|
|
|
|
})
|
|
|
|
|
|
{
|
|
|
|
|
|
self.tabs[i].active_grip = Some(GripEdit::lengthen(
|
|
|
|
|
|
popup.handle,
|
|
|
|
|
|
popup.grip_id,
|
|
|
|
|
|
grip.world,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_info(crate::t!("Specify point or enter distance:").as_ref());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_info(crate::tf!("{label}:").as_ref());
|
|
|
|
|
|
}
|
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
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
feat(pedit): convert prompt, join, fit/spline/decurve; join fixes (#263)
PEDIT reworked around a mode machine:
- Picking a line/arc asks "Object is not a polyline. Turn it into one?
[Yes/No] <Y>" — Yes converts in place (arc keeps its bulge) and
editing continues on the result. Pickfirst: an already-selected
polyline skips the select step, a selected line/arc goes straight to
the convert prompt.
- Join gathers with the normal selection system (single picks AND
window/crossing boxes) and merges through the JOIN machinery.
- Fit replaces every segment with a classic equal-parameter BIARC: two
arcs per segment meeting tangentially at an inserted knee, both
segments at a vertex sharing that vertex's tangent — the whole run is
tangent-continuous (verified: zero tangent jump on L/closed/zigzag/S
shapes). Spline resamples the vertex frame as a uniform cubic
B-spline (8 samples per span); Decurve straightens every bulge.
- Earlier hardening kept: undo snapshot before the mutation, immediate
repaint, polyline-only pick validation.
- Vertex editing lives on grips, so no Edit-vertex submode; instead the
vertex grip hover menu gains Break — a closed polyline opens at the
vertex, an open one splits into two (interior vertices only),
handled driver-side as an entity replacement.
JOIN itself: segs_of now emits one segment per span of an OPEN
polyline (bulges kept), so polylines merge with their neighbours —
PEDIT Join's set always contains the target polyline and used to abort
every time; plain JOIN had the same gap. JOIN also honours pickfirst:
launched with two or more objects selected it joins them immediately.
Closes #263
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 20:06:04 +03:00
|
|
|
|
// Break at vertex replaces the entity with the split pieces —
|
|
|
|
|
|
// structural, so it can't ride the in-place apply below.
|
|
|
|
|
|
if matches!(item.action, GripMenuAction::BreakVertex) {
|
|
|
|
|
|
let pieces = match self.tabs[i].scene.document.get_entity(popup.handle) {
|
|
|
|
|
|
Some(acadrust::EntityType::LwPolyline(p)) => {
|
|
|
|
|
|
crate::entities::lwpolyline::break_at_vertex(p, popup.grip_id)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
match pieces {
|
|
|
|
|
|
Some(pieces) => {
|
|
|
|
|
|
self.push_undo_snapshot(i, "BREAK");
|
|
|
|
|
|
self.tabs[i].scene.erase_entities(&[popup.handle]);
|
|
|
|
|
|
for e in pieces {
|
|
|
|
|
|
self.tabs[i].scene.add_entity(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
self.refresh_properties();
|
2026-08-02 22:09:25 +03:00
|
|
|
|
self.command_line.push_output(crate::t!("Polyline broken at vertex.").as_ref());
|
feat(pedit): convert prompt, join, fit/spline/decurve; join fixes (#263)
PEDIT reworked around a mode machine:
- Picking a line/arc asks "Object is not a polyline. Turn it into one?
[Yes/No] <Y>" — Yes converts in place (arc keeps its bulge) and
editing continues on the result. Pickfirst: an already-selected
polyline skips the select step, a selected line/arc goes straight to
the convert prompt.
- Join gathers with the normal selection system (single picks AND
window/crossing boxes) and merges through the JOIN machinery.
- Fit replaces every segment with a classic equal-parameter BIARC: two
arcs per segment meeting tangentially at an inserted knee, both
segments at a vertex sharing that vertex's tangent — the whole run is
tangent-continuous (verified: zero tangent jump on L/closed/zigzag/S
shapes). Spline resamples the vertex frame as a uniform cubic
B-spline (8 samples per span); Decurve straightens every bulge.
- Earlier hardening kept: undo snapshot before the mutation, immediate
repaint, polyline-only pick validation.
- Vertex editing lives on grips, so no Edit-vertex submode; instead the
vertex grip hover menu gains Break — a closed polyline opens at the
vertex, an open one splits into two (interior vertices only),
handled driver-side as an entity replacement.
JOIN itself: segs_of now emits one segment per span of an OPEN
polyline (bulges kept), so polylines merge with their neighbours —
PEDIT Join's set always contains the target polyline and used to abort
every time; plain JOIN had the same gap. JOIN also honours pickfirst:
launched with two or more objects selected it joins them immediately.
Closes #263
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 20:06:04 +03:00
|
|
|
|
}
|
|
|
|
|
|
None => self
|
|
|
|
|
|
.command_line
|
2026-08-02 22:09:25 +03:00
|
|
|
|
.push_error(crate::t!("Cannot break at this vertex.").as_ref()),
|
feat(pedit): convert prompt, join, fit/spline/decurve; join fixes (#263)
PEDIT reworked around a mode machine:
- Picking a line/arc asks "Object is not a polyline. Turn it into one?
[Yes/No] <Y>" — Yes converts in place (arc keeps its bulge) and
editing continues on the result. Pickfirst: an already-selected
polyline skips the select step, a selected line/arc goes straight to
the convert prompt.
- Join gathers with the normal selection system (single picks AND
window/crossing boxes) and merges through the JOIN machinery.
- Fit replaces every segment with a classic equal-parameter BIARC: two
arcs per segment meeting tangentially at an inserted knee, both
segments at a vertex sharing that vertex's tangent — the whole run is
tangent-continuous (verified: zero tangent jump on L/closed/zigzag/S
shapes). Spline resamples the vertex frame as a uniform cubic
B-spline (8 samples per span); Decurve straightens every bulge.
- Earlier hardening kept: undo snapshot before the mutation, immediate
repaint, polyline-only pick validation.
- Vertex editing lives on grips, so no Edit-vertex submode; instead the
vertex grip hover menu gains Break — a closed polyline opens at the
vertex, an open one splits into two (interior vertices only),
handled driver-side as an entity replacement.
JOIN itself: segs_of now emits one segment per span of an OPEN
polyline (bulges kept), so polylines merge with their neighbours —
PEDIT Join's set always contains the target polyline and used to abort
every time; plain JOIN had the same gap. JOIN also honours pickfirst:
launched with two or more objects selected it joins them immediately.
Closes #263
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 20:06:04 +03:00
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-31 23:36:07 +03:00
|
|
|
|
// Polyline Add Vertex is an interactive placement, not an
|
|
|
|
|
|
// immediate midpoint edit. Seed the undo snapshot before the
|
|
|
|
|
|
// provisional vertex exists, then engage its new grip so the
|
|
|
|
|
|
// regular snap/ortho/polar preview follows the cursor. One
|
|
|
|
|
|
// click commits the whole append+move; Escape restores the
|
|
|
|
|
|
// original entity.
|
|
|
|
|
|
if matches!(item.action, GripMenuAction::AddVertex) {
|
|
|
|
|
|
let placement = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.get_entity(popup.handle)
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.and_then(|original| {
|
|
|
|
|
|
polyline_add_vertex_target(&original, popup.grip_id)
|
|
|
|
|
|
.map(|(new_gid, old_len)| (original, new_gid, old_len))
|
|
|
|
|
|
});
|
|
|
|
|
|
if let Some((original, new_gid, old_len)) = placement {
|
|
|
|
|
|
let dirty_before = self.tabs[i].dirty;
|
|
|
|
|
|
if let Some(entity) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(popup.handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
entity.apply_grip_menu(popup.grip_id, item.action);
|
|
|
|
|
|
}
|
|
|
|
|
|
let inserted = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.get_entity(popup.handle)
|
|
|
|
|
|
.and_then(polyline_vertex_count)
|
|
|
|
|
|
.is_some_and(|len| len == old_len + 1);
|
|
|
|
|
|
if !inserted {
|
|
|
|
|
|
if let Some(entity) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(popup.handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
*entity = original;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.bump_entities(&[
|
|
|
|
|
|
(popup.handle, crate::scene::ChangeKind::Modified),
|
|
|
|
|
|
]);
|
|
|
|
|
|
self.tabs[i].dirty = dirty_before;
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
self.refresh_properties();
|
2026-08-02 22:09:25 +03:00
|
|
|
|
self.command_line.push_error(crate::t!("Cannot add a vertex here.").as_ref());
|
2026-07-31 23:36:07 +03:00
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.bump_entities(&[(popup.handle, crate::scene::ChangeKind::Modified)]);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
let grip_world = self.tabs[i]
|
|
|
|
|
|
.selected_grip_handles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.zip(self.tabs[i].selected_grips.iter())
|
|
|
|
|
|
.find(|(owner, grip)| {
|
|
|
|
|
|
**owner == popup.handle && grip.id == new_gid
|
|
|
|
|
|
})
|
|
|
|
|
|
.map(|(_, grip)| grip.world);
|
|
|
|
|
|
if let Some(grip_world) = grip_world {
|
|
|
|
|
|
self.grip_originals = vec![(popup.handle, original)];
|
|
|
|
|
|
self.grip_dirty_before = Some(dirty_before);
|
|
|
|
|
|
self.tabs[i].active_grip = Some(GripEdit::single(
|
|
|
|
|
|
popup.handle,
|
|
|
|
|
|
new_gid,
|
|
|
|
|
|
false,
|
|
|
|
|
|
grip_world,
|
|
|
|
|
|
));
|
|
|
|
|
|
self.command_line
|
2026-08-02 22:09:25 +03:00
|
|
|
|
.push_info(crate::t!("Specify new vertex location:").as_ref());
|
2026-07-31 23:36:07 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
if let Some(entity) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(popup.handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
*entity = original;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.bump_entities(&[
|
|
|
|
|
|
(popup.handle, crate::scene::ChangeKind::Modified),
|
|
|
|
|
|
]);
|
|
|
|
|
|
self.tabs[i].dirty = dirty_before;
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
self.command_line
|
2026-08-02 22:09:25 +03:00
|
|
|
|
.push_error(crate::t!("Cannot place the new vertex.").as_ref());
|
2026-07-31 23:36:07 +03:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
2026-07-21 19:56:23 +03:00
|
|
|
|
// Menu actions reshape dimensions too — drop a stale baked *D
|
|
|
|
|
|
// block so the edit is visible (no-op for non-dims). (#398)
|
2026-07-24 23:25:33 +03:00
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.invalidate_dim_block_recorded(popup.handle);
|
2026-07-28 17:16:27 +03:00
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.bump_entities(&[(popup.handle, crate::scene::ChangeKind::Modified)]);
|
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.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 {
|
2026-07-31 22:05:30 +03:00
|
|
|
|
if let Some((_, g)) = self.tabs[i]
|
|
|
|
|
|
.selected_grip_handles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.zip(self.tabs[i].selected_grips.iter())
|
|
|
|
|
|
.find(|(owner, g)| **owner == popup.handle && g.id == new_gid)
|
|
|
|
|
|
{
|
|
|
|
|
|
self.tabs[i].active_grip = Some(GripEdit::single(
|
|
|
|
|
|
popup.handle,
|
|
|
|
|
|
new_gid,
|
|
|
|
|
|
false,
|
|
|
|
|
|
g.world,
|
|
|
|
|
|
));
|
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.grip_add_provisional = Some((popup.handle, new_gid));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-22 12:15:36 +03:00
|
|
|
|
// Convert to Arc enters placement: the segment grip goes hot
|
|
|
|
|
|
// in Absolute mode, so the arc re-fits through the cursor as
|
|
|
|
|
|
// it moves and the next click seats it (#339).
|
|
|
|
|
|
if matches!(item.action, GripMenuAction::ConvertToArc) {
|
2026-07-31 22:05:30 +03:00
|
|
|
|
if let Some((_, g)) = self.tabs[i]
|
|
|
|
|
|
.selected_grip_handles
|
2026-07-22 12:15:36 +03:00
|
|
|
|
.iter()
|
2026-07-31 22:05:30 +03:00
|
|
|
|
.zip(self.tabs[i].selected_grips.iter())
|
|
|
|
|
|
.find(|(owner, g)| **owner == popup.handle && g.id == popup.grip_id)
|
2026-07-22 12:15:36 +03:00
|
|
|
|
{
|
2026-07-31 22:05:30 +03:00
|
|
|
|
self.tabs[i].active_grip = Some(GripEdit::single(
|
|
|
|
|
|
popup.handle,
|
|
|
|
|
|
popup.grip_id,
|
|
|
|
|
|
false,
|
|
|
|
|
|
g.world,
|
|
|
|
|
|
));
|
2026-07-22 12:15:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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"))]
|
2026-07-30 14:47:28 +03:00
|
|
|
|
return iced::clipboard::read_text().map(|result| {
|
|
|
|
|
|
Message::MTextPasteClip(result.ok().map(|text| (*text).clone()))
|
|
|
|
|
|
});
|
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.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,
|
|
|
|
|
|
});
|
2026-08-01 18:24:05 +03:00
|
|
|
|
self.reset_modal_geometry();
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-07-27 16:44:31 +03:00
|
|
|
|
let old_origin = dxf.pattern.lines.first().map(|line| {
|
|
|
|
|
|
(line.base_point.x, line.base_point.y)
|
|
|
|
|
|
});
|
|
|
|
|
|
let mut pattern = hatch_patterns::build_dxf_pattern(entry);
|
|
|
|
|
|
// Stored pattern lines are final world-space
|
|
|
|
|
|
// geometry. Preserve the selected hatch's scale,
|
|
|
|
|
|
// angle and origin when replacing the catalog
|
|
|
|
|
|
// pattern.
|
|
|
|
|
|
crate::entities::hatch::scale_pattern_geometry(
|
|
|
|
|
|
&mut pattern,
|
|
|
|
|
|
dxf.pattern_scale,
|
|
|
|
|
|
);
|
|
|
|
|
|
crate::entities::hatch::rotate_pattern_geometry(
|
|
|
|
|
|
&mut pattern,
|
|
|
|
|
|
dxf.pattern_angle,
|
|
|
|
|
|
);
|
|
|
|
|
|
if let (Some((old_x, old_y)), Some(new_origin)) =
|
|
|
|
|
|
(old_origin, pattern.lines.first())
|
|
|
|
|
|
{
|
|
|
|
|
|
let dx = old_x - new_origin.base_point.x;
|
|
|
|
|
|
let dy = old_y - new_origin.base_point.y;
|
|
|
|
|
|
crate::entities::hatch::translate_pattern_geometry(
|
|
|
|
|
|
&mut pattern,
|
|
|
|
|
|
dx,
|
|
|
|
|
|
dy,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
dxf.pattern = pattern;
|
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
|
|
|
|
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();
|
2026-07-30 19:50:33 +03:00
|
|
|
|
vp.ucs_per_viewport = true;
|
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 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;
|
2026-08-02 12:00:15 +03:00
|
|
|
|
let resolved_mleader_style = (field == "mleader_style")
|
|
|
|
|
|
.then(|| {
|
|
|
|
|
|
doc.objects.values().find_map(|object| match object {
|
|
|
|
|
|
acadrust::objects::ObjectType::MultiLeaderStyle(style)
|
|
|
|
|
|
if style.name == value => Some(style.clone()),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
.flatten();
|
2026-07-12 23:16:26 +03:00
|
|
|
|
let resolved: Option<acadrust::Handle> = match field {
|
2026-08-02 12:00:15 +03:00
|
|
|
|
"mleader_style" => {
|
|
|
|
|
|
resolved_mleader_style.as_ref().map(|style| style.handle)
|
|
|
|
|
|
}
|
2026-07-12 23:16:26 +03:00
|
|
|
|
"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;
|
|
|
|
|
|
}
|
2026-08-02 12:00:15 +03:00
|
|
|
|
let mut style_annotation = None;
|
|
|
|
|
|
if field == "mleader_style" {
|
|
|
|
|
|
if let Some(style) = &resolved_mleader_style {
|
|
|
|
|
|
crate::scene::annotative::apply_mleader_style_to_object(
|
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
style,
|
|
|
|
|
|
);
|
|
|
|
|
|
style_annotation = Some(style.is_annotative);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if let Some(acadrust::EntityType::MultiLeader(ml)) =
|
2026-07-12 23:16:26 +03:00
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"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,
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-02 12:00:15 +03:00
|
|
|
|
if let Some(annotative) = style_annotation {
|
|
|
|
|
|
if annotative {
|
|
|
|
|
|
if let Some(scale) =
|
|
|
|
|
|
self.tabs[i].scene.creation_annotation_scale_handle()
|
|
|
|
|
|
{
|
|
|
|
|
|
crate::scene::annotative::create_annotation_context(
|
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
scale,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
crate::scene::annotative::clear_annotation_context(
|
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-12 23:16:26 +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
|
|
|
|
} 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-22 11:11:10 +03:00
|
|
|
|
} else if field == "block" {
|
|
|
|
|
|
// Name dropdown on a block reference: re-point the
|
|
|
|
|
|
// selected inserts to the picked definition. A stale
|
|
|
|
|
|
// typed value in the text buffer would mask the pick,
|
|
|
|
|
|
// so drop it; the pick also closes the list.
|
|
|
|
|
|
self.tabs[i].properties.edit_buf.remove("block");
|
|
|
|
|
|
self.tabs[i].properties.edit_choice_open = false;
|
|
|
|
|
|
let canon = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.block_records
|
|
|
|
|
|
.get(&value)
|
|
|
|
|
|
.map(|br| br.name.clone());
|
|
|
|
|
|
if let Some(canon) = canon {
|
2026-07-28 17:16:27 +03:00
|
|
|
|
let mut changes = Vec::new();
|
2026-07-22 11:11:10 +03:00
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(acadrust::EntityType::Insert(ins)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
if ins.block_name != canon {
|
|
|
|
|
|
ins.block_name = canon.clone();
|
2026-07-28 17:16:27 +03:00
|
|
|
|
changes.push((
|
|
|
|
|
|
handle,
|
|
|
|
|
|
crate::scene::ChangeKind::Modified,
|
|
|
|
|
|
));
|
2026-07-22 11:11:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-28 17:16:27 +03:00
|
|
|
|
if !changes.is_empty() {
|
|
|
|
|
|
self.tabs[i].scene.bump_entities(&changes);
|
2026-07-22 11:11:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
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) {
|
2026-07-22 11:11:10 +03:00
|
|
|
|
// Block names are free-form text — a name like "10-5"
|
|
|
|
|
|
// must not be arithmetic-evaluated.
|
|
|
|
|
|
let val = if field == "block" {
|
|
|
|
|
|
raw_val
|
|
|
|
|
|
} else {
|
|
|
|
|
|
crate::app::expr_eval::eval_to_string(&raw_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
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
2026-07-22 11:11:10 +03:00
|
|
|
|
if field == "block" {
|
|
|
|
|
|
// Name row on a block reference: an existing name
|
|
|
|
|
|
// re-points the selected inserts; a new one renames
|
|
|
|
|
|
// the definition they share. Commit closes the list.
|
|
|
|
|
|
self.tabs[i].properties.edit_choice_open = false;
|
|
|
|
|
|
self.apply_block_name_commit(i, &handles, val.trim());
|
|
|
|
|
|
} else if field == "frozen_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
|
|
|
|
// 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
|
|
|
|
|
2026-07-22 11:11:10 +03:00
|
|
|
|
/// Properties-panel Name commit for block references. A `new` matching an
|
|
|
|
|
|
/// existing regular block re-points the selected inserts to it; otherwise
|
|
|
|
|
|
/// the definition the (single-block) selection references is renamed and
|
|
|
|
|
|
/// every insert of it follows. Anonymous/xref definitions never get here —
|
|
|
|
|
|
/// their Name row stays read-only.
|
|
|
|
|
|
fn apply_block_name_commit(&mut self, i: usize, handles: &[acadrust::Handle], new: &str) {
|
|
|
|
|
|
if new.is_empty() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Existing target → swap the selected references to it. Anonymous,
|
|
|
|
|
|
// xref and xref-dependent definitions are not valid targets.
|
|
|
|
|
|
let target = self.tabs[i].scene.document.block_records.get(new).map(|br| {
|
|
|
|
|
|
(
|
|
|
|
|
|
br.name.clone(),
|
|
|
|
|
|
br.is_anonymous() || br.flags.is_xref || br.name.contains('|'),
|
|
|
|
|
|
)
|
|
|
|
|
|
});
|
|
|
|
|
|
if let Some((canon, protected)) = target {
|
|
|
|
|
|
if protected {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-28 17:16:27 +03:00
|
|
|
|
let mut changes = Vec::new();
|
2026-07-22 11:11:10 +03:00
|
|
|
|
for &handle in handles {
|
|
|
|
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(acadrust::EntityType::Insert(ins)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
if ins.block_name != canon {
|
|
|
|
|
|
ins.block_name = canon.clone();
|
2026-07-28 17:16:27 +03:00
|
|
|
|
changes.push((handle, crate::scene::ChangeKind::Modified));
|
2026-07-22 11:11:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-28 17:16:27 +03:00
|
|
|
|
if !changes.is_empty() {
|
|
|
|
|
|
self.tabs[i].scene.bump_entities(&changes);
|
2026-07-22 11:11:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// New name → rename. Only when every selected insert references the
|
|
|
|
|
|
// same definition; a mixed selection makes the rename target ambiguous.
|
|
|
|
|
|
let mut old: Option<String> = None;
|
|
|
|
|
|
for &handle in handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Insert(ins)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
match &old {
|
|
|
|
|
|
None => old = Some(ins.block_name.clone()),
|
|
|
|
|
|
Some(o) if o.eq_ignore_ascii_case(&ins.block_name) => {}
|
|
|
|
|
|
_ => return,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(old) = old {
|
|
|
|
|
|
self.tabs[i].scene.rename_block(&old, new);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
2026-08-01 18:24:05 +03:00
|
|
|
|
self.reset_modal_geometry();
|
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) {
|
2026-08-02 22:09:25 +03:00
|
|
|
|
self.command_line.push_info(crate::tf!(
|
2026-07-01 13:14:33 +03:00
|
|
|
|
"Object is on locked layer \"{layer}\" — unlock the layer to edit its attributes."
|
2026-08-02 22:09:25 +03:00
|
|
|
|
).as_ref());
|
2026-07-01 13:14:33 +03:00
|
|
|
|
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.
|
2026-07-24 23:25:33 +03:00
|
|
|
|
self.discard_last_undo_entry(i);
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|