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 super::{Message, OpenCADStudio};
|
2026-06-27 11:50:11 +03:00
|
|
|
|
use crate::scene::VIEWCUBE_DRAW_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;
|
|
|
|
|
|
use acadrust::types::Color as AcadColor;
|
|
|
|
|
|
use iced::time::Instant;
|
|
|
|
|
|
use iced::Task;
|
|
|
|
|
|
|
|
|
|
|
|
/// Keystroke-derived messages that an open modal dialog must swallow so the
|
|
|
|
|
|
/// keyboard can't reach the main window (command line, F-key toggles, edit
|
|
|
|
|
|
/// shortcuts) while a dialog is up. `CommandEscape` is handled separately (it
|
|
|
|
|
|
/// closes the modal); a modal's own text fields emit their own messages, which
|
|
|
|
|
|
/// are not in this set. See [`OpenCADStudio::update`] and #126.
|
|
|
|
|
|
fn is_modal_blocked_key_msg(msg: &Message) -> bool {
|
|
|
|
|
|
matches!(
|
|
|
|
|
|
msg,
|
|
|
|
|
|
Message::CommandInput(_)
|
|
|
|
|
|
| Message::CommandAppendChar(_)
|
|
|
|
|
|
| Message::CommandSpace
|
|
|
|
|
|
| Message::CommandFinalize
|
|
|
|
|
|
| Message::CommandBackspace
|
|
|
|
|
|
| Message::CommandHistoryPrev
|
|
|
|
|
|
| Message::CommandHistoryNext
|
|
|
|
|
|
| Message::DynTabNext
|
|
|
|
|
|
| Message::MTextCaretMove(_)
|
|
|
|
|
|
| Message::DeleteSelected
|
|
|
|
|
|
| Message::ToggleSnapEnabled
|
|
|
|
|
|
| Message::ToggleGrid
|
|
|
|
|
|
| Message::ToggleOrtho
|
|
|
|
|
|
| Message::ToggleGridSnap
|
|
|
|
|
|
| Message::TogglePolar
|
|
|
|
|
|
| Message::ToggleOTrack
|
|
|
|
|
|
| Message::ToggleDynInput
|
|
|
|
|
|
| Message::TabNew
|
|
|
|
|
|
| Message::OpenFile
|
|
|
|
|
|
| Message::SaveFile
|
|
|
|
|
|
| Message::SaveAs
|
|
|
|
|
|
| Message::Undo
|
|
|
|
|
|
| Message::Redo
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
|
|
|
|
|
|
|
|
|
|
|
fn format_size(bytes: u64) -> String {
|
|
|
|
|
|
const KB: f64 = 1024.0;
|
|
|
|
|
|
const MB: f64 = KB * 1024.0;
|
|
|
|
|
|
const GB: f64 = MB * 1024.0;
|
|
|
|
|
|
let b = bytes as f64;
|
|
|
|
|
|
if b >= GB {
|
|
|
|
|
|
format!("{:.2} GB", b / GB)
|
|
|
|
|
|
} else if b >= MB {
|
|
|
|
|
|
format!("{:.1} MB", b / MB)
|
|
|
|
|
|
} else if b >= KB {
|
|
|
|
|
|
format!("{:.1} KB", b / KB)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
format!("{bytes} B")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mod command;
|
|
|
|
|
|
mod dialog;
|
|
|
|
|
|
mod dynamic;
|
|
|
|
|
|
mod file;
|
|
|
|
|
|
mod style;
|
|
|
|
|
|
mod util;
|
|
|
|
|
|
mod viewport;
|
|
|
|
|
|
|
|
|
|
|
|
impl OpenCADStudio {
|
|
|
|
|
|
/// Close the active in-canvas modal (Plan B), mirroring what closing the
|
|
|
|
|
|
/// old OS window did: a style editor discards its staged (un-applied)
|
|
|
|
|
|
/// changes, and the ribbon tool that launched the dialog is de-highlighted.
|
|
|
|
|
|
fn close_active_modal(&mut self) {
|
|
|
|
|
|
use super::ModalKind::*;
|
|
|
|
|
|
if matches!(
|
|
|
|
|
|
self.active_modal,
|
|
|
|
|
|
Some(TextStyle | DimStyle | TableStyle | MLeaderStyle | MlStyle)
|
|
|
|
|
|
) {
|
|
|
|
|
|
self.style_stage_discard();
|
|
|
|
|
|
}
|
|
|
|
|
|
match self.active_modal {
|
|
|
|
|
|
Some(Layers) => self.ribbon.deactivate_tool_if("LAYERS"),
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
Some(Plot) => {
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("PLOT");
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("PRINT");
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("PAGESETUP");
|
|
|
|
|
|
}
|
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
|
|
|
|
Some(TextStyle) => {
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("STYLE");
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("TEXTSTYLE");
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(TableStyle) => self.ribbon.deactivate_tool_if("TABLESTYLE"),
|
|
|
|
|
|
Some(MlStyle) => self.ribbon.deactivate_tool_if("MLSTYLE"),
|
|
|
|
|
|
Some(MLeaderStyle) => self.ribbon.deactivate_tool_if("MLEADERSTYLE"),
|
|
|
|
|
|
Some(LayoutManager) => {
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("LAYOUTMANAGER");
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("LAYOUTPANEL");
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(Plotstyle) => {
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("PLOTSTYLE");
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("STYLESMANAGER");
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(DimStyle) => self.ribbon.deactivate_tool_if("DIMSTYLE"),
|
|
|
|
|
|
Some(Shortcuts) => {
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("SHORTCUTS");
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("KEYBOARD");
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(About) => self.ribbon.deactivate_tool_if("ABOUT"),
|
|
|
|
|
|
// Dismissing these via ✕ is the cancel/decline path.
|
|
|
|
|
|
Some(Unsaved) => self.pending_close = None,
|
|
|
|
|
|
Some(AssocPrompt) => self.mark_assoc_prompted(),
|
2026-07-01 17:23:32 +03:00
|
|
|
|
// Cancel: leave the layer (and its objects) untouched.
|
|
|
|
|
|
Some(LayerDeleteWarning) => self.layer_delete_pending = None,
|
2026-07-01 08:05:25 +03:00
|
|
|
|
// Cancel: drop the working copy without touching the block.
|
|
|
|
|
|
Some(AttributeEditor) => {
|
|
|
|
|
|
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 = crate::ui::window::attribute_editor::AttrTab::Attribute;
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
feat(alias): centralize command aliases in user-editable ocad.pgp (#286/#288)
Command aliases (L→LINE, CC→COPYCLIP) were scattered across dispatch match
arms and CommandRegistration name arrays. Move them all into a single
user-editable file:
- assets/ocad.pgp: shipped default alias table (ALIAS,*COMMAND format),
embedded and copied to config/ocad.pgp on first launch; only that user
copy is read after. AA→AREA default closes #286.
- resolve_alias() rewrites the leading command token at the top of
dispatch_command_inner, so aliases resolve for every path (families,
plugins, Repeat menu). Args after the first space are untouched.
- Autocomplete hides alias keys and, when the whole input is an alias,
surfaces its target command as the top (highlighted) suggestion so the
highlight matches what Enter runs (AA→AREA, L→LINE).
- ALIASEDIT opens a new editor modal (add/edit/delete rows) with an Apply
button; Apply persists to ocad.pgp, closing discards unapplied edits.
- Strip alias tokens from all 8 dispatch match-arm files and 71 module
CommandRegistration arrays, keeping canonical/synonym/distinct-command
names. ocad.pgp is now the sole source: deleting or remapping an alias
there takes effect. CUI stays on the keyboard-shortcut panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:39:22 +03:00
|
|
|
|
// Closing (✕) discards edits made since the last Apply — matching the
|
|
|
|
|
|
// style editors. Committing happens only through the Apply button.
|
|
|
|
|
|
Some(Aliases) => {
|
|
|
|
|
|
self.alias_editor_rows.clear();
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("ALIASEDIT");
|
|
|
|
|
|
}
|
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.active_modal = None;
|
|
|
|
|
|
// Recentre the next dialog and drop any in-progress drag.
|
|
|
|
|
|
self.modal_offset = iced::Vector::ZERO;
|
|
|
|
|
|
self.modal_drag_last = None;
|
|
|
|
|
|
self.modal_dragging = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
|
|
|
|
|
// A modal dialog must capture the keyboard the same way it already
|
|
|
|
|
|
// captures the mouse. Otherwise keystrokes from the global key
|
|
|
|
|
|
// subscription leak past the modal into the command line and fire as
|
|
|
|
|
|
// commands once the dialog closes. While a modal is open, Escape
|
|
|
|
|
|
// closes it and every other keystroke-derived message is swallowed;
|
|
|
|
|
|
// the modal's own text fields keep working because they emit their own
|
|
|
|
|
|
// (non-blocked) messages. (#126)
|
|
|
|
|
|
if self.active_modal.is_some() {
|
|
|
|
|
|
if matches!(msg, Message::CommandEscape) {
|
|
|
|
|
|
return self.update(Message::CloseModal);
|
|
|
|
|
|
}
|
|
|
|
|
|
if is_modal_blocked_key_msg(&msg) {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let task = self.update_inner(msg);
|
|
|
|
|
|
// After every message, mirror the active command step's prompt so
|
|
|
|
|
|
// its history line stays pinned (non-fading) until the step changes.
|
|
|
|
|
|
let prompt = self.tabs[self.active_tab]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|c| c.prompt());
|
|
|
|
|
|
self.command_line.set_step_prompt(prompt);
|
feat(command): clickable option buttons in the command line (#304)
Commands now expose their step options as clickable buttons rendered
inline beside the command-line prompt, so options need not be typed.
Adds a CmdOption/options() API to CadCommand: each command returns the
options for its current step; the command line renders them as
uppercase buttons and a click feeds the keyword to the command exactly
as if typed (an empty keyword submits like Enter).
Construction commands become "central" — CIRCLE offers [3P/2P/TTR/TTT/
DIAMETER], ELLIPSE [ARC/AXIS], ARC [SCE/SCA/SEA/SER/CSA/3P], RECTANGLE
[ROTATION/CENTER], POLYGON [CIRCUMSCRIBED/EDGE] — each keyword hands off
to the existing dedicated variant command via CmdResult::Dispatch,
instead of the variants only being reachable as isolated commands.
Migrates the existing bracketed-option prompts (PLINE, PEDIT, SPLINE,
SCALE, FILLET, CHAMFER, HATCH, GRADIENT, SOLID, POINT, RAY, XLINE, TRIM,
EXTEND) to the same button API and strips the option text from the
prompt strings, keeping value/coordinate hints. PEDIT's Width option
now enters a value sub-step so the button acts on click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:16:59 +03:00
|
|
|
|
// Mirror the step's clickable options so they render as buttons (#304).
|
|
|
|
|
|
let opts = self.tabs[self.active_tab]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|c| c.options())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
self.command_line.set_step_options(opts);
|
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
|
|
|
|
// Persist UI preferences whenever a toggle changes them (issue #68).
|
|
|
|
|
|
self.persist_settings_if_changed();
|
|
|
|
|
|
// OTRACK acquires tracking points only while a command or grip drag is
|
|
|
|
|
|
// running; drop them once neither is active so the temporary tracking
|
|
|
|
|
|
// points / vectors disappear when the command ends (issue #64).
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_none()
|
|
|
|
|
|
&& self.tabs[i].active_grip.is_none()
|
|
|
|
|
|
&& !self.snapper.tracking_points.is_empty()
|
|
|
|
|
|
{
|
|
|
|
|
|
self.snapper.clear_tracking();
|
|
|
|
|
|
self.otrack_active = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
task
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Drop the OTRACK acquired points and the live alignment vector once a
|
|
|
|
|
|
/// point has been committed to the active command. Temporary tracking
|
|
|
|
|
|
/// points are reset on every input so they don't pile up across a
|
|
|
|
|
|
/// multi-point command and overwhelm the next pick (issue #85).
|
|
|
|
|
|
fn reset_tracking_after_point(&mut self) {
|
|
|
|
|
|
self.snapper.clear_tracking();
|
|
|
|
|
|
self.otrack_active = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn update_inner(&mut self, msg: Message) -> Task<Message> {
|
|
|
|
|
|
match msg {
|
|
|
|
|
|
// Web: a drawing referenced a script whose Noto subset isn't loaded
|
|
|
|
|
|
// yet (recorded during text tessellation). Kick off one fetch per
|
|
|
|
|
|
// pending script; the result comes back as `WebFontLoaded`. (#141)
|
|
|
|
|
|
Message::PollWebFonts => {
|
|
|
|
|
|
let pending = crate::scene::text::web_font::take_pending();
|
|
|
|
|
|
if pending.is_empty() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::batch(pending.into_iter().map(|script| {
|
|
|
|
|
|
Task::perform(crate::scene::text::web_font::fetch(script), move |res| {
|
|
|
|
|
|
Message::WebFontLoaded(script, res)
|
|
|
|
|
|
})
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Web: a per-script font arrived. Store it, drop the stale fallback
|
|
|
|
|
|
// glyph cache (entries that resolved to nothing while it loaded),
|
|
|
|
|
|
// and re-tessellate so the text appears. (#141)
|
|
|
|
|
|
Message::WebFontLoaded(script, res) => {
|
|
|
|
|
|
match res {
|
|
|
|
|
|
Ok(bytes) => {
|
|
|
|
|
|
crate::scene::text::web_font::insert(script, Some(bytes));
|
|
|
|
|
|
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
|
|
|
|
|
for tab in self.tabs.iter_mut() {
|
|
|
|
|
|
tab.scene.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
crate::scene::text::web_font::insert(script, None);
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error(&format!("Font load failed ({script:?}): {e}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::Tick(t) => self.on_tick(t),
|
|
|
|
|
|
|
|
|
|
|
|
Message::OpenFile => self.on_open_file(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::OpenPathPicked(None) => Task::none(),
|
|
|
|
|
|
|
2026-06-27 12:21:14 +03:00
|
|
|
|
Message::OpenUrl(url) => {
|
|
|
|
|
|
crate::sys::open_url(&url);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 20:53:11 +03:00
|
|
|
|
Message::StartSectionSelect(section) => {
|
|
|
|
|
|
self.start_section = section;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 21:02:05 +03:00
|
|
|
|
Message::TogglePropertiesBar => {
|
|
|
|
|
|
self.props_expanded = !self.props_expanded;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-27 14:24:47 +03:00
|
|
|
|
Message::ScrollLayoutTabs(dx) => iced::widget::operation::scroll_by(
|
|
|
|
|
|
iced::advanced::widget::Id::new(crate::ui::statusbar::LAYOUT_TABS_SCROLL_ID),
|
|
|
|
|
|
iced::widget::scrollable::AbsoluteOffset { x: dx, y: 0.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
|
|
|
|
Message::OpenRecent(path) => {
|
|
|
|
|
|
// Recents are read from disk every save → the path may be
|
|
|
|
|
|
// stale. Skip silently if the file no longer exists; the
|
|
|
|
|
|
// entry stays in the list so the user can clean it up.
|
|
|
|
|
|
match std::fs::metadata(&path) {
|
|
|
|
|
|
Ok(m) => self.update(Message::OpenPathPicked(Some((path, m.len())))),
|
|
|
|
|
|
Err(_) => {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"Recent file no longer exists: {}",
|
|
|
|
|
|
path.display()
|
|
|
|
|
|
));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RecentRemove(path) => {
|
feat(ui): replace app-menu logo with a quick-access file toolbar
Remove the OCS-logo button and the dropdown menu it opened; the top strip
now shows direct New / Open / Save / Save As / Print buttons ahead of the
undo/redo controls. Drop the HELP and OPTIONS commands (HELP dumped text to
the command line, OPTIONS aliased the drafting-settings popup).
The app-menu overlay is gone, so its rendering code, MENU_ITEMS, four dead
messages, and the LOGO_RED / GEAR / HELP assets are removed. The recents
list it held is the only survivor — moved into OpenCADStudio.recent_files
via a new app/recent.rs, backing the Start page's Recent Documents panel.
Quick-access icons are redrawn in a cleaner style (file-plus, folder-open,
device-floppy, file-export, printer) and tinted light so they read on the
dark strip; FOLDER / DOC keep their old glyphs for the file browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:51:49 +03:00
|
|
|
|
self.remove_recent(&path);
|
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-06 01:06:01 +03:00
|
|
|
|
Message::SetRecentLimit(limit) => {
|
|
|
|
|
|
self.set_recent_limit(limit);
|
|
|
|
|
|
// Resync the input box to the clamped, applied value.
|
|
|
|
|
|
self.recent_limit_input = self.recent_limit.to_string();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RecentLimitInput(s) => {
|
|
|
|
|
|
// Keep only digits while typing; applied on Enter (SetRecentLimit).
|
|
|
|
|
|
self.recent_limit_input = s.chars().filter(|c| c.is_ascii_digit()).collect();
|
|
|
|
|
|
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
|
|
|
|
Message::OpenPathPicked(Some((path, size_bytes))) => {
|
|
|
|
|
|
let name = path
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| "unknown".into());
|
|
|
|
|
|
let phase = std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
|
|
|
|
|
|
super::OPEN_PHASE_READING,
|
|
|
|
|
|
));
|
|
|
|
|
|
self.opening = Some(super::OpenProgress {
|
|
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
size_bytes,
|
|
|
|
|
|
phase: phase.clone(),
|
|
|
|
|
|
started: Instant::now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
let size_label = format_size(size_bytes);
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_info(&format!("Opening \"{name}\" ({size_label})…"));
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
crate::io::open_path_with_phase(path, phase),
|
|
|
|
|
|
Message::FileOpened,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::OpenCancel => {
|
|
|
|
|
|
if let Some(p) = self.opening.take() {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_info(&format!("Open cancelled: \"{}\"", p.name));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
Message::FileOpened(Ok((name, path, doc, caches))) => {
|
|
|
|
|
|
self.on_file_opened(name, path, doc, caches)
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
Message::FileOpened(Err(e)) => {
|
|
|
|
|
|
// If the user cancelled, the overlay was already cleared and
|
|
|
|
|
|
// we suppress the noise.
|
|
|
|
|
|
let was_open = self.opening.take().is_some();
|
|
|
|
|
|
if was_open && e != "Cancelled" {
|
|
|
|
|
|
self.command_line.push_error(&format!("Open failed: {e}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ImagePick => {
|
|
|
|
|
|
Task::perform(crate::io::pick_image_file(), Message::ImagePickResult)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ImagePickResult(Ok((path, pw, ph))) => {
|
|
|
|
|
|
use crate::command::CadCommand;
|
|
|
|
|
|
use crate::modules::draw::draw::raster_image::ImageCommand;
|
|
|
|
|
|
let path_str = path.to_string_lossy().into_owned();
|
|
|
|
|
|
let short = std::path::Path::new(&path_str)
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.and_then(|n| n.to_str())
|
|
|
|
|
|
.unwrap_or(&path_str)
|
|
|
|
|
|
.to_string();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("IMAGE \"{short}\": {pw}×{ph} px"));
|
|
|
|
|
|
let cmd = ImageCommand::new(path_str, pw, ph);
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ImagePickResult(Err(e)) => {
|
|
|
|
|
|
if e != "Cancelled" {
|
|
|
|
|
|
self.command_line.push_error(&format!("IMAGE: {e}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::XAttachPick => Task::perform(
|
|
|
|
|
|
async {
|
|
|
|
|
|
let handle = rfd::AsyncFileDialog::new()
|
|
|
|
|
|
.set_title("Select External Reference File")
|
2026-06-29 12:49:02 +03:00
|
|
|
|
.add_filter("CAD Files", &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"])
|
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
|
|
|
|
.add_filter("DWG Files", &["dwg", "DWG"])
|
|
|
|
|
|
.add_filter("DXF Files", &["dxf", "DXF"])
|
2026-06-29 12:49:02 +03:00
|
|
|
|
.add_filter("Backup Files", &["bak", "BAK"])
|
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
|
|
|
|
.pick_file()
|
|
|
|
|
|
.await;
|
|
|
|
|
|
match handle {
|
|
|
|
|
|
Some(h) => Ok(crate::sys::handle_path(&h)),
|
|
|
|
|
|
None => Err("Cancelled".to_string()),
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::XAttachPickResult,
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
Message::XAttachPickResult(Ok(path)) => {
|
|
|
|
|
|
use crate::command::CadCommand;
|
|
|
|
|
|
use crate::modules::insert::xattach::XAttachCommand;
|
|
|
|
|
|
let path_str = path.to_string_lossy().into_owned();
|
|
|
|
|
|
let cmd = XAttachCommand::with_path(path_str);
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::XAttachPickResult(Err(e)) => {
|
|
|
|
|
|
if e != "Cancelled" {
|
|
|
|
|
|
self.command_line.push_error(&format!("XATTACH: {e}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::WblockSave(block_name) => {
|
|
|
|
|
|
let name = block_name.clone();
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move {
|
|
|
|
|
|
let path = rfd::AsyncFileDialog::new()
|
|
|
|
|
|
.set_title("Save Block As")
|
|
|
|
|
|
.set_file_name("block.dwg")
|
|
|
|
|
|
.add_filter("DWG Files", &["dwg"])
|
|
|
|
|
|
.save_file()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map(|h| crate::sys::handle_path(&h));
|
|
|
|
|
|
(name, path)
|
|
|
|
|
|
},
|
|
|
|
|
|
|(name, path)| Message::WblockSaveResult(name, path),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
Message::WblockSaveResult(block_name, Some(path)) => {
|
|
|
|
|
|
self.on_wblock_save_result_some(block_name, path)
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
Message::WblockSaveResult(_, None) => Task::none(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::DataExtractionSave(csv) => {
|
|
|
|
|
|
let csv_clone = csv.clone();
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move {
|
|
|
|
|
|
let path = rfd::AsyncFileDialog::new()
|
|
|
|
|
|
.set_title("Save Data Extraction")
|
|
|
|
|
|
.set_file_name("extraction.csv")
|
|
|
|
|
|
.add_filter("CSV", &["csv"])
|
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
|
.save_file()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map(|h| crate::sys::handle_path(&h));
|
|
|
|
|
|
(csv_clone, path)
|
|
|
|
|
|
},
|
|
|
|
|
|
|(csv, path)| Message::DataExtractionSaveResult(csv, path),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::DataExtractionSaveResult(csv, Some(path)) => {
|
|
|
|
|
|
match std::fs::write(&path, csv.as_bytes()) {
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
let rows = csv.lines().count().saturating_sub(1);
|
|
|
|
|
|
let fname = path
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| path.to_string_lossy().into_owned());
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("DATAEXTRACTION {rows} rows → \"{fname}\""));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_error(&format!("DATAEXTRACTION: write failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::DataExtractionSaveResult(_, None) => Task::none(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::StlExport => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.meshes.is_empty() {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("STLOUT: no 3D mesh data in this drawing.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async {
|
|
|
|
|
|
rfd::AsyncFileDialog::new()
|
|
|
|
|
|
.set_title("Export STL")
|
|
|
|
|
|
.set_file_name("export.stl")
|
|
|
|
|
|
.add_filter("STL Files", &["stl"])
|
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
|
.save_file()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map(|h| crate::sys::handle_path(&h))
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::StlExportPath,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::StlExportPath(Some(path)) => self.on_stl_export_path_some(path),
|
|
|
|
|
|
|
|
|
|
|
|
Message::StlExportPath(None) => Task::none(),
|
|
|
|
|
|
|
|
|
|
|
|
// ── STEP AP203 export ─────────────────────────────────────────
|
|
|
|
|
|
Message::StepExport => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.meshes.is_empty() {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("STEPOUT: no 3D mesh data in this drawing.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async {
|
|
|
|
|
|
rfd::AsyncFileDialog::new()
|
|
|
|
|
|
.set_title("Export STEP AP203")
|
|
|
|
|
|
.set_file_name("export.step")
|
|
|
|
|
|
.add_filter("STEP Files", &["step", "stp"])
|
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
|
.save_file()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map(|h| crate::sys::handle_path(&h))
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::StepExportPath,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::StepExportPath(Some(path)) => self.on_step_export_path_some(path),
|
|
|
|
|
|
|
|
|
|
|
|
Message::StepExportPath(None) => Task::none(),
|
|
|
|
|
|
|
|
|
|
|
|
// ── OBJ import ────────────────────────────────────────────────
|
|
|
|
|
|
Message::ObjImport => Task::perform(
|
|
|
|
|
|
async {
|
|
|
|
|
|
rfd::AsyncFileDialog::new()
|
|
|
|
|
|
.set_title("Import OBJ Mesh")
|
|
|
|
|
|
.add_filter("Wavefront OBJ", &["obj", "OBJ"])
|
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
|
.pick_file()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map(|h| crate::sys::handle_path(&h))
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::ObjImportPath,
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ObjImportPath(Some(path)) => self.on_obj_import_path_some(path),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ObjImportPath(None) => Task::none(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::SaveFile => self.on_save_file(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::SaveAs => {
|
|
|
|
|
|
if self.read_only {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Read-only session (--read-only): saving is disabled.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.save_dialog_for_unsaved = false;
|
|
|
|
|
|
self.open_save_dialog_window(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SaveDialogFormatChanged(fmt) => {
|
|
|
|
|
|
let (ext, _) = crate::io::parse_save_format(&fmt);
|
|
|
|
|
|
let stem = std::path::Path::new(&self.save_dialog_filename)
|
|
|
|
|
|
.file_stem()
|
|
|
|
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| "drawing".to_string());
|
|
|
|
|
|
self.save_dialog_filename = format!("{stem}.{ext}");
|
|
|
|
|
|
self.save_dialog_format = fmt;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SaveDialogFilenameChanged(name) => {
|
|
|
|
|
|
self.save_dialog_filename = name;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SaveDialogConfirm => self.on_save_dialog_confirm(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::SaveDialogCancel => self.close_save_dialog_window(),
|
|
|
|
|
|
|
2026-07-11 09:12:36 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
Message::SaveDialogPathPicked(picked) => self.on_save_dialog_path_picked(picked),
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
Message::SaveDialogPathPicked(_) => 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
|
|
|
|
Message::ClearScene => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.push_undo_snapshot(i, "CLEAR");
|
|
|
|
|
|
self.tabs[i].scene.clear();
|
2026-06-26 23:06:40 +03:00
|
|
|
|
crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document);
|
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].properties = PropertiesPanel::empty();
|
|
|
|
|
|
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.command_line
|
|
|
|
|
|
.push_output("Scene cleared. Standard linetypes loaded.");
|
|
|
|
|
|
self.tabs[i].current_path = None;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SetWireframe(w) => {
|
|
|
|
|
|
// Back-compat shim: forward to the new render-mode path so
|
|
|
|
|
|
// the ribbon button + WIREFRAME / SOLID command line still
|
|
|
|
|
|
// work without duplicating the rendering plumbing.
|
|
|
|
|
|
let mode = if w {
|
|
|
|
|
|
acadrust::entities::ViewportRenderMode::Wireframe2D
|
|
|
|
|
|
} else {
|
|
|
|
|
|
acadrust::entities::ViewportRenderMode::FlatShaded
|
|
|
|
|
|
};
|
|
|
|
|
|
Task::done(Message::SetRenderMode(mode))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SetRenderMode(mode) => self.on_set_render_mode(mode),
|
|
|
|
|
|
|
|
|
|
|
|
Message::SetProjection(ortho) => {
|
|
|
|
|
|
use crate::scene::Projection;
|
|
|
|
|
|
let proj = if ortho {
|
|
|
|
|
|
Projection::Orthographic
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Projection::Perspective
|
|
|
|
|
|
};
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.camera.borrow_mut().projection = proj;
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
self.ribbon.set_ortho(ortho);
|
|
|
|
|
|
self.command_line.push_output(if ortho {
|
|
|
|
|
|
"Projection: Orthographic"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"Projection: Perspective"
|
|
|
|
|
|
});
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RibbonSelectTab(idx) => {
|
|
|
|
|
|
self.ribbon.select(idx);
|
|
|
|
|
|
Task::none()
|
2026-07-06 01:06:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SetRibbonCollapseMode(mode) => {
|
|
|
|
|
|
self.ribbon.set_collapse_mode(mode);
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
self.save_config();
|
2026-07-06 01:06:01 +03:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
Message::RibbonToolClick { tool_id, event } => {
|
|
|
|
|
|
self.on_ribbon_tool_click(tool_id, event)
|
|
|
|
|
|
}
|
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
|
|
|
|
Message::PluginFileDialogResult { command, path } => {
|
|
|
|
|
|
if let Some(path) = path {
|
|
|
|
|
|
// Dispatch "<command> <path>" with original case intact —
|
|
|
|
|
|
// the command line would upper-case the whole string and
|
|
|
|
|
|
// mangle case-sensitive paths on Linux/macOS.
|
|
|
|
|
|
let line = format!("{} {}", command, path.to_string_lossy());
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if !crate::plugin::try_dispatch(self, i, &line) {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error(&format!("No plugin handled: {command}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Document tabs ─────────────────────────────────────────────
|
|
|
|
|
|
Message::TabNew => {
|
feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:45:08 +03:00
|
|
|
|
// Preserve the outgoing drawing's Ortho / running OSNAP.
|
|
|
|
|
|
self.stamp_header_sysvars(self.active_tab);
|
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.tab_counter += 1;
|
|
|
|
|
|
let new_tab = super::document::DocumentTab::new_drawing(self.tab_counter);
|
|
|
|
|
|
self.tabs.push(new_tab);
|
|
|
|
|
|
self.active_tab = self.tabs.len() - 1;
|
|
|
|
|
|
let idx = self.active_tab;
|
feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:45:08 +03:00
|
|
|
|
// A fresh drawing inherits the app's current Ortho / OSNAP so
|
|
|
|
|
|
// creating one doesn't silently reset them (its header default is
|
|
|
|
|
|
// 0 = no snaps); saving then persists them into the file.
|
|
|
|
|
|
self.stamp_header_sysvars(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.apply_bg_default(idx);
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.sync_ribbon_styles();
|
|
|
|
|
|
// #21: reset ribbon Color / Linetype / Lineweight to the
|
|
|
|
|
|
// fresh tab's defaults (ByLayer) instead of inheriting the
|
|
|
|
|
|
// previous tab's last selection.
|
|
|
|
|
|
self.sync_ribbon_from_selection();
|
|
|
|
|
|
// A fresh drawing starts with grid/snap off (its tile defaults).
|
|
|
|
|
|
self.adopt_view_display(self.active_tab);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TabSwitch(idx) => {
|
|
|
|
|
|
if idx < self.tabs.len() {
|
2026-07-01 08:05:25 +03:00
|
|
|
|
if idx != self.active_tab {
|
|
|
|
|
|
// The attribute editor is tab-scoped; leaving its tab
|
|
|
|
|
|
// drops it (its handle is that document's, not this one's).
|
|
|
|
|
|
self.cancel_attr_editor();
|
feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:45:08 +03:00
|
|
|
|
// Persist the outgoing drawing's Ortho / running OSNAP
|
|
|
|
|
|
// before leaving it, so switching back restores them.
|
|
|
|
|
|
let prev = self.active_tab;
|
|
|
|
|
|
self.stamp_header_sysvars(prev);
|
2026-07-01 08:05:25 +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
|
|
|
|
self.active_tab = idx;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.sync_ribbon_styles();
|
|
|
|
|
|
// #21: also re-seed ribbon Color / Linetype / Lineweight
|
|
|
|
|
|
// from the newly active tab so they reflect that doc's
|
|
|
|
|
|
// CECOLOR / CELTYPE / CELWEIGHT (or its current selection
|
|
|
|
|
|
// if there is one), not the prior tab's choice.
|
|
|
|
|
|
self.sync_ribbon_from_selection();
|
|
|
|
|
|
// Grid/snap follow the newly active drawing's viewport.
|
|
|
|
|
|
self.adopt_view_display(idx);
|
feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:45:08 +03:00
|
|
|
|
// Ortho / running OSNAP follow the newly active drawing.
|
|
|
|
|
|
self.adopt_header_sysvars(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
|
|
|
|
// Shared CJK ideographs follow the newly active drawing's
|
|
|
|
|
|
// language; re-tessellate if it differs from the last. (#141)
|
|
|
|
|
|
if crate::scene::text::web_font::set_cjk_lang_from_codepage(
|
|
|
|
|
|
&self.tabs[idx].scene.document.header.code_page,
|
|
|
|
|
|
) {
|
|
|
|
|
|
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
|
|
|
|
|
self.tabs[idx].scene.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TabClose(idx) => self.on_tab_close(idx),
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandInput(s) => {
|
2026-07-09 13:18:49 +03:00
|
|
|
|
// Space submits (acts like Enter) so a command advances
|
|
|
|
|
|
// token-by-token, matching CAD convention. A leading `>` switches
|
|
|
|
|
|
// to literal-space mode so an argument containing spaces (a text
|
|
|
|
|
|
// string, a path, `UCS Z 90` as one line) can be typed; the `>`
|
|
|
|
|
|
// is stripped on submit. (Unfocused Space repeats the last
|
|
|
|
|
|
// command via CommandSpace.)
|
|
|
|
|
|
// Command-line entry is shown uppercase.
|
|
|
|
|
|
let s = s.to_uppercase();
|
|
|
|
|
|
// A space submits (acts like Enter). The whole value is handed to
|
|
|
|
|
|
// the submit path, which tokenises multi-token lines — so a typed
|
|
|
|
|
|
// token, a pasted `LINE 0,0 10,10`, or API-fed text all run their
|
|
|
|
|
|
// spaces as step separators. A leading `>` keeps spaces literal.
|
|
|
|
|
|
if !s.starts_with('>') && s.contains(' ') {
|
|
|
|
|
|
self.command_line.input = s;
|
|
|
|
|
|
return self.update(Message::CommandSubmit);
|
|
|
|
|
|
}
|
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 = s;
|
|
|
|
|
|
// Typing invalidates the previous arrow-key cursor —
|
|
|
|
|
|
// the matches list has likely changed.
|
|
|
|
|
|
self.command_line.autocomplete_cursor = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandAppendChar(s) => self.on_command_append_char(s),
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandBackspace => self.on_command_backspace(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::DynTabNext if self.grip_popup.is_some() => {
|
|
|
|
|
|
if let Some(popup) = self.grip_popup.as_mut() {
|
|
|
|
|
|
if !popup.items.is_empty() {
|
|
|
|
|
|
popup.selected = (popup.selected + 1) % popup.items.len();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::DynTabNext => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let n = self.tabs[i].dyn_fields.len();
|
|
|
|
|
|
if n > 0 {
|
|
|
|
|
|
self.tabs[i].dyn_active = (self.tabs[i].dyn_active + 1) % n;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.focus_cmd_input()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SplitModelViewport(horizontal) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-06-27 11:50:11 +03:00
|
|
|
|
self.tabs[i].scene.split_active_pane(horizontal);
|
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].scene.camera_generation += 1;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CloseModelViewport => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-06-27 11:50:11 +03:00
|
|
|
|
self.tabs[i].scene.close_active_pane();
|
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].scene.camera_generation += 1;
|
2026-06-27 11:50:11 +03:00
|
|
|
|
self.sync_render_mode_to_active_tile(i);
|
|
|
|
|
|
self.adopt_view_display(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
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandHistoryPrev => {
|
|
|
|
|
|
// Grip popup wins first — arrow keys walk its items.
|
|
|
|
|
|
if let Some(popup) = self.grip_popup.as_mut() {
|
|
|
|
|
|
if !popup.items.is_empty() {
|
|
|
|
|
|
popup.selected = if popup.selected == 0 {
|
|
|
|
|
|
popup.items.len() - 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
popup.selected - 1
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// While autocomplete is showing suggestions, ↑ walks up
|
|
|
|
|
|
// that list. Otherwise it falls back to recall history.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_none() && self.command_line.autocomplete_prev() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.history_prev();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandHistoryNext => {
|
|
|
|
|
|
if let Some(popup) = self.grip_popup.as_mut() {
|
|
|
|
|
|
if !popup.items.is_empty() {
|
|
|
|
|
|
popup.selected = (popup.selected + 1) % popup.items.len();
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_none() && self.command_line.autocomplete_next() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.history_next();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandHistoryToggle => {
|
|
|
|
|
|
self.command_line.toggle_history();
|
2026-07-01 18:17:46 +03:00
|
|
|
|
// On open, snapshot the current log into the read-only editor
|
|
|
|
|
|
// buffer so it can be drag-selected across lines and copied.
|
|
|
|
|
|
if self.command_line.history_open {
|
|
|
|
|
|
use iced::widget::text_editor::{Action, Motion};
|
|
|
|
|
|
self.history_content = iced::widget::text_editor::Content::with_text(
|
|
|
|
|
|
&self.command_line.history_plain_text(),
|
|
|
|
|
|
);
|
|
|
|
|
|
// Scroll to the newest line (bottom), the most useful when
|
|
|
|
|
|
// opening the log to grab a recent error.
|
|
|
|
|
|
self.history_content.perform(Action::Move(Motion::DocumentEnd));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandHistoryCopy => {
|
|
|
|
|
|
let text = self.command_line.history_plain_text();
|
|
|
|
|
|
if text.is_empty() {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::clipboard::write(text)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandHistoryClear => {
|
|
|
|
|
|
self.command_line.clear_history();
|
|
|
|
|
|
self.history_content = iced::widget::text_editor::Content::new();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandHistoryEdit(action) => {
|
|
|
|
|
|
// Read-only: drop edits, keep selection / cursor / scroll so
|
|
|
|
|
|
// the user can still highlight and Ctrl+C the log.
|
|
|
|
|
|
if !action.is_edit() {
|
|
|
|
|
|
self.history_content.perform(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
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandSuggestionPick(cmd) => {
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
self.command_line.close_history();
|
|
|
|
|
|
self.dispatch_command(&cmd)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(command): clickable option buttons in the command line (#304)
Commands now expose their step options as clickable buttons rendered
inline beside the command-line prompt, so options need not be typed.
Adds a CmdOption/options() API to CadCommand: each command returns the
options for its current step; the command line renders them as
uppercase buttons and a click feeds the keyword to the command exactly
as if typed (an empty keyword submits like Enter).
Construction commands become "central" — CIRCLE offers [3P/2P/TTR/TTT/
DIAMETER], ELLIPSE [ARC/AXIS], ARC [SCE/SCA/SEA/SER/CSA/3P], RECTANGLE
[ROTATION/CENTER], POLYGON [CIRCUMSCRIBED/EDGE] — each keyword hands off
to the existing dedicated variant command via CmdResult::Dispatch,
instead of the variants only being reachable as isolated commands.
Migrates the existing bracketed-option prompts (PLINE, PEDIT, SPLINE,
SCALE, FILLET, CHAMFER, HATCH, GRADIENT, SOLID, POINT, RAY, XLINE, TRIM,
EXTEND) to the same button API and strips the option text from the
prompt strings, keeping value/coordinate hints. PEDIT's Width option
now enters a value sub-step so the button acts on click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:16:59 +03:00
|
|
|
|
Message::CommandOptionPick(kw) => {
|
|
|
|
|
|
// Clicking an option button feeds its keyword to the active
|
|
|
|
|
|
// command through the same path as typed text; an empty keyword
|
|
|
|
|
|
// finishes the step like Enter. (#304)
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
self.command_line.close_history();
|
|
|
|
|
|
if kw.is_empty() {
|
|
|
|
|
|
return self.feed_command(crate::command::StepInput::Enter);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.feed_active_cmd(&kw);
|
|
|
|
|
|
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
|
|
|
|
Message::CommandSubmit => self.on_command_submit(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandSpace => {
|
|
|
|
|
|
// Space is a literal space inside the MText preview; otherwise
|
|
|
|
|
|
// it finalises the active command like Enter.
|
|
|
|
|
|
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
|
|
|
|
|
self.mtext_type(" ");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-09 13:18:49 +03:00
|
|
|
|
// A leading `>` puts the command line in "literal space" mode so
|
|
|
|
|
|
// the user can type arguments that contain spaces; otherwise
|
|
|
|
|
|
// Space works like Enter. The `>` is stripped on submit.
|
|
|
|
|
|
if self.command_line.input.starts_with('>') {
|
|
|
|
|
|
self.command_line.input.push(' ');
|
|
|
|
|
|
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
|
|
|
|
return self.update(Message::CommandFinalize);
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CommandFinalize => self.on_command_finalize(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandEscape => self.on_command_escape(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::Command(cmd) => {
|
|
|
|
|
|
// Close viewport context menu if open.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
|
|
|
|
|
// Any command also dismisses the Isolate action menu.
|
|
|
|
|
|
self.isolate_popup_open = false;
|
2026-07-05 23:12:52 +02:00
|
|
|
|
// "Pick window" (PLOTWINDOW) from Page Setup needs the backdrop
|
|
|
|
|
|
// gone so the viewport pick lands; every other command leaves an
|
|
|
|
|
|
// open modal (and its staged edits) untouched.
|
|
|
|
|
|
if cmd.trim().eq_ignore_ascii_case("PLOTWINDOW")
|
|
|
|
|
|
|| cmd.trim().eq_ignore_ascii_case("PW")
|
|
|
|
|
|
{
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
}
|
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.dispatch_command(&cmd)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ToggleLayers => {
|
|
|
|
|
|
if self.active_modal == Some(super::ModalKind::Layers) {
|
|
|
|
|
|
self.ribbon.deactivate_tool_if("LAYERS");
|
|
|
|
|
|
self.active_modal = None;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::Layers);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::WindowCloseRequested(id) => {
|
|
|
|
|
|
if self.main_window == Some(id) {
|
|
|
|
|
|
if self.tabs.iter().any(|t| t.dirty) {
|
|
|
|
|
|
self.pending_close = Some(super::PendingClose::Quit);
|
|
|
|
|
|
return self.open_unsaved_dialog_window();
|
|
|
|
|
|
}
|
2026-06-29 13:34:16 +03:00
|
|
|
|
return self.exit_app();
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::OsWindowClosed(id) => {
|
|
|
|
|
|
// Only the main window exists now; all dialogs are in-canvas
|
|
|
|
|
|
// modals (Plan B). Closing it exits.
|
|
|
|
|
|
if self.main_window == Some(id) {
|
2026-06-29 13:34:16 +03:00
|
|
|
|
return self.exit_app();
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Layer panel messages ───────────────────────────────────────
|
|
|
|
|
|
Message::LayerToggleVisible(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-03 16:06:10 +03:00
|
|
|
|
// New state = toggle of the clicked row, applied to every target
|
|
|
|
|
|
// (the whole selection when the clicked row is part of it) (#236).
|
|
|
|
|
|
let on = self.tabs[i].layers.layers.get(idx).map(|l| !l.visible);
|
|
|
|
|
|
let targets = self.layer_row_action_targets(i, idx);
|
|
|
|
|
|
if let Some(on) = on {
|
|
|
|
|
|
if !targets.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER OFF/ON");
|
|
|
|
|
|
for name in &targets {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
dl.flags.off = !on;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(pl) =
|
|
|
|
|
|
self.tabs[i].layers.layers.iter_mut().find(|l| &l.name == name)
|
|
|
|
|
|
{
|
|
|
|
|
|
pl.visible = on;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"{} layer(s) turned {}",
|
|
|
|
|
|
targets.len(),
|
|
|
|
|
|
if on { "on" } else { "off" }
|
|
|
|
|
|
));
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerSort(col) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].layers.sort_by(col);
|
|
|
|
|
|
// Keep the ribbon dropdown's order (and its toggle indices) in
|
|
|
|
|
|
// step with the re-sorted manager table.
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerToggleLock(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-03 16:06:10 +03:00
|
|
|
|
let locked = self.tabs[i].layers.layers.get(idx).map(|l| !l.locked);
|
|
|
|
|
|
let targets = self.layer_row_action_targets(i, idx);
|
|
|
|
|
|
if let Some(locked) = locked {
|
|
|
|
|
|
if !targets.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK");
|
|
|
|
|
|
for name in &targets {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
dl.flags.locked = locked;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(pl) =
|
|
|
|
|
|
self.tabs[i].layers.layers.iter_mut().find(|l| &l.name == name)
|
|
|
|
|
|
{
|
|
|
|
|
|
pl.locked = locked;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"{} layer(s) {}",
|
|
|
|
|
|
targets.len(),
|
|
|
|
|
|
if locked { "locked" } else { "unlocked" }
|
|
|
|
|
|
));
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerToggleFreeze(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-03 16:06:10 +03:00
|
|
|
|
let frozen = self.tabs[i].layers.layers.get(idx).map(|l| !l.frozen);
|
|
|
|
|
|
let targets = self.layer_row_action_targets(i, idx);
|
|
|
|
|
|
if let Some(frozen) = frozen {
|
|
|
|
|
|
if !targets.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER FREEZE");
|
|
|
|
|
|
for name in &targets {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
if frozen {
|
|
|
|
|
|
dl.freeze();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dl.thaw();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(pl) =
|
|
|
|
|
|
self.tabs[i].layers.layers.iter_mut().find(|l| &l.name == name)
|
|
|
|
|
|
{
|
|
|
|
|
|
pl.frozen = frozen;
|
|
|
|
|
|
}
|
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-03 16:06:10 +03:00
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"{} layer(s) {}",
|
|
|
|
|
|
targets.len(),
|
|
|
|
|
|
if frozen { "frozen" } else { "thawed" }
|
|
|
|
|
|
));
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
Message::LayerToggleVpFreeze(layer_idx, vp_col_idx) => {
|
|
|
|
|
|
self.on_layer_toggle_vp_freeze(layer_idx, vp_col_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
|
|
|
|
|
|
|
|
|
|
Message::LayerNew => self.on_layer_new(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerDelete => self.on_layer_delete(),
|
|
|
|
|
|
|
2026-07-01 17:23:32 +03:00
|
|
|
|
Message::LayerDeleteConfirm => self.on_layer_delete_confirm(),
|
|
|
|
|
|
|
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
|
|
|
|
Message::LayerSetCurrent => self.on_layer_set_current(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerSelect(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].layers.editing.is_some() {
|
|
|
|
|
|
return Task::done(Message::LayerRenameCommit);
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
let (shift, ctrl) = (self.shift_down, self.ctrl_down);
|
|
|
|
|
|
let panel = &mut self.tabs[i].layers;
|
|
|
|
|
|
if ctrl {
|
|
|
|
|
|
// Ctrl/Cmd-click toggles this row in the selection.
|
|
|
|
|
|
if let Some(pos) = panel.selected_multi.iter().position(|&x| x == idx) {
|
|
|
|
|
|
panel.selected_multi.remove(pos);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
panel.selected_multi.push(idx);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if shift {
|
|
|
|
|
|
// Shift-click selects the range from the anchor to here.
|
|
|
|
|
|
let anchor = panel.selected.unwrap_or(idx);
|
|
|
|
|
|
let (lo, hi) = (anchor.min(idx), anchor.max(idx));
|
|
|
|
|
|
panel.selected_multi = (lo..=hi).collect();
|
2026-07-03 16:06:10 +03:00
|
|
|
|
} else if !panel.selected_multi.contains(&idx) {
|
|
|
|
|
|
// Plain click collapses to this row — but NOT when it is
|
|
|
|
|
|
// already part of a multi-selection, so clicking a property
|
|
|
|
|
|
// combo (linetype / lineweight) on one of the selected rows
|
|
|
|
|
|
// keeps the selection and the edit stays bulk (#236).
|
2026-07-01 17:44:19 +03:00
|
|
|
|
panel.selected_multi = vec![idx];
|
|
|
|
|
|
}
|
|
|
|
|
|
panel.selected = Some(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
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerRenameStart(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
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
|
|
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|
|
|
|
|
self.tabs[i].layers.edit_buf = layer.name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].layers.editing = Some(idx);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerRenameEdit(s) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].layers.edit_buf = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerRenameCommit => self.on_layer_rename_commit(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerColorPickerToggle(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let panel = &mut self.tabs[i].layers;
|
|
|
|
|
|
if panel.color_picker_row == Some(idx) {
|
|
|
|
|
|
panel.color_picker_row = None;
|
|
|
|
|
|
panel.color_full_palette = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
panel.color_picker_row = Some(idx);
|
|
|
|
|
|
panel.color_full_palette = false;
|
|
|
|
|
|
panel.selected = Some(idx);
|
2026-07-01 17:44:19 +03:00
|
|
|
|
// Opening the swatch on a row outside the current
|
|
|
|
|
|
// multi-selection narrows to just that row; on a selected
|
|
|
|
|
|
// row it keeps the multi-selection so the pick applies to all.
|
|
|
|
|
|
if !panel.selected_multi.contains(&idx) {
|
|
|
|
|
|
panel.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
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerColorMorePalette => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].layers.color_full_palette = !self.tabs[i].layers.color_full_palette;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerColorSet(aci) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-01 17:44:19 +03:00
|
|
|
|
// Apply to every selected layer (multi-select), not just one.
|
|
|
|
|
|
let names = self.selected_layer_names(i);
|
|
|
|
|
|
if !names.is_empty() {
|
|
|
|
|
|
use crate::ui::window::layers::iced_color_from_acad;
|
|
|
|
|
|
let new_color = iced_color_from_acad(&AcadColor::Index(aci));
|
|
|
|
|
|
for name in &names {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(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
|
|
|
|
dl.color = AcadColor::Index(aci);
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
for pl in self.tabs[i].layers.layers.iter_mut() {
|
|
|
|
|
|
if names.contains(&pl.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
|
|
|
|
pl.color = new_color;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
// ByLayer color is baked into the cached wires at
|
|
|
|
|
|
// tessellation time, so bump the geometry epoch to
|
|
|
|
|
|
// invalidate the wire cache and repaint with the new color.
|
|
|
|
|
|
self.tabs[i].scene.bump_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
|
|
|
|
self.tabs[i].layers.color_picker_row = None;
|
|
|
|
|
|
self.tabs[i].layers.color_full_palette = false;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerLinetypeSet(lt) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-01 17:44:19 +03:00
|
|
|
|
let names = self.selected_layer_names(i);
|
|
|
|
|
|
if !names.is_empty() {
|
|
|
|
|
|
for name in &names {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(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
|
|
|
|
dl.line_type = lt.clone();
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
for pl in self.tabs[i].layers.layers.iter_mut() {
|
|
|
|
|
|
if names.contains(&pl.name) {
|
|
|
|
|
|
pl.linetype = lt.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
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
// Linetype is baked into the cached wires; repaint.
|
|
|
|
|
|
self.tabs[i].scene.bump_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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerLineweightSet(lw) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-01 17:44:19 +03:00
|
|
|
|
let names = self.selected_layer_names(i);
|
|
|
|
|
|
if !names.is_empty() {
|
|
|
|
|
|
for name in &names {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(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
|
|
|
|
dl.line_weight = lw;
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
for pl in self.tabs[i].layers.layers.iter_mut() {
|
|
|
|
|
|
if names.contains(&pl.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
|
|
|
|
pl.lineweight = lw;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-01 17:44:19 +03:00
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
// Lineweight is baked into the cached wires; repaint.
|
|
|
|
|
|
self.tabs[i].scene.bump_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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerTransparencyEdit(idx, s) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-03 16:06:10 +03:00
|
|
|
|
let val = if let Ok(v) = s.parse::<i32>() {
|
|
|
|
|
|
Some(v.clamp(0, 90))
|
|
|
|
|
|
} else if s.is_empty() {
|
|
|
|
|
|
Some(0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
// Apply the edited transparency to every selected layer (#236).
|
|
|
|
|
|
if let Some(v) = val {
|
|
|
|
|
|
let targets = self.layer_row_action_targets(i, idx);
|
|
|
|
|
|
for name in &targets {
|
|
|
|
|
|
if let Some(pl) =
|
|
|
|
|
|
self.tabs[i].layers.layers.iter_mut().find(|l| &l.name == name)
|
|
|
|
|
|
{
|
|
|
|
|
|
pl.transparency = v;
|
|
|
|
|
|
}
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Cursor / viewport messages ─────────────────────────────────
|
|
|
|
|
|
Message::CursorMoved(p) => self.on_cursor_moved(p),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportMove(p) => self.on_viewport_move(p),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportExit => self.on_viewport_exit(),
|
|
|
|
|
|
|
2026-06-27 11:50:11 +03:00
|
|
|
|
// ── Per-pane Model viewport ───────────────────────────────────
|
|
|
|
|
|
Message::PaneResized(ev) => self.on_pane_resized(ev),
|
|
|
|
|
|
Message::PaneClicked(pane) => self.on_pane_clicked(pane),
|
|
|
|
|
|
Message::PaneDragged(ev) => self.on_pane_dragged(ev),
|
|
|
|
|
|
Message::PaneMove(idx, local) => {
|
|
|
|
|
|
let p = self.pane_canvas_point(idx, local);
|
|
|
|
|
|
// While dragging a pane, just track the cursor (no focus swap or
|
|
|
|
|
|
// snap) so the drop target reads cleanly.
|
|
|
|
|
|
if self.pane_move_from.is_some() {
|
|
|
|
|
|
self.tabs[self.active_tab]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.selection
|
|
|
|
|
|
.borrow_mut()
|
|
|
|
|
|
.last_move_pos = Some(p);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.focus_model_pane(idx);
|
|
|
|
|
|
self.on_viewport_move(p)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PaneMoveStart => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.pane_move_from = Some(self.tabs[i].scene.active_model_tile.get());
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PanePress(idx) => {
|
|
|
|
|
|
// A fresh press ends any stale (un-dropped) pane move.
|
|
|
|
|
|
self.pane_move_from = None;
|
|
|
|
|
|
self.focus_model_pane(idx);
|
|
|
|
|
|
self.on_viewport_left_press()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PaneRelease(idx) => {
|
|
|
|
|
|
// Finishing a pane-move drag: swap the source pane with the one
|
|
|
|
|
|
// released over, instead of the normal release handling.
|
|
|
|
|
|
if let Some(from) = self.pane_move_from.take() {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.swap_model_panes(from, idx);
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.focus_model_pane(idx);
|
|
|
|
|
|
self.on_viewport_left_release()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PaneRightPress(idx) => {
|
|
|
|
|
|
self.focus_model_pane(idx);
|
|
|
|
|
|
self.update(Message::ViewportRightPress)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PaneRightRelease(idx) => {
|
|
|
|
|
|
self.focus_model_pane(idx);
|
|
|
|
|
|
self.update(Message::ViewportRightRelease)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PaneMiddlePress(idx) => {
|
|
|
|
|
|
self.focus_model_pane(idx);
|
|
|
|
|
|
self.update(Message::ViewportMiddlePress)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PaneMiddleRelease(idx) => {
|
|
|
|
|
|
self.focus_model_pane(idx);
|
|
|
|
|
|
self.update(Message::ViewportMiddleRelease)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PaneScroll(idx, d) => {
|
|
|
|
|
|
self.focus_model_pane(idx);
|
|
|
|
|
|
self.update(Message::ViewportScroll(d))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
Message::ViewportLeftPress => self.on_viewport_left_press(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportLeftRelease => self.on_viewport_left_release(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportRightPress => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
let Some(p) = sel.last_move_pos else {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
sel.context_menu = None;
|
|
|
|
|
|
sel.right_down = true;
|
|
|
|
|
|
sel.right_press_pos = Some(p);
|
2026-06-27 16:16:08 +03:00
|
|
|
|
sel.right_press_time = Some(iced::time::Instant::now());
|
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.right_last_pos = Some(p);
|
|
|
|
|
|
sel.right_dragging = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportRightRelease => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
2026-06-27 16:16:08 +03:00
|
|
|
|
let Some(click_pos) = sel.last_move_pos else {
|
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-06-27 16:16:08 +03:00
|
|
|
|
if !sel.right_down {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let was_click = !sel.right_dragging;
|
|
|
|
|
|
sel.right_down = false;
|
|
|
|
|
|
sel.right_press_pos = None;
|
|
|
|
|
|
sel.right_press_time = None;
|
|
|
|
|
|
sel.right_last_pos = None;
|
|
|
|
|
|
sel.right_dragging = false;
|
2026-07-01 21:58:04 +03:00
|
|
|
|
sel.orbit_pivot = None;
|
2026-06-27 16:16:08 +03:00
|
|
|
|
if !was_click {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-06-29 11:20:01 +03:00
|
|
|
|
// A command name (or option keyword / value) typed into the
|
|
|
|
|
|
// command line but not yet entered runs on right-click, exactly
|
|
|
|
|
|
// as pressing Enter would. Route through CommandFinalize — the
|
|
|
|
|
|
// canonical Enter action — so the same MText / grip-popup guards
|
|
|
|
|
|
// apply and a non-empty line is forwarded to the submit path.
|
|
|
|
|
|
// Without this the typed text would be swallowed by the context
|
|
|
|
|
|
// menu (when idle) or the Enter cycle. Every other right-click
|
|
|
|
|
|
// behaviour below is unchanged and only applies when the command
|
|
|
|
|
|
// line is empty. Pending text always runs and resets the Enter
|
|
|
|
|
|
// cycle so the next right-click acts as Enter again.
|
|
|
|
|
|
if !self.command_line.input.trim().is_empty() {
|
|
|
|
|
|
sel.right_click_entered = false;
|
|
|
|
|
|
drop(sel);
|
|
|
|
|
|
return self.update(Message::CommandFinalize);
|
|
|
|
|
|
}
|
2026-06-27 16:32:40 +03:00
|
|
|
|
// A right-click (no orbit). While a command is active the first
|
|
|
|
|
|
// right-click acts as Enter (commit / close); a second
|
|
|
|
|
|
// consecutive right-click opens the context menu instead. When
|
|
|
|
|
|
// idle it always opens the menu. (Right-drag, handled above,
|
|
|
|
|
|
// always orbits.) Any other interaction — a left-click pick or a
|
|
|
|
|
|
// new command — resets the cycle so the next right-click is Enter.
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() && !sel.right_click_entered {
|
|
|
|
|
|
sel.right_click_entered = true;
|
2026-06-27 16:16:08 +03:00
|
|
|
|
drop(sel);
|
|
|
|
|
|
return self.update(Message::CommandFinalize);
|
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-06-27 16:32:40 +03:00
|
|
|
|
sel.right_click_entered = false;
|
2026-06-27 16:16:08 +03:00
|
|
|
|
sel.context_menu = Some(click_pos);
|
|
|
|
|
|
sel.draworder_submenu = 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
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportMiddlePress => self.on_viewport_middle_press(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportMiddleRelease => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.middle_down = false;
|
|
|
|
|
|
sel.middle_last_pos = None;
|
2026-07-01 21:58:04 +03:00
|
|
|
|
// End of a Shift+MMB orbit — drop the captured pivot so the next
|
|
|
|
|
|
// gesture recomputes it against the current selection. (#229)
|
|
|
|
|
|
sel.orbit_pivot = 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
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportScroll(delta) => self.on_viewport_scroll(delta),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportClick => self.on_viewport_click(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::WindowResized(w, h) => {
|
|
|
|
|
|
self.vp_size = ((w - 440.0).max(200.0), h);
|
feat(ui): anchor every ribbon dropdown and status-bar popup to its widget, flipping at window edges
All ribbon dropdowns (tool dropdowns, undo/redo history, and the layer /
colour / linetype / lineweight / style combos) and all status-bar popups
(scale, units, OSNAP, isolate, filter, customization menu, layout list)
now open directly at their widget instead of at a layout-estimated
position, and flip to the other side when the panel would cross a window
edge.
Each dropdown/pill widget is wrapped in a transparent PosReport that
records its screen bounds into a thread-local map on draw. The overlays
read those bounds:
- Ribbon: Ribbon::dd_anchor(id, panel_w, win_w) returns
(align_right, h_pad, top); position_ribbon_dropdown places the panel
below the widget, flipping right-aligned near the right edge.
- Status bar: popup builders take (pill_bounds, win_size) and call
popup::position_statusbar_popup / popup_anchor, opening just above the
pill and flipping left/right at the window edge.
The window size is now tracked (win_size, set on WindowResized). The old
layout estimators compute_dropdown_left / compute_layer_combo_left /
compute_prop_combo_left / compute_history_dropdown_left and
osnap_popup_right_offset are deleted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:51:54 +03:00
|
|
|
|
self.win_size = (w, 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
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewCubeSnap(region) => self.on_view_cube_snap(region),
|
2026-06-27 01:12:45 +03:00
|
|
|
|
Message::ViewCubeSnapWorld(region) => self.on_view_cube_snap_world(region),
|
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
|
|
|
|
|
|
|
|
|
|
Message::ViewCubeHome => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let r_ucs = self.tabs[i].scene.viewcube_ucs_mat();
|
|
|
|
|
|
if self.tabs[i].scene.active_viewport.is_some() {
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.mutate_active_viewport_camera(|c| c.home_view(r_ucs));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].scene.camera.borrow_mut().home_view(r_ucs);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
self.command_line.push_output("View: Home");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewCubeRoll(cw) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let ang = if cw {
|
|
|
|
|
|
std::f32::consts::FRAC_PI_2
|
|
|
|
|
|
} else {
|
|
|
|
|
|
-std::f32::consts::FRAC_PI_2
|
|
|
|
|
|
};
|
|
|
|
|
|
if self.tabs[i].scene.active_viewport.is_some() {
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.mutate_active_viewport_camera(|c| c.roll_by(ang));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].scene.camera.borrow_mut().roll_by(ang);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewCubeNudge(dir) => {
|
|
|
|
|
|
use crate::scene::NudgeDir;
|
|
|
|
|
|
let (horizontal, positive) = match dir {
|
|
|
|
|
|
NudgeDir::Up => (false, false),
|
|
|
|
|
|
NudgeDir::Down => (false, true),
|
|
|
|
|
|
NudgeDir::Left => (true, false),
|
|
|
|
|
|
NudgeDir::Right => (true, true),
|
|
|
|
|
|
};
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.active_viewport.is_some() {
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.mutate_active_viewport_camera(|c| c.nudge_90(horizontal, positive));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.camera
|
|
|
|
|
|
.borrow_mut()
|
|
|
|
|
|
.nudge_90(horizontal, positive);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SetViewcubeUcs(name) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if name.is_empty() || name == "WCS" {
|
|
|
|
|
|
self.tabs[i].active_ucs = None;
|
|
|
|
|
|
self.command_line.push_output("UCS: World");
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
} else if let Some(named) = self.tabs[i].scene.document.ucss.get(&name).cloned() {
|
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].active_ucs = Some(named);
|
|
|
|
|
|
self.command_line.push_output(&format!("UCS: {}", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].sync_ucs_to_scene();
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::GripDwellTick => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// Reuse the move-time logic — `p` is the last cursor
|
|
|
|
|
|
// position the viewport saw, which is also what the
|
|
|
|
|
|
// hover state was last set with.
|
|
|
|
|
|
let p = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.selection
|
|
|
|
|
|
.borrow()
|
|
|
|
|
|
.last_move_pos
|
|
|
|
|
|
.unwrap_or(self.cursor_pos);
|
|
|
|
|
|
self.update_grip_hover(i, p);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::HoverDwellTick => self.on_hover_dwell_tick(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::VisibilityPick(idx) => {
|
|
|
|
|
|
if let Some(popup) = self.visibility_popup.take() {
|
|
|
|
|
|
self.apply_visibility_state(popup.insert_handle, idx);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::GripMenuPick(idx) => self.on_grip_menu_pick(idx),
|
|
|
|
|
|
|
|
|
|
|
|
// ── Snap / mode toggles ───────────────────────────────────────
|
|
|
|
|
|
Message::ToggleSnapEnabled => {
|
|
|
|
|
|
self.snapper.toggle_global();
|
|
|
|
|
|
self.sync_vport_display(self.active_tab);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleGridSnap => {
|
|
|
|
|
|
self.snapper.toggle_grid_snap();
|
|
|
|
|
|
self.sync_vport_display(self.active_tab);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleGrid => {
|
|
|
|
|
|
self.show_grid ^= true;
|
|
|
|
|
|
self.sync_vport_display(self.active_tab);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleOrtho => {
|
|
|
|
|
|
self.ortho_mode ^= true;
|
|
|
|
|
|
if self.ortho_mode {
|
|
|
|
|
|
self.polar_mode = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// If the user manually toggles ortho during a command that
|
|
|
|
|
|
// suppressed it (e.g. RECTANG), the toggle is permanent —
|
|
|
|
|
|
// don't restore the pre-command state when the command ends.
|
|
|
|
|
|
self.rect_suppressed_ortho = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleLineweightDisplay => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if i < self.tabs.len() {
|
|
|
|
|
|
let h = &mut self.tabs[i].scene.document.header;
|
|
|
|
|
|
h.lineweight_display = !h.lineweight_display;
|
|
|
|
|
|
// No retessellate — the wire shader reads the flag from uniforms.
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-10 15:54:57 +03:00
|
|
|
|
Message::CycleCoordsMode => {
|
|
|
|
|
|
// $COORDS 0 (static) → 1 (live absolute) → 2 (polar) → 0.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if i < self.tabs.len() {
|
|
|
|
|
|
let mode = {
|
|
|
|
|
|
let h = &mut self.tabs[i].scene.document.header;
|
|
|
|
|
|
h.coords_mode = (h.coords_mode + 1).rem_euclid(3);
|
|
|
|
|
|
h.coords_mode
|
|
|
|
|
|
};
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
let label = match mode {
|
|
|
|
|
|
0 => "static",
|
|
|
|
|
|
2 => "polar",
|
|
|
|
|
|
_ => "live",
|
|
|
|
|
|
};
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("COORDS = {mode} ({label})"));
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
Message::TogglePolar => {
|
|
|
|
|
|
self.polar_mode ^= true;
|
|
|
|
|
|
if self.polar_mode {
|
|
|
|
|
|
self.ortho_mode = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleDynInput => {
|
|
|
|
|
|
self.dyn_input ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleViewCube => {
|
|
|
|
|
|
self.show_viewcube ^= true;
|
2026-07-11 00:40:22 +01:00
|
|
|
|
self.ribbon.set_viewcube(self.show_viewcube);
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleProperties => {
|
|
|
|
|
|
self.show_properties ^= true;
|
2026-07-12 00:56:15 +01:00
|
|
|
|
self.ribbon.set_properties(self.show_properties);
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleFileTabs => {
|
|
|
|
|
|
self.show_file_tabs ^= true;
|
2026-07-12 00:56:15 +01:00
|
|
|
|
self.ribbon.set_file_tabs(self.show_file_tabs);
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleLayoutTabs => {
|
|
|
|
|
|
self.show_layout_tabs ^= true;
|
2026-07-12 00:56:15 +01:00
|
|
|
|
self.ribbon.set_layout_tabs(self.show_layout_tabs);
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleOTrack => {
|
|
|
|
|
|
self.snapper.otrack_enabled ^= true;
|
|
|
|
|
|
if !self.snapper.otrack_enabled {
|
|
|
|
|
|
self.snapper.clear_tracking();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SetPolarAngle(deg) => {
|
|
|
|
|
|
self.polar_increment_deg = deg;
|
|
|
|
|
|
self.polar_mode = true;
|
|
|
|
|
|
self.ortho_mode = false;
|
2026-07-09 21:35:47 +03:00
|
|
|
|
self.polar_popup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TogglePolarPopup => {
|
|
|
|
|
|
self.polar_popup_open ^= true;
|
|
|
|
|
|
if self.polar_popup_open {
|
|
|
|
|
|
// Start the custom field empty each time; picking a preset or
|
|
|
|
|
|
// typing a value is what actually enables polar tracking.
|
|
|
|
|
|
self.polar_custom_input.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ClosePolarPopup => {
|
|
|
|
|
|
self.polar_popup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PolarCustomInput(s) => {
|
|
|
|
|
|
self.polar_custom_input = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SubmitPolarCustom => {
|
|
|
|
|
|
// Accept any positive angle up to a full turn; ignore garbage.
|
|
|
|
|
|
if let Ok(v) = self.polar_custom_input.trim().parse::<f32>() {
|
|
|
|
|
|
if v > 0.0 && v <= 360.0 {
|
|
|
|
|
|
self.polar_increment_deg = v;
|
|
|
|
|
|
self.polar_mode = true;
|
|
|
|
|
|
self.ortho_mode = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.polar_custom_input.clear();
|
|
|
|
|
|
self.polar_popup_open = 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
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SetAnnotationScale(scale) => {
|
|
|
|
|
|
self.scale_popup_open = false;
|
|
|
|
|
|
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
|
|
|
|
|
tab.scene.annotation_scale = scale;
|
|
|
|
|
|
tab.scene.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SetViewportScale(scale) => {
|
|
|
|
|
|
self.scale_popup_open = false;
|
|
|
|
|
|
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
|
|
|
|
|
tab.scene.set_viewport_scale(scale);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleScalePopup => {
|
|
|
|
|
|
self.scale_popup_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CloseScalePopup => {
|
|
|
|
|
|
self.scale_popup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-13 10:18:05 +03:00
|
|
|
|
Message::ScaleListDelete(name) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let current = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.header
|
|
|
|
|
|
.current_annotation_scale
|
|
|
|
|
|
.clone();
|
|
|
|
|
|
// Keep the popup open so several can be removed in a row; never
|
|
|
|
|
|
// remove the current annotation scale out from under the drawing.
|
|
|
|
|
|
if !name.eq_ignore_ascii_case(¤t) {
|
|
|
|
|
|
self.push_undo_snapshot(i, "SCALELISTEDIT");
|
|
|
|
|
|
if self.tabs[i].scene.remove_scale(&name) {
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ScaleListAddPrompt => {
|
|
|
|
|
|
// Close the popup and prime the command line; the user types the
|
|
|
|
|
|
// ratio (e.g. 1:50) and commits it via SCALELISTEDIT ADD.
|
|
|
|
|
|
self.scale_popup_open = false;
|
|
|
|
|
|
self.command_line.input = "SCALELISTEDIT ADD ".to_string();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-06-27 14:24:47 +03:00
|
|
|
|
Message::ToggleLayoutList => {
|
|
|
|
|
|
self.layout_list_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CloseLayoutList => {
|
|
|
|
|
|
self.layout_list_open = false;
|
|
|
|
|
|
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
|
|
|
|
Message::ToggleStatusBarMenu => {
|
|
|
|
|
|
self.statusbar_menu_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CloseStatusBarMenu => {
|
|
|
|
|
|
self.statusbar_menu_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleStatusPill(pill) => {
|
|
|
|
|
|
// Keep the menu open so several pills can be toggled in a row.
|
|
|
|
|
|
self.statusbar_config.toggle(pill);
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
self.save_config();
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleCleanScreen => {
|
|
|
|
|
|
self.clean_screen ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleTransparencyDisplay => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if i < self.tabs.len() {
|
|
|
|
|
|
// No retessellate — the wire shader reads the flag from uniforms.
|
|
|
|
|
|
self.tabs[i].scene.transparency_display ^= true;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleQuickProperties => {
|
|
|
|
|
|
self.quick_properties ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleSelectionCycling => {
|
|
|
|
|
|
self.selection_cycling ^= true;
|
|
|
|
|
|
self.cycle_candidates = None;
|
|
|
|
|
|
self.tabs[self.active_tab].scene.set_hover_highlight(None);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CycleSelect(handle) => {
|
|
|
|
|
|
// Add the picked object to the current selection (accumulate).
|
|
|
|
|
|
self.cycle_candidates = None;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.set_hover_highlight(None);
|
|
|
|
|
|
self.tabs[i].scene.select_entity(handle, false);
|
|
|
|
|
|
self.tabs[i].scene.expand_selection_for_groups(&[handle]);
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CycleHover(handle) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.set_hover_highlight(handle);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CycleHoverExit(handle) => {
|
|
|
|
|
|
// Only clear if another row hasn't already taken the highlight;
|
|
|
|
|
|
// enter/exit can fire out of order when moving between rows.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.hover_highlight == Some(handle) {
|
|
|
|
|
|
self.tabs[i].scene.set_hover_highlight(None);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CycleCancel => {
|
|
|
|
|
|
self.cycle_candidates = None;
|
|
|
|
|
|
self.tabs[self.active_tab].scene.set_hover_highlight(None);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleSelectionFilterPopup => {
|
|
|
|
|
|
self.selection_filter_popup_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CloseSelectionFilterPopup => {
|
|
|
|
|
|
self.selection_filter_popup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleSelectionFilterType(name) => {
|
|
|
|
|
|
let f = &mut self.tabs[self.active_tab].scene.selection_filter;
|
|
|
|
|
|
if !f.remove(&name) {
|
|
|
|
|
|
f.insert(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-01 09:47:13 +03:00
|
|
|
|
Message::SelectionFilterSelectAll => {
|
|
|
|
|
|
self.tabs[self.active_tab].scene.selection_filter.clear();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SelectionFilterClearAll => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let types = self.tabs[i].scene.entity_type_names_in_layout();
|
|
|
|
|
|
let f = &mut self.tabs[i].scene.selection_filter;
|
|
|
|
|
|
for t in types {
|
|
|
|
|
|
f.insert(t.to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
Message::ToggleUnitsPopup => {
|
|
|
|
|
|
self.units_popup_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CloseUnitsPopup => {
|
|
|
|
|
|
self.units_popup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SetDrawingUnits(code) => {
|
|
|
|
|
|
self.units_popup_open = false;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.document.header.insertion_units = code;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleIsolatePopup => {
|
|
|
|
|
|
self.isolate_popup_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CloseIsolatePopup => {
|
|
|
|
|
|
self.isolate_popup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleSnap(t) => {
|
|
|
|
|
|
self.snapper.toggle(t);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleSnapPopup => {
|
|
|
|
|
|
self.snap_popup_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CloseSnapPopup => {
|
|
|
|
|
|
self.snap_popup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SnapSelectAll => {
|
|
|
|
|
|
self.snapper.enable_all();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SnapClearAll => {
|
|
|
|
|
|
self.snapper.disable_all();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Ribbon dropdowns ──────────────────────────────────────────
|
|
|
|
|
|
Message::ToggleRibbonDropdown(id) => {
|
|
|
|
|
|
self.ribbon.toggle_dropdown(&id);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-05 18:04:55 +03:00
|
|
|
|
Message::ToggleRibbonPanel(id) => {
|
|
|
|
|
|
self.ribbon.toggle_collapsed_panel(&id);
|
|
|
|
|
|
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
|
|
|
|
Message::CloseRibbonDropdown => {
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DropdownSelectItem { dropdown_id, cmd } => {
|
|
|
|
|
|
self.ribbon.select_dropdown_item(dropdown_id, cmd);
|
|
|
|
|
|
self.ribbon.activate_tool(cmd);
|
|
|
|
|
|
self.dispatch_command(cmd)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::DeleteSelected => {
|
|
|
|
|
|
// In the MText preview, Delete removes text at the caret.
|
|
|
|
|
|
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
|
|
|
|
|
self.mtext_delete();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "ERASE");
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
// Stash the erased entities so OOPS can restore them.
|
|
|
|
|
|
self.oops_cache = handles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
|
|
|
|
|
.collect();
|
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].scene.erase_entities(&handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:44:19 +03:00
|
|
|
|
Message::SetModifiers { shift, ctrl } => {
|
2026-07-08 20:39:40 +03:00
|
|
|
|
let ctrl_changed = self.ctrl_down != ctrl;
|
2026-07-01 17:44:19 +03:00
|
|
|
|
self.shift_down = shift;
|
|
|
|
|
|
self.ctrl_down = ctrl;
|
2026-07-08 20:39:40 +03:00
|
|
|
|
// A live command may key its preview off Ctrl (arc-direction
|
|
|
|
|
|
// flip). Rebuild it at the current cursor so the flip shows
|
|
|
|
|
|
// without waiting for the next mouse move.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if ctrl_changed && self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
let p = self.tabs[i].last_cursor_screen;
|
|
|
|
|
|
return Task::done(Message::ViewportMove(p));
|
|
|
|
|
|
}
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── In-place MText editor ───────────────────────────────────
|
|
|
|
|
|
Message::MTextEdit(action) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.content.perform(action);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextFmt(kind) => {
|
|
|
|
|
|
self.mtext_apply_fmt(kind);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextHeight(s) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.height = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
Message::MTextColorChanged(color) => {
|
|
|
|
|
|
self.mtext_apply_color(color);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextColorPickerToggle => {
|
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(ed) = self.mtext_editor.as_mut() {
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
ed.color_picker_open = !ed.color_picker_open;
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextStyle(s) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.style = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextFont(f) => {
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
self.mtext_apply_font(&f);
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextOblique(s) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.oblique = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextWidth(s) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.width = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextCharSpace(s) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.char_space = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextJustify(ap) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.attachment = ap;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextAlign(a) => {
|
|
|
|
|
|
self.mtext_apply_align(a);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextLineSpacing(f) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.line_spacing = f;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextShowPreview(on) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.show_preview = on;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
// Focus the text area when switching to Edit so the caret
|
|
|
|
|
|
// shows and typing/clicking edits immediately.
|
|
|
|
|
|
if on {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::operation::focus(iced::widget::Id::new(
|
|
|
|
|
|
super::view::MTEXT_TEXT_ID,
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextSelStart(off) => {
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
// Count quick same-spot clicks: 1 = place caret, 2 = select the
|
|
|
|
|
|
// word, 3 = select all.
|
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
|
let count = match self.mtext_click_time {
|
|
|
|
|
|
Some(t)
|
|
|
|
|
|
if now.duration_since(t).as_millis() < 400
|
|
|
|
|
|
&& off.abs_diff(self.mtext_click_off) <= 1 =>
|
|
|
|
|
|
{
|
|
|
|
|
|
(self.mtext_click_count + 1).min(3)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
self.mtext_click_time = Some(now);
|
|
|
|
|
|
self.mtext_click_off = off;
|
|
|
|
|
|
self.mtext_click_count = count;
|
|
|
|
|
|
match count {
|
|
|
|
|
|
2 => self.mtext_select_word(off),
|
|
|
|
|
|
3 => self.mtext_select_all(),
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.sel_anchor = off;
|
|
|
|
|
|
ed.sel = Some((off, off));
|
|
|
|
|
|
ed.caret = off;
|
|
|
|
|
|
ed.caret_blink_on = 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
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextSelTo(off) => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
let a = ed.sel_anchor;
|
|
|
|
|
|
ed.sel = Some((a.min(off), a.max(off)));
|
|
|
|
|
|
ed.caret = off;
|
|
|
|
|
|
ed.caret_blink_on = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextCaretMove(d) => {
|
|
|
|
|
|
self.mtext_caret_move(d);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextCaretBlink => {
|
|
|
|
|
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
|
|
|
|
|
ed.caret_blink_on = !ed.caret_blink_on;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextOk => {
|
|
|
|
|
|
let committed = self.mtext_commit();
|
|
|
|
|
|
self.post_editor_closed(committed)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MTextCancel => {
|
|
|
|
|
|
self.mtext_cancel();
|
|
|
|
|
|
self.post_editor_closed(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TextInlineInput(s) => {
|
|
|
|
|
|
if let Some(ed) = self.text_inline.as_mut() {
|
|
|
|
|
|
ed.value = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ctrl+V. The MText editor and (on the web) the TEXT editor read the
|
|
|
|
|
|
// system clipboard asynchronously — the only paste path that works
|
|
|
|
|
|
// in the browser, where the synchronous clipboard the iced
|
|
|
|
|
|
// text_input expects is empty. With no editor open it falls through
|
|
|
|
|
|
// to the entity paste command.
|
|
|
|
|
|
Message::PasteShortcut => self.on_paste_shortcut(),
|
2026-07-03 16:06:10 +03:00
|
|
|
|
|
|
|
|
|
|
Message::SelectAllShortcut => {
|
|
|
|
|
|
let i = self.active_tab;
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
|
|
|
|
|
// Ctrl+A in the MText editor selects all of its text.
|
|
|
|
|
|
self.mtext_select_all();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
} else if self.active_modal == Some(super::ModalKind::Layers) {
|
2026-07-03 16:06:10 +03:00
|
|
|
|
// Select every row in the Layer Manager (#236).
|
|
|
|
|
|
let n = self.tabs[i].layers.layers.len();
|
|
|
|
|
|
self.tabs[i].layers.selected_multi = (0..n).collect();
|
|
|
|
|
|
self.tabs[i].layers.selected = (n > 0).then_some(0);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.dispatch_command("SELECTALL")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
Message::MTextPasteClip(text) => {
|
|
|
|
|
|
if let Some(text) = text.filter(|t| !t.is_empty()) {
|
|
|
|
|
|
// CR/LF arrive as line breaks; MText keeps "\n", drop "\r".
|
|
|
|
|
|
self.mtext_type(&text.replace('\r', ""));
|
|
|
|
|
|
self.rebuild_mtext_preview();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextInlinePasteClip(text) => {
|
|
|
|
|
|
if let Some(text) = text.filter(|t| !t.is_empty()) {
|
|
|
|
|
|
// Single-line field: collapse newlines, append at the end.
|
|
|
|
|
|
let flat = text.replace(['\r', '\n'], " ");
|
|
|
|
|
|
if let Some(ed) = self.text_inline.as_mut() {
|
|
|
|
|
|
ed.value.push_str(&flat);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextInlineOk => {
|
|
|
|
|
|
let committed = self.text_inline_commit();
|
|
|
|
|
|
self.post_editor_closed(committed)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::DrawOrderSubmenuToggle => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.draworder_submenu = !sel.draworder_submenu;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::DrawOrderPickRef(above) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
|
|
|
|
|
let to_move: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
|
|
|
|
|
|
if to_move.is_empty() {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("DRAWORDER: select entities first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::command::CadCommand;
|
|
|
|
|
|
let cmd = super::commands::DrawOrderRefCommand::new(to_move, above);
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SelectSimilar => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
|
|
|
|
|
let added = self.tabs[i].scene.select_similar();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Select Similar: {} added.", added));
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::InvertSelection => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
|
|
|
|
|
let count = self.tabs[i].scene.invert_selection();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Invert Selection: {} object(s) selected.", count));
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::QSelectOpen => self.on_qselect_open(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::QSelectClose => {
|
|
|
|
|
|
self.qselect = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::QSelectSetType(t) => {
|
|
|
|
|
|
if let Some(state) = self.qselect.as_mut() {
|
|
|
|
|
|
// Drop the property when it no longer applies to the
|
|
|
|
|
|
// chosen type: type-specific fields like `start_x`
|
|
|
|
|
|
// would otherwise stay selected but never match.
|
|
|
|
|
|
let kept_property = state.property.clone().and_then(|p| {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let props = self.tabs[i].scene.qselect_properties(t.as_deref());
|
|
|
|
|
|
if props.iter().any(|(f, _)| f == &p.field) {
|
|
|
|
|
|
Some(p)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
state.type_filter = t;
|
|
|
|
|
|
state.property = kept_property;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::QSelectSetProperty(p) => {
|
|
|
|
|
|
if let Some(state) = self.qselect.as_mut() {
|
|
|
|
|
|
state.property = p;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::QSelectSetOperator(op) => {
|
|
|
|
|
|
if let Some(state) = self.qselect.as_mut() {
|
|
|
|
|
|
state.operator = op;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::QSelectSetValue(v) => {
|
|
|
|
|
|
if let Some(state) = self.qselect.as_mut() {
|
|
|
|
|
|
state.value = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::QSelectSetAppend(b) => {
|
|
|
|
|
|
if let Some(state) = self.qselect.as_mut() {
|
|
|
|
|
|
state.append = b;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::QSelectApply => {
|
|
|
|
|
|
let Some(state) = self.qselect.take() else {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let matched = self.tabs[i].scene.qselect(
|
|
|
|
|
|
state.type_filter.as_deref(),
|
|
|
|
|
|
state.property.as_ref().map(|p| p.field.as_str()),
|
|
|
|
|
|
state.operator,
|
|
|
|
|
|
&state.value,
|
|
|
|
|
|
state.append,
|
|
|
|
|
|
);
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("QSELECT: {} object(s) selected.", matched));
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Properties panel messages ─────────────────────────────────
|
|
|
|
|
|
Message::PropSelectionGroupChanged(group) => {
|
|
|
|
|
|
self.tabs[self.active_tab].properties.selected_group = Some(group);
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RibbonLayerChanged(layer) => self.on_ribbon_layer_changed(layer),
|
|
|
|
|
|
|
|
|
|
|
|
Message::RibbonColorChanged(color) => self.on_ribbon_color_changed(color),
|
|
|
|
|
|
Message::RibbonColorPaletteToggle => {
|
|
|
|
|
|
self.ribbon.prop_color_palette_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::RibbonLinetypeChanged(lt) => self.on_ribbon_linetype_changed(lt),
|
|
|
|
|
|
Message::RibbonLineweightChanged(lw) => {
|
|
|
|
|
|
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 (CELWEIGHT). #21.
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_line_weight = lw.value();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.ribbon.active_lineweight = lw;
|
|
|
|
|
|
} 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_line_weight(entity, lw);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-01 19:14:35 +03:00
|
|
|
|
// Lineweight is 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_lineweight = lw;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RibbonStyleChanged { key, name } => self.on_ribbon_style_changed(key, name),
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropLayerChanged(layer) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
crate::scene::view::dispatch::apply_common_prop(
|
|
|
|
|
|
entity, "layer", &layer,
|
|
|
|
|
|
);
|
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.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropColorChanged(color) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
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].properties.color_picker_open = false;
|
|
|
|
|
|
self.tabs[i].properties.color_palette_open = false;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropLwChanged(lw) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
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_line_weight(entity, lw);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropLinetypeChanged(lt) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
crate::scene::view::dispatch::apply_common_prop(
|
|
|
|
|
|
entity, "linetype", <,
|
|
|
|
|
|
);
|
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.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropHatchPatternChanged(name) => self.on_prop_hatch_pattern_changed(name),
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropBoolToggle(field) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for &handle in &handles {
|
2026-07-13 09:32:22 +03:00
|
|
|
|
match field {
|
|
|
|
|
|
// Per-object annotative flag (MTEXT / MULTILEADER): a
|
|
|
|
|
|
// doc-aware toggle so turning it off also removes the
|
|
|
|
|
|
// per-object annotation context, not just the flag.
|
|
|
|
|
|
"is_annotative" | "enable_annotation_scale" => {
|
|
|
|
|
|
let cur = match self.tabs[i].scene.document.get_entity(handle) {
|
|
|
|
|
|
Some(acadrust::EntityType::MText(t)) => t.is_annotative,
|
|
|
|
|
|
Some(acadrust::EntityType::MultiLeader(m)) => {
|
|
|
|
|
|
m.enable_annotation_scale
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => continue,
|
|
|
|
|
|
};
|
|
|
|
|
|
crate::scene::annotative::set_entity_annotative(
|
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
!cur,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
"invisible" => {
|
|
|
|
|
|
if let Some(entity) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
crate::scene::view::dispatch::toggle_invisible(entity);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
if let Some(entity) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
crate::scene::view::dispatch::apply_geom_prop(
|
|
|
|
|
|
entity, field, "toggle",
|
|
|
|
|
|
);
|
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.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 17:53:05 +03:00
|
|
|
|
Message::PropVertexStep(delta) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
// Vertex navigation applies to a single selected polyline.
|
|
|
|
|
|
let n = if handles.len() == 1 {
|
|
|
|
|
|
match self.tabs[i].scene.document.get_entity(handles[0]) {
|
|
|
|
|
|
Some(acadrust::EntityType::LwPolyline(p)) => p.vertices.len(),
|
|
|
|
|
|
Some(acadrust::EntityType::Polyline2D(p)) => p.vertices.len(),
|
|
|
|
|
|
_ => 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
0
|
|
|
|
|
|
};
|
|
|
|
|
|
if n > 0 {
|
|
|
|
|
|
let cur = self.tabs[i].properties.prop_vertex.min(n - 1) as i64;
|
|
|
|
|
|
// Wrap around so ◀ from the first vertex lands on the last.
|
|
|
|
|
|
let next = (cur + delta as i64).rem_euclid(n as i64) as usize;
|
|
|
|
|
|
self.tabs[i].properties.prop_vertex = next;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
Message::PropGeomChoiceChanged { field, value } => {
|
|
|
|
|
|
self.on_prop_geom_choice_changed(field, value)
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
Message::PropGeomInput { field, value } => {
|
|
|
|
|
|
self.tabs[self.active_tab]
|
|
|
|
|
|
.properties
|
|
|
|
|
|
.edit_buf
|
|
|
|
|
|
.insert(field.to_string(), value);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropGeomCommit(field) => self.on_prop_geom_commit(field),
|
|
|
|
|
|
|
2026-07-01 07:42:50 +03:00
|
|
|
|
Message::PropAttrInput { tag, value } => {
|
|
|
|
|
|
self.tabs[self.active_tab]
|
|
|
|
|
|
.properties
|
|
|
|
|
|
.edit_buf
|
|
|
|
|
|
.insert(crate::ui::properties::attr_edit_key(&tag), value);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropAttrCommit(tag) => self.on_prop_attr_commit(tag),
|
|
|
|
|
|
|
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
|
|
|
|
Message::PropColorPickerToggle => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].properties.color_picker_open =
|
|
|
|
|
|
!self.tabs[i].properties.color_picker_open;
|
|
|
|
|
|
if self.tabs[i].properties.color_picker_open {
|
|
|
|
|
|
self.tabs[i].properties.color_palette_open = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 17:41:16 +03:00
|
|
|
|
Message::PropBgColorPickerToggle => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].properties.bg_color_picker_open =
|
|
|
|
|
|
!self.tabs[i].properties.bg_color_picker_open;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropBgColorChanged(color) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for &handle in &handles {
|
2026-07-08 23:35:27 +03:00
|
|
|
|
match self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
Some(acadrust::EntityType::MText(m)) => {
|
|
|
|
|
|
m.background_color = color.clone();
|
|
|
|
|
|
// Picking a colour turns the background on in Fill
|
|
|
|
|
|
// mode (specific colour), preserving the frame bit.
|
|
|
|
|
|
m.background_fill_flags =
|
|
|
|
|
|
(m.background_fill_flags & !0x02) | 0x01;
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(acadrust::EntityType::Hatch(h)) => {
|
|
|
|
|
|
crate::entities::hatch::set_background_color(h, &color);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
|
|
|
|
|
self.tabs[i].properties.bg_color_picker_open = false;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropColorFieldToggle(field) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let p = &mut self.tabs[i].properties;
|
|
|
|
|
|
p.open_color_field = if p.open_color_field.as_deref() == Some(field.as_str()) {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(field)
|
|
|
|
|
|
};
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropColorFieldChanged { field, color } => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
let idx = if field == "gradient_color_2" { 1 } else { 0 };
|
|
|
|
|
|
for &handle in &handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Hatch(h)) =
|
2026-07-07 17:41:16 +03:00
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
2026-07-08 23:35:27 +03:00
|
|
|
|
while h.gradient_color.colors.len() <= idx {
|
|
|
|
|
|
let value = if h.gradient_color.colors.is_empty() {
|
|
|
|
|
|
0.0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
};
|
|
|
|
|
|
h.gradient_color.colors.push(
|
|
|
|
|
|
acadrust::entities::hatch::GradientColorEntry {
|
|
|
|
|
|
value,
|
|
|
|
|
|
color: acadrust::types::Color::Index(7),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
h.gradient_color.colors[idx].color = color.clone();
|
2026-07-07 17:41:16 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-08 23:35:27 +03:00
|
|
|
|
// Rebuild hatch seeds so the gradient fill picks up the new
|
|
|
|
|
|
// colour (synced_hatch_models only patches the main colour).
|
|
|
|
|
|
self.tabs[i].scene.populate_hatches_from_document();
|
2026-07-07 17:41:16 +03:00
|
|
|
|
self.invalidate_property_targets(i, &handles);
|
2026-07-08 23:35:27 +03:00
|
|
|
|
self.tabs[i].properties.open_color_field = None;
|
2026-07-07 17:41:16 +03:00
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 19:47:32 +03:00
|
|
|
|
Message::PropColorPickerClose => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].properties.color_picker_open = false;
|
|
|
|
|
|
self.tabs[i].properties.color_palette_open = false;
|
|
|
|
|
|
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
|
|
|
|
Message::PropColorPaletteToggle => {
|
|
|
|
|
|
self.tabs[self.active_tab].properties.color_palette_open =
|
|
|
|
|
|
!self.tabs[self.active_tab].properties.color_palette_open;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-27 14:24:47 +03:00
|
|
|
|
Message::LayoutSwitch(name) => {
|
|
|
|
|
|
self.layout_list_open = false;
|
|
|
|
|
|
self.on_layout_switch(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
|
|
|
|
|
|
|
|
|
|
Message::LayoutCreate => self.on_layout_create(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutDelete(name) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT DEL");
|
|
|
|
|
|
if self.tabs[i].scene.delete_layout(&name) {
|
|
|
|
|
|
self.layout_context_menu = None;
|
|
|
|
|
|
self.layout_rename_state = None;
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Layout \"{name}\" silindi"));
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutRenameStart(name) => {
|
|
|
|
|
|
if name != "Model" {
|
|
|
|
|
|
self.layout_rename_state = Some((name.clone(), name));
|
|
|
|
|
|
self.layout_context_menu = None;
|
|
|
|
|
|
// Focus the inline field so the user types into it
|
|
|
|
|
|
// directly instead of the command line (issue #86).
|
|
|
|
|
|
return iced::widget::operation::focus(iced::widget::Id::new(
|
|
|
|
|
|
crate::ui::statusbar::LAYOUT_RENAME_INPUT_ID,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutRenameEdit(val) => {
|
|
|
|
|
|
if let Some((orig, _)) = &self.layout_rename_state {
|
|
|
|
|
|
let orig = orig.clone();
|
|
|
|
|
|
self.layout_rename_state = Some((orig, val));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutRenameCommit => self.on_layout_rename_commit(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutRenameCancel => {
|
|
|
|
|
|
self.layout_rename_state = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutContextMenu(name) => {
|
|
|
|
|
|
if name != "Model" {
|
|
|
|
|
|
self.layout_context_menu = Some(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutContextMenuClose => {
|
|
|
|
|
|
self.layout_context_menu = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Layout Manager Panel ──────────────────────────────────────────
|
|
|
|
|
|
Message::LayoutManagerOpen => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let current = self.tabs[i].scene.current_layout.clone();
|
|
|
|
|
|
self.layout_manager_selected = current.clone();
|
|
|
|
|
|
self.layout_manager_rename_buf = if current == "Model" {
|
|
|
|
|
|
String::new()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
current
|
|
|
|
|
|
};
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::LayoutManager);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerSelect(name) => {
|
|
|
|
|
|
self.layout_manager_rename_buf = if name == "Model" {
|
|
|
|
|
|
String::new()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
name.clone()
|
|
|
|
|
|
};
|
|
|
|
|
|
self.layout_manager_selected = name;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerRenameBuf(s) => {
|
|
|
|
|
|
self.layout_manager_rename_buf = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerRenameCommit => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let old_name = self.layout_manager_selected.clone();
|
|
|
|
|
|
let new_name = self.layout_manager_rename_buf.trim().to_string();
|
|
|
|
|
|
if old_name == "Model" {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Cannot rename the Model layout.");
|
|
|
|
|
|
} else if new_name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Layout name cannot be empty.");
|
|
|
|
|
|
} else if new_name == old_name {
|
|
|
|
|
|
// no-op
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT RENAME");
|
|
|
|
|
|
self.tabs[i].scene.rename_layout(&old_name, &new_name);
|
|
|
|
|
|
if self.tabs[i].scene.current_layout == old_name {
|
2026-07-12 01:01:40 +03:00
|
|
|
|
self.tabs[i].scene.set_current_layout(new_name.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
|
|
|
|
}
|
|
|
|
|
|
self.layout_manager_selected = new_name.clone();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Layout renamed: '{old_name}' → '{new_name}'"));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerNew => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let existing = self.tabs[i].scene.layout_names();
|
|
|
|
|
|
let n = (1usize..)
|
|
|
|
|
|
.find(|n| !existing.contains(&format!("Layout{n}")))
|
|
|
|
|
|
.unwrap_or(1);
|
|
|
|
|
|
let name = format!("Layout{n}");
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT NEW");
|
|
|
|
|
|
match self.tabs[i].scene.document.add_layout(&name) {
|
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.layout_manager_selected = name.clone();
|
|
|
|
|
|
self.layout_manager_rename_buf = name.clone();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Layout '{name}' created."));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("LAYOUT: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerDelete => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.layout_manager_selected.clone();
|
|
|
|
|
|
if name == "Model" {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Cannot delete the Model layout.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT DELETE");
|
|
|
|
|
|
self.tabs[i].scene.delete_layout(&name);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
// Switch to Model if active layout was deleted.
|
|
|
|
|
|
if self.tabs[i].scene.current_layout == name {
|
2026-07-12 01:01:40 +03:00
|
|
|
|
self.tabs[i].scene.set_current_layout("Model".to_string());
|
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.layout_manager_selected = "Model".to_string();
|
|
|
|
|
|
self.layout_manager_rename_buf = String::new();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Layout '{name}' deleted."));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerMoveLeft => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.layout_manager_selected.clone();
|
|
|
|
|
|
if name == "Model" {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let names = self.tabs[i].scene.layout_names();
|
|
|
|
|
|
// Find position among paper layouts only.
|
|
|
|
|
|
let paper: Vec<&str> = names.iter().skip(1).map(|s| s.as_str()).collect();
|
|
|
|
|
|
if let Some(pos) = paper.iter().position(|&n| n == name) {
|
|
|
|
|
|
if pos > 0 {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT REORDER");
|
|
|
|
|
|
self.tabs[i].scene.swap_layout_order(&name, paper[pos - 1]);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerMoveRight => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.layout_manager_selected.clone();
|
|
|
|
|
|
if name == "Model" {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let names = self.tabs[i].scene.layout_names();
|
|
|
|
|
|
let paper: Vec<&str> = names.iter().skip(1).map(|s| s.as_str()).collect();
|
|
|
|
|
|
if let Some(pos) = paper.iter().position(|&n| n == name) {
|
|
|
|
|
|
if pos + 1 < paper.len() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT REORDER");
|
|
|
|
|
|
self.tabs[i].scene.swap_layout_order(&name, paper[pos + 1]);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::LayoutManagerSetCurrent => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.layout_manager_selected.clone();
|
2026-07-12 01:01:40 +03:00
|
|
|
|
self.tabs[i].scene.set_current_layout(name.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
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Switched to layout '{name}'."));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SetTheme(theme) => {
|
|
|
|
|
|
self.active_theme = theme;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Keyboard Shortcuts Panel ──────────────────────────────────────
|
|
|
|
|
|
Message::ShortcutsPanelOpen => {
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::Shortcuts);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ShortcutsPanelClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(alias): centralize command aliases in user-editable ocad.pgp (#286/#288)
Command aliases (L→LINE, CC→COPYCLIP) were scattered across dispatch match
arms and CommandRegistration name arrays. Move them all into a single
user-editable file:
- assets/ocad.pgp: shipped default alias table (ALIAS,*COMMAND format),
embedded and copied to config/ocad.pgp on first launch; only that user
copy is read after. AA→AREA default closes #286.
- resolve_alias() rewrites the leading command token at the top of
dispatch_command_inner, so aliases resolve for every path (families,
plugins, Repeat menu). Args after the first space are untouched.
- Autocomplete hides alias keys and, when the whole input is an alias,
surfaces its target command as the top (highlighted) suggestion so the
highlight matches what Enter runs (AA→AREA, L→LINE).
- ALIASEDIT opens a new editor modal (add/edit/delete rows) with an Apply
button; Apply persists to ocad.pgp, closing discards unapplied edits.
- Strip alias tokens from all 8 dispatch match-arm files and 71 module
CommandRegistration arrays, keeping canonical/synonym/distinct-command
names. ocad.pgp is now the sole source: deleting or remapping an alias
there takes effect. CUI stays on the keyboard-shortcut panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:39:22 +03:00
|
|
|
|
// ── Command Alias Editor (ALIASEDIT) ──────────────────────────────
|
|
|
|
|
|
Message::AliasEditorOpen => {
|
|
|
|
|
|
// Seed the working buffer from the current table, sorted by alias
|
|
|
|
|
|
// so the list is stable and diffable.
|
|
|
|
|
|
let mut rows: Vec<(String, String)> = self
|
|
|
|
|
|
.command_aliases
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(a, c)| (a.clone(), c.clone()))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
|
|
|
|
|
self.alias_editor_rows = rows;
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::Aliases);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AliasEditorInput { idx, field, value } => {
|
|
|
|
|
|
use crate::ui::window::alias_editor::AliasField;
|
|
|
|
|
|
// Aliases and commands are stored uppercase; uppercasing as the
|
|
|
|
|
|
// user types keeps display and the committed table consistent.
|
|
|
|
|
|
let value = value.to_uppercase();
|
|
|
|
|
|
if let Some(rowdata) = self.alias_editor_rows.get_mut(idx) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
AliasField::Alias => rowdata.0 = value,
|
|
|
|
|
|
AliasField::Command => rowdata.1 = value,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AliasEditorAdd => {
|
|
|
|
|
|
self.alias_editor_rows.push((String::new(), String::new()));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AliasEditorRemove(idx) => {
|
|
|
|
|
|
if idx < self.alias_editor_rows.len() {
|
|
|
|
|
|
self.alias_editor_rows.remove(idx);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AliasEditorApply => {
|
|
|
|
|
|
self.apply_alias_editor_rows();
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{} alias(es) applied.",
|
|
|
|
|
|
self.command_aliases.len()
|
|
|
|
|
|
));
|
|
|
|
|
|
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
|
|
|
|
// ── About window ──────────────────────────────────────────────
|
|
|
|
|
|
Message::AboutOpen => {
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::About);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CloseModal => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-01 08:05:25 +03:00
|
|
|
|
Message::AttrEditorOpen(handle) => {
|
|
|
|
|
|
self.open_attribute_editor(handle);
|
|
|
|
|
|
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
|
|
|
|
Message::AttrEditorTab(t) => {
|
|
|
|
|
|
self.attr_editor_tab = t;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorSelect(idx) => {
|
|
|
|
|
|
if idx < self.attr_editor_rows.len() {
|
|
|
|
|
|
self.attr_editor_selected = idx;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-01 08:05:25 +03:00
|
|
|
|
Message::AttrEditorInput { idx, value } => {
|
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 let Some(r) = self.attr_editor_rows.get_mut(idx) {
|
|
|
|
|
|
r.value = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorTextStyle(s) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.text_style = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorJustify(label) => {
|
|
|
|
|
|
if let Some((h, v)) =
|
|
|
|
|
|
crate::ui::window::attribute_editor::justify_from_label(&label)
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.h_align = h;
|
|
|
|
|
|
r.v_align = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorHeight(s) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.height = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorRotation(s) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.rotation = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorWidth(s) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.width_factor = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorOblique(s) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.oblique = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorBackwards(b) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.backwards = b;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorUpsideDown(b) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.upside_down = b;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorLayer(s) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.layer = s;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorLinetype(s) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.linetype = if s == "ByLayer" { String::new() } else { s };
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorColor(label) => {
|
|
|
|
|
|
if let Some(c) = crate::ui::window::attribute_editor::color_from_label(&label) {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.color = c;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AttrEditorLineweight(lw) => {
|
|
|
|
|
|
if let Some(r) = self.attr_row_selected_mut() {
|
|
|
|
|
|
r.line_weight = lw;
|
2026-07-01 08:05:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
Message::AttrEditorApply => self.on_attr_editor_apply(),
|
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
|
|
|
|
Message::ModalGrab => {
|
|
|
|
|
|
// Start a drag; the first ModalDragMove seeds the reference.
|
|
|
|
|
|
self.modal_dragging = true;
|
|
|
|
|
|
self.modal_drag_last = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ModalDragMove(p) => {
|
|
|
|
|
|
if self.modal_dragging {
|
|
|
|
|
|
if let Some(last) = self.modal_drag_last {
|
|
|
|
|
|
self.modal_offset.x += p.x - last.x;
|
|
|
|
|
|
self.modal_offset.y += p.y - last.y;
|
|
|
|
|
|
// Clamp so the dialog stops at the window edge instead
|
|
|
|
|
|
// of being squeezed (the off-centre padding shrinks the
|
|
|
|
|
|
// dialog once it overlaps a border).
|
|
|
|
|
|
if let Some((cw, ch)) = self.modal_outer_size() {
|
|
|
|
|
|
let ww = self.vp_size.0 + 440.0;
|
|
|
|
|
|
let wh = self.vp_size.1;
|
|
|
|
|
|
let max_x = ((ww - cw) * 0.5).max(0.0);
|
|
|
|
|
|
let max_y = ((wh - ch) * 0.5).max(0.0);
|
|
|
|
|
|
self.modal_offset.x = self.modal_offset.x.clamp(-max_x, max_x);
|
|
|
|
|
|
self.modal_offset.y = self.modal_offset.y.clamp(-max_y, max_y);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.modal_drag_last = Some(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ModalDragRelease => {
|
|
|
|
|
|
self.modal_dragging = false;
|
|
|
|
|
|
self.modal_drag_last = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::AboutCopyInfo => {
|
|
|
|
|
|
let info = format!(
|
|
|
|
|
|
"Open CAD Studio v{}\nOS: {}\nArch: {}",
|
|
|
|
|
|
env!("CARGO_PKG_VERSION"),
|
|
|
|
|
|
std::env::consts::OS,
|
|
|
|
|
|
std::env::consts::ARCH,
|
|
|
|
|
|
);
|
|
|
|
|
|
iced::clipboard::write(info)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Plugin Manager window ─────────────────────────────────────
|
|
|
|
|
|
Message::PluginManagerOpen => {
|
|
|
|
|
|
// Refresh the on-disk external-plugin list each time the manager
|
|
|
|
|
|
// opens so newly dropped-in packages show up.
|
|
|
|
|
|
self.external_plugins = crate::plugin::external::discover();
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::PluginManager);
|
|
|
|
|
|
// Fetch the curated registry and release lists for linked repos.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut tasks = vec![self.fetch_registry_task()];
|
|
|
|
|
|
tasks.extend(
|
|
|
|
|
|
self.plugin_repos
|
|
|
|
|
|
.clone()
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.map(|r| self.fetch_releases_task(r)),
|
|
|
|
|
|
);
|
|
|
|
|
|
return Task::batch(tasks);
|
|
|
|
|
|
}
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginManagerClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::SetPluginEnabled(id, enabled) => {
|
|
|
|
|
|
if enabled {
|
|
|
|
|
|
self.disabled_plugins.remove(&id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.disabled_plugins.insert(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_ribbon_modules();
|
|
|
|
|
|
self.persist_settings_if_changed();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginRepoInput(s) => {
|
|
|
|
|
|
self.plugin_repo_input = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginRepoAdd => {
|
|
|
|
|
|
let repo = self
|
|
|
|
|
|
.plugin_repo_input
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.trim_start_matches("https://github.com/")
|
|
|
|
|
|
.trim_end_matches('/')
|
|
|
|
|
|
.to_string();
|
|
|
|
|
|
if repo.is_empty() || self.plugin_repos.contains(&repo) {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.plugin_repos.push(repo.clone());
|
|
|
|
|
|
self.plugin_repo_input.clear();
|
|
|
|
|
|
self.persist_settings_if_changed();
|
|
|
|
|
|
self.marketplace_status = format!("Fetching releases for {repo}…");
|
|
|
|
|
|
self.fetch_releases_task(repo)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginRepoRemove(repo) => {
|
|
|
|
|
|
self.plugin_repos.retain(|r| r != &repo);
|
|
|
|
|
|
self.repo_release_tags.remove(&repo);
|
|
|
|
|
|
self.repo_selected_tag.remove(&repo);
|
|
|
|
|
|
self.persist_settings_if_changed();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginRegistryFetched(Ok(entries)) => {
|
|
|
|
|
|
// Fetch releases for every curated repo so the dropdowns fill in.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
let tasks: Vec<_> = entries
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|e| self.fetch_releases_task(e.repo.clone()))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
self.plugin_registry = entries;
|
|
|
|
|
|
return Task::batch(tasks);
|
|
|
|
|
|
}
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
|
|
|
|
|
self.plugin_registry = entries;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginRegistryFetched(Err(e)) => {
|
|
|
|
|
|
self.marketplace_status = format!("Registry: {e}");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-07-02 01:00:22 +03:00
|
|
|
|
Message::PatronsFetched(Ok(names)) => {
|
2026-07-08 00:32:12 +03:00
|
|
|
|
// Merge the hand-maintained supporters and rank everyone by
|
|
|
|
|
|
// amount (also sorts the web list, which arrives unsorted).
|
|
|
|
|
|
self.patrons = crate::patreon::merge_manual(names);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
// No token / offline: still show any hand-maintained supporters
|
|
|
|
|
|
// (Start page shows a "Support on Patreon" prompt when empty).
|
|
|
|
|
|
Message::PatronsFetched(Err(_)) => {
|
|
|
|
|
|
self.patrons = crate::patreon::merge_manual(Vec::new());
|
2026-07-02 01:00:22 +03:00
|
|
|
|
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
|
|
|
|
Message::PluginReleasesFetched(repo, Ok(tags)) => {
|
|
|
|
|
|
if let Some(first) = tags.first() {
|
|
|
|
|
|
self.repo_selected_tag
|
|
|
|
|
|
.entry(repo.clone())
|
|
|
|
|
|
.or_insert_with(|| first.clone());
|
|
|
|
|
|
}
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
self.marketplace_status = format!("{repo}: {} installable release(s)", tags.len());
|
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.repo_release_tags.insert(repo, tags);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginReleasesFetched(repo, Err(e)) => {
|
|
|
|
|
|
self.marketplace_status = format!("{repo}: {e}");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginReleaseSelect(repo, tag) => {
|
|
|
|
|
|
self.repo_selected_tag.insert(repo, tag);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginInstall(repo) => {
|
|
|
|
|
|
let Some(tag) = self.repo_selected_tag.get(&repo).cloned() else {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
self.marketplace_status = format!("Installing {repo} {tag}…");
|
|
|
|
|
|
self.install_task(repo, tag)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginInstalled(Ok(id)) => {
|
|
|
|
|
|
self.marketplace_status = format!("Installed '{id}'. Restart to load it.");
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
self.external_plugins = crate::plugin::external::discover();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginInstalled(Err(e)) => {
|
|
|
|
|
|
self.marketplace_status = format!("Install failed: {e}");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PluginUninstall(id) => {
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
match crate::plugin::external::uninstall(&id) {
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
self.marketplace_status =
|
|
|
|
|
|
format!("Uninstalled '{id}'. Restart to unload it.");
|
|
|
|
|
|
self.external_plugins = crate::plugin::external::discover();
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
self.marketplace_status = format!("Uninstall failed: {e}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
let _ = id;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PointStyleSetMode(mode) => {
|
|
|
|
|
|
self.set_point_mode_bits(!0, mode);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PointStyleSizeRelative(relative) => {
|
|
|
|
|
|
self.point_size_relative = relative;
|
|
|
|
|
|
self.apply_point_size();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PointStyleSizeInput(s) => {
|
|
|
|
|
|
self.point_size_buf = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PointStyleApplySize => {
|
|
|
|
|
|
self.apply_point_size();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PointStyleOk => {
|
|
|
|
|
|
self.apply_point_size();
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::EnterViewport(handle) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// Clear paper-space selection before entering model space.
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.tabs[i].scene.active_viewport = Some(handle);
|
|
|
|
|
|
// Fold a stale UTM saved view onto the effective (auto-fit)
|
|
|
|
|
|
// centre so pan/zoom, paper↔model and the display all agree —
|
|
|
|
|
|
// otherwise the camera auto-fits to the model while the cursor
|
|
|
|
|
|
// math stays at the origin, jittering as pan toggles the two.
|
|
|
|
|
|
self.tabs[i].scene.normalize_active_viewport_view();
|
|
|
|
|
|
// Grid/snap follow the entered viewport.
|
|
|
|
|
|
self.adopt_view_display(i);
|
|
|
|
|
|
// Adopt the entered viewport's own per-viewport UCS.
|
|
|
|
|
|
self.tabs[i].refresh_active_ucs();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
self.command_line.push_output("MSPACE");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ExitViewport => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// Clear model-space selection before returning to paper space.
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.tabs[i].scene.active_viewport = None;
|
|
|
|
|
|
// Grid/snap return to the paper sheet's own state.
|
|
|
|
|
|
self.adopt_view_display(i);
|
|
|
|
|
|
// Paper space has no UCS — drop the viewport's UCS.
|
|
|
|
|
|
self.tabs[i].refresh_active_ucs();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
self.command_line.push_output("PSPACE");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::MspaceCommand => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.current_layout == "Model" {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("MS is only available in paper space layouts.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.tabs[i].scene.active_viewport.is_some() {
|
|
|
|
|
|
// Already in MSPACE — nothing to do.
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
match self.tabs[i].scene.first_user_viewport() {
|
|
|
|
|
|
Some(handle) => Task::done(Message::EnterViewport(handle)),
|
|
|
|
|
|
None => {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("No viewport found in this layout.");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PspaceCommand => Task::done(Message::ExitViewport),
|
|
|
|
|
|
|
|
|
|
|
|
Message::Undo => {
|
|
|
|
|
|
self.undo_active_tab();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::Redo => {
|
|
|
|
|
|
self.redo_active_tab();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::UndoMany(steps) => {
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
self.undo_steps(steps);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RedoMany(steps) => {
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
self.redo_steps(steps);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::Noop => Task::none(),
|
|
|
|
|
|
|
|
|
|
|
|
// ── Unsaved-changes dialog ────────────────────────────────────
|
|
|
|
|
|
Message::UnsavedDialogCancel => {
|
|
|
|
|
|
self.pending_close = None;
|
|
|
|
|
|
self.close_unsaved_dialog_window()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::UnsavedDialogDiscard => self.on_unsaved_dialog_discard(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::UnsavedDialogSave => self.on_unsaved_dialog_save(),
|
|
|
|
|
|
|
2026-06-29 11:47:37 +03:00
|
|
|
|
Message::AecDropSameVersion => self.on_aec_drop_same_version(),
|
|
|
|
|
|
Message::AecDropProceed => self.on_aec_drop_proceed(),
|
|
|
|
|
|
Message::AecDropBack => {
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:17:40 +03:00
|
|
|
|
Message::AutoSave => self.on_autosave(),
|
2026-06-29 11:47:37 +03:00
|
|
|
|
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
Message::UnsavedPickedSavePath(Some(path)) => {
|
|
|
|
|
|
self.on_unsaved_picked_save_path_some(path)
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
Message::UnsavedPickedSavePath(None) => {
|
|
|
|
|
|
// User cancelled the save-as dialog — re-open the confirmation dialog.
|
|
|
|
|
|
if self.pending_close.is_some() {
|
|
|
|
|
|
return self.open_unsaved_dialog_window();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Page Setup ────────────────────────────────────────────────
|
|
|
|
|
|
Message::UpdateCheckResult(latest) => {
|
|
|
|
|
|
let Some(info) = latest else {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
self.update_notice_version = Some(info.version);
|
|
|
|
|
|
self.update_notice_body = Some(info.body);
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::UpdateNotice);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::UpdateNoticeClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::UpdateNoticeOpenRelease => {
|
2026-06-26 23:06:40 +03:00
|
|
|
|
crate::sys::open_url(crate::io::update_check::RELEASES_PAGE);
|
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.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AssocPromptYes => {
|
2026-06-29 13:06:33 +03:00
|
|
|
|
self.file_assoc_enabled = 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
|
|
|
|
self.mark_assoc_prompted();
|
|
|
|
|
|
self.active_modal = None;
|
2026-06-29 13:06:33 +03:00
|
|
|
|
// set_default_app registers the handler first, then makes us the
|
|
|
|
|
|
// default — boot no longer does this automatically.
|
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::perform(
|
|
|
|
|
|
crate::io::file_association::set_default_app(),
|
|
|
|
|
|
Message::AssocResult,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AssocPromptNo => {
|
2026-06-29 13:06:33 +03:00
|
|
|
|
self.file_assoc_enabled = 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.mark_assoc_prompted();
|
|
|
|
|
|
self.active_modal = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AssocResult(result) => {
|
|
|
|
|
|
match result {
|
|
|
|
|
|
Ok(msg) => self.command_line.push_info(&msg),
|
|
|
|
|
|
Err(err) => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_error(&format!("Could not set default app: {err}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
Message::PlotDialogOpen => self.on_plot_dialog_open(),
|
|
|
|
|
|
Message::PlotDlg(m) => self.on_plot_dlg(m),
|
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
|
|
|
|
|
|
|
|
|
|
// ── Plot / Export ─────────────────────────────────────────────
|
|
|
|
|
|
Message::PlotExport => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let stem = self.tabs[i]
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.and_then(|p: &std::path::Path| p.file_stem())
|
|
|
|
|
|
.map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| "drawing".into());
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
crate::io::pdf_export::pick_pdf_path_owned(stem),
|
|
|
|
|
|
Message::PlotExportPath,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotExportPath(None) => Task::none(),
|
|
|
|
|
|
Message::PlotExportPath(Some(path)) => self.on_plot_export_path_some(path),
|
|
|
|
|
|
|
2026-07-05 23:12:52 +02:00
|
|
|
|
Message::PlotFormat(f) => {
|
|
|
|
|
|
self.plot_format = f;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotOrientation(o) => {
|
|
|
|
|
|
self.plot_orientation = o;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotWindowExport => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let stem = self.tabs[i]
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.and_then(|p: &std::path::Path| p.file_stem())
|
|
|
|
|
|
.map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| "drawing".into());
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
crate::io::pdf_export::pick_pdf_path_owned(stem),
|
|
|
|
|
|
Message::PlotWindowExportPath,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotWindowExportPath(None) => Task::none(),
|
|
|
|
|
|
Message::PlotWindowExportPath(Some(path)) => self.on_plot_window_export_path_some(path),
|
|
|
|
|
|
|
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
|
|
|
|
// ── Print to system printer ───────────────────────────────────────
|
|
|
|
|
|
Message::PrintToPrinter => self.on_print_to_printer(),
|
|
|
|
|
|
Message::PrintResult(Ok(printer)) => {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_info(&format!("Sent to printer: {printer}"));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PrintResult(Err(e)) => {
|
|
|
|
|
|
self.command_line.push_error(&format!("Print failed: {e}"));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Plot Style Table ──────────────────────────────────────────────
|
|
|
|
|
|
Message::PlotStyleLoad => {
|
|
|
|
|
|
Task::perform(crate::io::pick_plot_style(), Message::PlotStyleLoaded)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotStyleLoaded(Some(table)) => {
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Plot style '{}' loaded ({} color entries).",
|
|
|
|
|
|
table.name,
|
|
|
|
|
|
table
|
|
|
|
|
|
.aci_entries
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|e| e.color.is_some())
|
|
|
|
|
|
.count()
|
|
|
|
|
|
));
|
|
|
|
|
|
self.active_plot_style = Some(table);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotStyleLoaded(None) => Task::none(),
|
|
|
|
|
|
Message::PlotStyleClear => {
|
|
|
|
|
|
self.active_plot_style = None;
|
|
|
|
|
|
self.command_line.push_output("Plot style table cleared.");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Plot Style Panel ──────────────────────────────────────────────
|
|
|
|
|
|
Message::PlotStylePanelOpen => {
|
|
|
|
|
|
// Initialise edit buffers for ACI 1.
|
|
|
|
|
|
self.plotstyle_panel_aci = 1;
|
|
|
|
|
|
let entry = self
|
|
|
|
|
|
.active_plot_style
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|t| t.aci_entries.get(1));
|
|
|
|
|
|
self.ps_color_buf = entry
|
|
|
|
|
|
.and_then(|e| {
|
|
|
|
|
|
e.color
|
|
|
|
|
|
.map(|[r, g, b]| format!("#{:02X}{:02X}{:02X}", r, g, b))
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
self.ps_lineweight_buf = entry
|
|
|
|
|
|
.map(|e| e.lineweight.to_string())
|
|
|
|
|
|
.unwrap_or("255".into());
|
|
|
|
|
|
self.ps_screening_buf = entry
|
|
|
|
|
|
.map(|e| e.screening.to_string())
|
|
|
|
|
|
.unwrap_or("100".into());
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::Plotstyle);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotStylePanelClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotStylePanelSelectAci(aci) => {
|
|
|
|
|
|
self.plotstyle_panel_aci = aci;
|
|
|
|
|
|
let entry = self
|
|
|
|
|
|
.active_plot_style
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|t| t.aci_entries.get(aci as usize));
|
|
|
|
|
|
self.ps_color_buf = entry
|
|
|
|
|
|
.and_then(|e| {
|
|
|
|
|
|
e.color
|
|
|
|
|
|
.map(|[r, g, b]| format!("#{:02X}{:02X}{:02X}", r, g, b))
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
self.ps_lineweight_buf = entry
|
|
|
|
|
|
.map(|e| e.lineweight.to_string())
|
|
|
|
|
|
.unwrap_or("255".into());
|
|
|
|
|
|
self.ps_screening_buf = entry
|
|
|
|
|
|
.map(|e| e.screening.to_string())
|
|
|
|
|
|
.unwrap_or("100".into());
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotStylePanelColorBuf(s) => {
|
|
|
|
|
|
self.ps_color_buf = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotStylePanelLwBuf(s) => {
|
|
|
|
|
|
self.ps_lineweight_buf = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotStylePanelScreenBuf(s) => {
|
|
|
|
|
|
self.ps_screening_buf = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PlotStylePanelApply => self.on_plot_style_panel_apply(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::PlotStylePanelSave => self.on_plot_style_panel_save(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::PlotStylePanelSavePath(Some(path)) => {
|
|
|
|
|
|
if let Some(table) = &self.active_plot_style {
|
|
|
|
|
|
match table.save(&path) {
|
|
|
|
|
|
Ok(()) => self.command_line.push_output(&format!(
|
|
|
|
|
|
"Plot style table saved to \"{}\".",
|
|
|
|
|
|
path.display()
|
|
|
|
|
|
)),
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Save error: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotStylePanelSavePath(None) => Task::none(),
|
|
|
|
|
|
|
|
|
|
|
|
// ── TextStyle Font Browser ────────────────────────────────────────
|
|
|
|
|
|
Message::TextStyleDialogOpen => self.on_text_style_dialog_open(),
|
|
|
|
|
|
Message::TextStyleDialogClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextStyleDialogSelect(name) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.textstyle_selected = name;
|
|
|
|
|
|
self.load_textstyle_bufs(i);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextStyleDialogSetCurrent => {
|
|
|
|
|
|
// Staged: persists on Apply.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.textstyle_selected.clone();
|
|
|
|
|
|
if self.tabs[i].scene.document.text_styles.get(&name).is_some() {
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_text_style_name = name.clone();
|
|
|
|
|
|
self.sync_ribbon_styles();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Current text style: {}", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextStyleDialogNew => {
|
|
|
|
|
|
self.style_new(crate::app::StyleKind::Text);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextStyleDialogCopy => {
|
|
|
|
|
|
self.style_copy(crate::app::StyleKind::Text);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextStyleDialogDelete => {
|
|
|
|
|
|
self.style_delete(crate::app::StyleKind::Text);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
// ── Shared inline rename (all style managers) ─────────────────
|
|
|
|
|
|
Message::StyleRenameStart(kind, name) => {
|
|
|
|
|
|
self.style_rename_start(kind, name);
|
|
|
|
|
|
// Focus the freshly-shown rename field so the user can type
|
|
|
|
|
|
// immediately after the double click.
|
refactor: group ui/ style+popup dirs, rename cmd_result driver
- Group the flat src/ui/ dir: move the 9 style editors into ui/style/
(dimstyle, mleaderstyle, mlstyle, plotstyle, textstyle, tablestyle,
point_style, style_list, style_manager) and the 6 popups into
ui/popup/ (cycle, isolate, scale, selection_filter, snap, units).
Call sites repointed to crate::ui::style::/crate::ui::popup::; no
compat re-exports. ui/ root drops from 36 to 21 files.
- Rename app/cmd_result.rs -> app/command_driver.rs: the file is the
StepInput->feed_command driver, not the CmdResult type (which lives
in command.rs). Name now matches contents.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 23:34:10 +03:00
|
|
|
|
iced::widget::operation::focus(crate::ui::style::style_list::rename_input_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
|
|
|
|
}
|
|
|
|
|
|
Message::StyleRenameEdit(s) => {
|
|
|
|
|
|
self.style_rename_buf = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::StyleRenameCommit(kind) => {
|
|
|
|
|
|
self.style_rename_commit(kind);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::StyleRenameCancel => {
|
|
|
|
|
|
self.style_rename_cancel();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextStyleEdit { field, value } => {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"font" => self.textstyle_font = value,
|
|
|
|
|
|
"width" => self.textstyle_width = value,
|
|
|
|
|
|
"oblique" => self.textstyle_oblique = value,
|
|
|
|
|
|
"height" => self.textstyle_height = value,
|
|
|
|
|
|
"bigfont" => self.textstyle_bigfont = value,
|
|
|
|
|
|
"ttf" => self.textstyle_ttf = value,
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextStyleToggle(field) => {
|
|
|
|
|
|
// Staged: mutate live for preview, persist on Apply.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.textstyle_selected.clone();
|
|
|
|
|
|
if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"backward" => s.flags.backward = !s.flags.backward,
|
|
|
|
|
|
"upside_down" => s.flags.upside_down = !s.flags.upside_down,
|
|
|
|
|
|
"annotative" => s.annotative = !s.annotative,
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TextStyleApply => self.on_text_style_apply(),
|
|
|
|
|
|
Message::TextStyleFontPick(font_file) => {
|
|
|
|
|
|
// Staged: update the buffer + live style; persist on Apply.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.textstyle_font = font_file.clone();
|
|
|
|
|
|
let name = self.textstyle_selected.clone();
|
|
|
|
|
|
if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) {
|
|
|
|
|
|
s.font_file = font_file;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── TableStyle Dialog ─────────────────────────────────────────────
|
|
|
|
|
|
Message::TableStyleDialogOpen => {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tablestyle_selected = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.find_map(|o| {
|
|
|
|
|
|
if let ObjectType::TableStyle(s) = o {
|
|
|
|
|
|
Some(s.name.clone())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_else(|| "Standard".to_string());
|
|
|
|
|
|
self.load_tablestyle_bufs(i);
|
|
|
|
|
|
self.active_modal = Some(super::ModalKind::TableStyle);
|
|
|
|
|
|
self.style_stage_begin();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TableStyleDialogClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TableStyleDialogSelect(name) => {
|
|
|
|
|
|
self.tablestyle_selected = name;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.load_tablestyle_bufs(i);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleEdit { field, value } => {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"hmargin" => self.ts_hmargin = value,
|
|
|
|
|
|
"vmargin" => self.ts_vmargin = value,
|
|
|
|
|
|
"description" => self.ts_description = value,
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleApply => {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.tablestyle_selected.clone();
|
|
|
|
|
|
let h: Option<f64> = self.ts_hmargin.trim().parse().ok();
|
|
|
|
|
|
let v: Option<f64> = self.ts_vmargin.trim().parse().ok();
|
|
|
|
|
|
let desc = self.ts_description.clone();
|
|
|
|
|
|
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
|
|
|
|
|
if let ObjectType::TableStyle(s) = obj {
|
|
|
|
|
|
if s.name == name {
|
|
|
|
|
|
if let Some(h) = h {
|
|
|
|
|
|
s.horizontal_margin = h;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(v) = v {
|
|
|
|
|
|
s.vertical_margin = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
s.description = desc.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.style_stage_commit();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleSetFlow(value) => {
|
|
|
|
|
|
use acadrust::objects::TableFlowDirection;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(s) = self.tablestyle_mut(i) {
|
|
|
|
|
|
s.flow_direction = match value.as_str() {
|
|
|
|
|
|
"Up" => TableFlowDirection::Up,
|
|
|
|
|
|
_ => TableFlowDirection::Down,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableColorMore(row, field) => {
|
|
|
|
|
|
self.ts_color_open = if self.ts_color_open == Some((row, field)) {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some((row, field))
|
|
|
|
|
|
};
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TableStyleCellEdit { row, field, value } => {
|
|
|
|
|
|
self.ts_color_open = None;
|
|
|
|
|
|
let r = row as usize;
|
|
|
|
|
|
if r < 3 {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"textstyle" => self.ts_cell_textstyle[r] = value,
|
|
|
|
|
|
"height" => self.ts_cell_height[r] = value,
|
|
|
|
|
|
"textcolor" => self.ts_cell_textcolor[r] = value,
|
|
|
|
|
|
"fillcolor" => self.ts_cell_fillcolor[r] = value,
|
|
|
|
|
|
"datatype" => self.ts_cell_datatype[r] = value,
|
|
|
|
|
|
"unittype" => self.ts_cell_unittype[r] = value,
|
|
|
|
|
|
"format" => self.ts_cell_format[r] = value,
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleBorderEdit {
|
|
|
|
|
|
cell,
|
|
|
|
|
|
border,
|
|
|
|
|
|
field,
|
|
|
|
|
|
value,
|
|
|
|
|
|
} => {
|
|
|
|
|
|
let (c, b) = (cell as usize, border as usize);
|
|
|
|
|
|
if c < 3 && b < 6 {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"lw" => self.ts_border_lw[c][b] = value,
|
|
|
|
|
|
"color" => self.ts_border_color[c][b] = value,
|
|
|
|
|
|
"spacing" => self.ts_border_spacing[c][b] = value,
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleBorderSetType {
|
|
|
|
|
|
cell,
|
|
|
|
|
|
border,
|
|
|
|
|
|
value,
|
|
|
|
|
|
} => {
|
|
|
|
|
|
use acadrust::objects::TableBorderType;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(s) = self.tablestyle_mut(i) {
|
|
|
|
|
|
if let Some(bd) =
|
|
|
|
|
|
Self::ts_cell_of(s, cell).and_then(|c| Self::ts_border_of(c, border))
|
|
|
|
|
|
{
|
|
|
|
|
|
bd.border_type = match value.as_str() {
|
|
|
|
|
|
"Double" => TableBorderType::Double,
|
|
|
|
|
|
_ => TableBorderType::Single,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleBorderToggleInvisible { cell, border } => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(s) = self.tablestyle_mut(i) {
|
|
|
|
|
|
if let Some(bd) =
|
|
|
|
|
|
Self::ts_cell_of(s, cell).and_then(|c| Self::ts_border_of(c, border))
|
|
|
|
|
|
{
|
|
|
|
|
|
bd.is_invisible = !bd.is_invisible;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleCellToggleFill(row) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(s) = self.tablestyle_mut(i) {
|
|
|
|
|
|
if let Some(c) = Self::ts_cell_of(s, row) {
|
|
|
|
|
|
c.fill_enabled = !c.fill_enabled;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleCellSetAlign { row, value } => {
|
|
|
|
|
|
use acadrust::objects::CellAlignment;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(s) = self.tablestyle_mut(i) {
|
|
|
|
|
|
if let Some(c) = Self::ts_cell_of(s, row) {
|
|
|
|
|
|
c.alignment = match value.as_str() {
|
|
|
|
|
|
"TopLeft" => CellAlignment::TopLeft,
|
|
|
|
|
|
"TopCenter" => CellAlignment::TopCenter,
|
|
|
|
|
|
"TopRight" => CellAlignment::TopRight,
|
|
|
|
|
|
"MiddleLeft" => CellAlignment::MiddleLeft,
|
|
|
|
|
|
"MiddleRight" => CellAlignment::MiddleRight,
|
|
|
|
|
|
"BottomLeft" => CellAlignment::BottomLeft,
|
|
|
|
|
|
"BottomCenter" => CellAlignment::BottomCenter,
|
|
|
|
|
|
"BottomRight" => CellAlignment::BottomRight,
|
|
|
|
|
|
_ => CellAlignment::MiddleCenter,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleCellApply(row) => self.on_table_style_cell_apply(row),
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleToggle(field) => {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.tablestyle_selected.clone();
|
|
|
|
|
|
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
|
|
|
|
|
if let ObjectType::TableStyle(s) = obj {
|
|
|
|
|
|
if s.name == name {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"title_sup" => s.title_suppressed = !s.title_suppressed,
|
|
|
|
|
|
"header_sup" => s.header_suppressed = !s.header_suppressed,
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleToggleAnnotative => {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.tablestyle_selected.clone();
|
|
|
|
|
|
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
|
|
|
|
|
if let ObjectType::TableStyle(s) = obj {
|
|
|
|
|
|
if s.name == name {
|
|
|
|
|
|
s.annotative = !s.annotative;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TableStyleDialogNew => {
|
|
|
|
|
|
self.style_new(crate::app::StyleKind::Table);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TableStyleDialogCopy => {
|
|
|
|
|
|
self.style_copy(crate::app::StyleKind::Table);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TableStyleDialogDelete => {
|
|
|
|
|
|
self.style_delete(crate::app::StyleKind::Table);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TableStyleDialogSetCurrent => {
|
|
|
|
|
|
// Staged: persists on Apply. The header field is the round-trip
|
|
|
|
|
|
// source of truth ($CTABLESTYLE); the ribbon mirrors it.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.tablestyle_selected.clone();
|
|
|
|
|
|
if self
|
|
|
|
|
|
.style_names(crate::app::StyleKind::Table)
|
|
|
|
|
|
.contains(&name)
|
|
|
|
|
|
{
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_table_style_name = name.clone();
|
|
|
|
|
|
self.ribbon.active_table_style = name.clone();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Current table style: {name}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── MLineStyle Dialog ─────────────────────────────────────────────
|
|
|
|
|
|
Message::MlStyleDialogOpen => self.on_ml_style_dialog_open(),
|
|
|
|
|
|
Message::MlStyleDialogClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MlStyleDialogSelect(name) => {
|
|
|
|
|
|
self.mlstyle_selected = name;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MlStyleDialogSetCurrent => {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let name = self.mlstyle_selected.clone();
|
|
|
|
|
|
let exists = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.any(|o| matches!(o, ObjectType::MLineStyle(s) if s.name == name));
|
|
|
|
|
|
if exists {
|
|
|
|
|
|
// Staged: persists on Apply.
|
|
|
|
|
|
self.tabs[i].scene.document.header.multiline_style = name.clone();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Current multiline style: {}", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
// Placeholder so the Multiline manager has the same Set Current +
|
|
|
|
|
|
// Apply pair as every other style manager. The editor is currently
|
|
|
|
|
|
// read-only, so there is nothing to apply yet — wire this up when
|
|
|
|
|
|
// editable MLineStyle properties land.
|
|
|
|
|
|
Message::MlStyleApply => {
|
|
|
|
|
|
// Multiline styles have no editable properties yet; Apply still
|
|
|
|
|
|
// commits any staged structural / current-style changes.
|
|
|
|
|
|
self.style_stage_commit();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MlStyleDialogNew => {
|
|
|
|
|
|
self.style_new(crate::app::StyleKind::MLine);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MlStyleDialogCopy => {
|
|
|
|
|
|
self.style_copy(crate::app::StyleKind::MLine);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MlStyleDialogDelete => {
|
|
|
|
|
|
self.style_delete(crate::app::StyleKind::MLine);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── MLeaderStyle Dialog ───────────────────────────────────────────
|
|
|
|
|
|
Message::MLeaderStyleDialogOpen => self.on_mleader_style_dialog_open(),
|
|
|
|
|
|
Message::MLeaderStyleDialogClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MLeaderStyleDialogSelect(name) => {
|
|
|
|
|
|
self.mleaderstyle_selected = name;
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.load_mleaderstyle_bufs(i);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MLeaderStyleDialogSetCurrent => self.on_mleader_style_dialog_set_current(),
|
|
|
|
|
|
Message::MLeaderStyleDialogNew => {
|
|
|
|
|
|
self.style_new(crate::app::StyleKind::MLeader);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MLeaderStyleDialogCopy => {
|
|
|
|
|
|
self.style_copy(crate::app::StyleKind::MLeader);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MLeaderStyleDialogDelete => {
|
|
|
|
|
|
self.style_delete(crate::app::StyleKind::MLeader);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MLeaderStyleEdit { field, value } => self.on_mleader_style_edit(field, value),
|
|
|
|
|
|
Message::MLeaderStyleToggle(field) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(s) = self.mleaderstyle_mut(i) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"enable_landing" => s.enable_landing = !s.enable_landing,
|
|
|
|
|
|
"enable_dogleg" => s.enable_dogleg = !s.enable_dogleg,
|
|
|
|
|
|
"text_frame" => s.text_frame = !s.text_frame,
|
|
|
|
|
|
"text_always_left" => s.text_always_left = !s.text_always_left,
|
|
|
|
|
|
"annotative" => s.is_annotative = !s.is_annotative,
|
|
|
|
|
|
"enable_block_scale" => s.enable_block_scale = !s.enable_block_scale,
|
|
|
|
|
|
"enable_block_rotation" => {
|
|
|
|
|
|
s.enable_block_rotation = !s.enable_block_rotation
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MLeaderColorMore(field) => {
|
|
|
|
|
|
self.mls_color_open = if self.mls_color_open == Some(field) {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(field)
|
|
|
|
|
|
};
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
Message::MLeaderStyleSetEnum { field, value } => {
|
|
|
|
|
|
self.on_mleader_style_set_enum(field, value)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::MLeaderStyleSetHandle { field, value } => {
|
|
|
|
|
|
self.on_mleader_style_set_handle(field, value)
|
|
|
|
|
|
}
|
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
|
|
|
|
Message::MLeaderStyleApply => self.on_mleader_style_apply(),
|
|
|
|
|
|
|
|
|
|
|
|
// ── DimStyle Dialog ───────────────────────────────────────────────
|
|
|
|
|
|
Message::DimStyleDialogOpen => self.on_dim_style_dialog_open(),
|
|
|
|
|
|
Message::DimStyleDialogClose => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DimStyleDialogApply => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.apply_dimstyle_bufs(i);
|
|
|
|
|
|
self.style_stage_commit();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DimStyleDialogSelect(name) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.dimstyle_selected = name;
|
|
|
|
|
|
self.load_dimstyle_bufs(i);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DimStyleDialogTab(tab) => {
|
|
|
|
|
|
self.dimstyle_tab = tab;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DimStyleDialogNew => {
|
|
|
|
|
|
self.style_new(crate::app::StyleKind::Dim);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DimStyleDialogCopy => {
|
|
|
|
|
|
self.style_copy(crate::app::StyleKind::Dim);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DimStyleDialogSetCurrent => {
|
|
|
|
|
|
// Staged: persists on Apply.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_dimstyle_name =
|
|
|
|
|
|
self.dimstyle_selected.clone();
|
|
|
|
|
|
self.sync_ribbon_styles();
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Current dim style set to '{}'.",
|
|
|
|
|
|
self.dimstyle_selected
|
|
|
|
|
|
));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DimStyleDialogDelete => {
|
|
|
|
|
|
self.style_delete(crate::app::StyleKind::Dim);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DsEdit(field, val) => {
|
|
|
|
|
|
self.apply_ds_edit(field, val);
|
|
|
|
|
|
self.ds_color_open = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DsToggle(field) => {
|
|
|
|
|
|
self.apply_ds_toggle(field);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::DsColorMore(field) => {
|
|
|
|
|
|
self.ds_color_open = if self.ds_color_open.as_ref() == Some(&field) {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(field)
|
|
|
|
|
|
};
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::OpenColorWindow(target) => {
|
|
|
|
|
|
self.color_pick_target = Some(target);
|
|
|
|
|
|
self.ds_color_open = None;
|
|
|
|
|
|
self.mls_color_open = None;
|
|
|
|
|
|
self.ts_color_open = None;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].properties.color_picker_open = false;
|
|
|
|
|
|
self.tabs[i].layers.color_picker_row = None;
|
|
|
|
|
|
// Shown as a nested modal over the active dialog (Plan B):
|
|
|
|
|
|
// `color_pick_target.is_some()` drives the overlay in view_main.
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::CloseColorPicker => {
|
|
|
|
|
|
self.color_pick_target = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ColorWindowPick(color) => self.on_color_window_pick(color),
|
|
|
|
|
|
Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|