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::document::DocumentTab;
|
|
|
|
|
|
use super::document::DynComponent;
|
|
|
|
|
|
use super::helpers::grid_plane_from_camera;
|
|
|
|
|
|
use super::history::history_dropdown_labels;
|
|
|
|
|
|
use super::{Message, OpenCADStudio};
|
|
|
|
|
|
use crate::scene::pick::grip::{grips_to_screen, grips_to_screen_paper, grips_to_screen_rte};
|
|
|
|
|
|
use crate::scene::view::viewport_pane::ViewportPane;
|
|
|
|
|
|
use crate::scene::{VIEWCUBE_PAD, VIEWCUBE_REGION_PX};
|
2026-07-10 14:58:48 +03:00
|
|
|
|
use crate::ui::wrap_bar::DensitySwap;
|
2026-07-05 16:44:44 +03:00
|
|
|
|
use crate::ui::wrap_bar::WrapFlow;
|
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 iced::widget::{
|
2026-07-05 21:02:05 +03:00
|
|
|
|
button, canvas, column, container, mouse_area, pane_grid, responsive, row, shader, stack, text,
|
|
|
|
|
|
Row, Space,
|
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 iced::window;
|
|
|
|
|
|
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
|
|
|
|
|
|
|
|
|
|
|
mod controls;
|
|
|
|
|
|
mod modal;
|
|
|
|
|
|
mod overlay;
|
|
|
|
|
|
mod viewcube;
|
|
|
|
|
|
|
|
|
|
|
|
use controls::{dyn_component_value, viewport_controls};
|
|
|
|
|
|
use overlay::{
|
|
|
|
|
|
layout_context_menu_overlay, mtext_editor_overlay, position_canvas_overlay, qselect_overlay,
|
|
|
|
|
|
text_inline_overlay, viewport_context_menu_overlay,
|
|
|
|
|
|
};
|
2026-06-27 01:56:17 +03:00
|
|
|
|
use viewcube::{viewcube_nav_controls, viewcube_ucs_picker, UCS_PICKER_W};
|
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
|
|
|
|
|
|
|
|
|
|
// Re-export the text-input element ids so sibling modules can address them at
|
|
|
|
|
|
// the `view::` path as before the split.
|
|
|
|
|
|
pub(in crate::app) use overlay::{MTEXT_TEXT_ID, TEXT_INLINE_ID};
|
|
|
|
|
|
|
|
|
|
|
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_REGION_PX;
|
|
|
|
|
|
|
2026-07-10 14:58:48 +03:00
|
|
|
|
/// Clear gap (px) kept between the render-mode bar (top-left) and the ViewCube
|
|
|
|
|
|
/// (top-right) before the cube is judged to collide and hides.
|
|
|
|
|
|
const VIEWCUBE_GAP: f32 = 12.0;
|
|
|
|
|
|
|
|
|
|
|
|
/// True when a viewport `tile_w` px wide still has room for the ViewCube beside
|
|
|
|
|
|
/// a render-mode bar of measured width `bar_w`. When it doesn't, the cube hides
|
|
|
|
|
|
/// first (the bar keeps priority); the bar itself hides separately, only when it
|
|
|
|
|
|
/// no longer fits at all (its `DensitySwap`).
|
|
|
|
|
|
fn viewcube_has_room(bar_w: f32, tile_w: f32) -> bool {
|
|
|
|
|
|
bar_w + VIEWCUBE_GAP + VIEWCUBE_REGION_PX + VIEWCUBE_PAD <= tile_w
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
|
|
|
|
|
|
/// so wrap it locally with a friendly name renderer.
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
|
pub(super) struct RenderModeChoice(pub acadrust::entities::ViewportRenderMode);
|
|
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for RenderModeChoice {
|
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
|
use acadrust::entities::ViewportRenderMode as M;
|
|
|
|
|
|
f.write_str(match self.0 {
|
|
|
|
|
|
M::Wireframe2D => "Wireframe 2D",
|
|
|
|
|
|
M::Wireframe3D => "Wireframe 3D",
|
|
|
|
|
|
M::HiddenLine => "Hidden Line",
|
|
|
|
|
|
M::FlatShaded => "Flat Shaded",
|
|
|
|
|
|
M::GouraudShaded => "Gouraud Shaded",
|
|
|
|
|
|
M::FlatShadedWithEdges => "Flat Shaded + Edges",
|
|
|
|
|
|
M::GouraudShadedWithEdges => "Gouraud Shaded + Edges",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl OpenCADStudio {
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub fn view(&self, window_id: window::Id) -> Element<'_, Message> {
|
|
|
|
|
|
// ── Floating panel windows ─────────────────────────────────────────
|
|
|
|
|
|
// All dialogs are in-canvas modals now (Plan B); view_main stacks the
|
|
|
|
|
|
// active one. `window_id` is unused — there is only the main window.
|
|
|
|
|
|
let _ = window_id;
|
|
|
|
|
|
self.view_main()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The primary window: viewport, ribbon, tab bar, status bar. Split out of
|
|
|
|
|
|
/// `view` so the single-window web build can render it directly, bypassing
|
|
|
|
|
|
/// the multi-window id dispatch above (the web build has no extra windows).
|
|
|
|
|
|
pub fn view_main(&self) -> Element<'_, Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let tab = &self.tabs[i];
|
|
|
|
|
|
let is_paper = tab.scene.current_layout != "Model";
|
2026-07-10 14:58:48 +03:00
|
|
|
|
// Adaptive corner widgets: the ViewCube shows only while the active
|
|
|
|
|
|
// viewport is wide enough to hold it *beside* the render-mode bar, whose
|
|
|
|
|
|
// real width is measured each frame by its `DensitySwap` and read back
|
|
|
|
|
|
// here (from the previous frame). This drives the GPU cube (via the pane
|
|
|
|
|
|
// flag), the ViewCube nav/UCS widgets, and the hover hit-test alike, so
|
|
|
|
|
|
// they never overlap. The bar itself hides independently (its own
|
|
|
|
|
|
// DensitySwap) only when it no longer fits at all.
|
|
|
|
|
|
let render_bar_w =
|
|
|
|
|
|
f32::from_bits(self.render_bar_w.load(std::sync::atomic::Ordering::Relaxed));
|
|
|
|
|
|
let active_vp_w = if is_paper {
|
|
|
|
|
|
tab.scene
|
|
|
|
|
|
.active_viewport
|
|
|
|
|
|
.and_then(|h| {
|
|
|
|
|
|
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
tab.scene.viewport_screen_rect(h, (cw, ch))
|
|
|
|
|
|
})
|
|
|
|
|
|
.map(|r| r.width)
|
|
|
|
|
|
.unwrap_or(f32::INFINITY)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
tab.scene.active_model_tile_bounds(vw, vh).width
|
|
|
|
|
|
};
|
|
|
|
|
|
let viewcube_visible =
|
|
|
|
|
|
self.show_viewcube && !tab.is_start && viewcube_has_room(render_bar_w, active_vp_w);
|
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
|
|
|
|
// Start tab: render welcome page in place of the viewport.
|
|
|
|
|
|
// Surrounding chrome (tab bar, status bar) stays; the welcome widget
|
|
|
|
|
|
// returned here also flags the rest of `view` to skip drawing-only
|
|
|
|
|
|
// overlays via `tab.is_start`.
|
|
|
|
|
|
// Unified GPU widget for both layouts. A paper layout renders through
|
|
|
|
|
|
// the same shader as model space: a full-canvas top-locked "sheet"
|
|
|
|
|
|
// viewport draws the layout's own geometry (white sheet + entities +
|
|
|
|
|
|
// borders) and the floating content viewports blit on top.
|
|
|
|
|
|
let viewport_3d: Element<'_, Message> = if tab.is_start {
|
2026-07-05 20:53:11 +03:00
|
|
|
|
start_page_view(
|
|
|
|
|
|
&self.patrons,
|
2026-07-21 10:07:21 +03:00
|
|
|
|
&self.videos,
|
|
|
|
|
|
self.videos_loading,
|
|
|
|
|
|
&self.video_thumbs,
|
2026-07-05 20:53:11 +03:00
|
|
|
|
&self.recent_files,
|
2026-07-10 23:03:23 +03:00
|
|
|
|
&self.recent_thumbs,
|
2026-07-06 01:06:01 +03:00
|
|
|
|
self.recent_limit,
|
|
|
|
|
|
&self.recent_limit_input,
|
2026-07-05 20:53:11 +03:00
|
|
|
|
self.win_size.0,
|
|
|
|
|
|
self.start_section,
|
|
|
|
|
|
)
|
2026-06-27 11:50:11 +03:00
|
|
|
|
} else if is_paper {
|
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
|
|
|
|
shader(ViewportPane::model(
|
|
|
|
|
|
&tab.scene,
|
2026-07-10 14:58:48 +03:00
|
|
|
|
viewcube_visible,
|
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
|
|
|
|
tab.render_mode,
|
|
|
|
|
|
))
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
2026-06-27 11:50:11 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
// Model space: a pane_grid of per-pane shader widgets (rendering
|
|
|
|
|
|
// only). The input mouse_areas live in a SECOND, identical pane_grid
|
|
|
|
|
|
// layered ABOVE the crosshair overlay (`model_input_layer` below) —
|
|
|
|
|
|
// they must sit above the selection overlay, whose `Hidden` cursor
|
|
|
|
|
|
// interaction otherwise "levitates" the cursor and starves any layer
|
|
|
|
|
|
// beneath it of mouse events. A separate eventless `responsive`
|
|
|
|
|
|
// Space captures the area size to keep `vp_size` / tile rects
|
|
|
|
|
|
// current (building the pane_grid inside `responsive` resets the
|
|
|
|
|
|
// mouse_areas' hover state and drops their move events).
|
|
|
|
|
|
let scene = &tab.scene;
|
2026-07-10 14:58:48 +03:00
|
|
|
|
let show_viewcube = viewcube_visible;
|
2026-06-27 11:50:11 +03:00
|
|
|
|
let render_mode = tab.render_mode;
|
|
|
|
|
|
let size_probe: Element<'_, Message> = responsive(move |size| {
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut sel = scene.selection.borrow_mut();
|
|
|
|
|
|
sel.vp_size = (size.width, size.height);
|
|
|
|
|
|
}
|
|
|
|
|
|
scene.sync_tiles_from_panes(size.width, size.height);
|
|
|
|
|
|
Space::new().width(Fill).height(Fill).into()
|
|
|
|
|
|
})
|
|
|
|
|
|
.into();
|
|
|
|
|
|
let shaders = pane_grid::PaneGrid::new(
|
|
|
|
|
|
&scene.model_panes,
|
|
|
|
|
|
move |_pane, &idx, _maximized| {
|
|
|
|
|
|
pane_grid::Content::new(
|
|
|
|
|
|
shader(ViewportPane::for_pane(
|
|
|
|
|
|
scene,
|
|
|
|
|
|
show_viewcube,
|
|
|
|
|
|
render_mode,
|
|
|
|
|
|
idx,
|
|
|
|
|
|
))
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.spacing(crate::scene::TILE_DIVIDER_PX);
|
|
|
|
|
|
stack![size_probe, shaders].width(Fill).height(Fill).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Per-pane input layer: a pane_grid of transparent mouse_areas matching
|
|
|
|
|
|
// the shader pane_grid (same `model_panes` → identical layout). Layered
|
|
|
|
|
|
// above the crosshair overlay so it actually receives mouse events, and
|
|
|
|
|
|
// it owns the divider resize. Only built for the Model layout.
|
|
|
|
|
|
let model_input_layer: Option<Element<'_, Message>> = if is_paper || tab.is_start {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let scene = &tab.scene;
|
|
|
|
|
|
Some(
|
|
|
|
|
|
pane_grid::PaneGrid::new(&scene.model_panes, |_pane, &idx, _maximized| {
|
|
|
|
|
|
pane_grid::Content::new(pane_mouse_area(idx))
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.spacing(crate::scene::TILE_DIVIDER_PX)
|
|
|
|
|
|
.on_resize(6.0, Message::PaneResized)
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let grid_overlay = {
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let model_basis = {
|
|
|
|
|
|
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
(o, (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()))
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
};
|
|
|
|
|
|
let grid: Vec<crate::ui::overlay::GridParams> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.grid_views(vw, vh)
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.map(|(bounds, cam, handle)| {
|
|
|
|
|
|
let (origin, axes): (glam::DVec3, _) = if is_paper {
|
|
|
|
|
|
match tab.ucs_from_viewport(handle) {
|
|
|
|
|
|
Some(u) => {
|
|
|
|
|
|
let (o, ux, uy, uz) =
|
|
|
|
|
|
super::helpers::UcsXform::from_ucs(&u).axes();
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
(o, (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()))
|
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
|
|
|
|
}
|
|
|
|
|
|
None => (
|
|
|
|
|
|
glam::DVec3::ZERO,
|
|
|
|
|
|
(glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z),
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
model_basis
|
|
|
|
|
|
};
|
|
|
|
|
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
|
|
|
|
|
crate::ui::overlay::GridParams {
|
|
|
|
|
|
view_rot: cam.view_proj_rte(bounds),
|
|
|
|
|
|
eye: cam.eye(),
|
|
|
|
|
|
bounds,
|
|
|
|
|
|
plane,
|
|
|
|
|
|
origin,
|
|
|
|
|
|
axes,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
crate::ui::overlay::grid_overlay(grid)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let selection_overlay = {
|
|
|
|
|
|
let sel = tab.scene.selection.borrow().clone();
|
|
|
|
|
|
let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type));
|
2026-07-01 19:39:21 +03:00
|
|
|
|
let snap_ext_base = tab.snap_result.and_then(|s| s.extension_base);
|
2026-07-03 17:46:08 +03:00
|
|
|
|
let snap_ext_base2 = tab.snap_result.and_then(|s| s.extension_base2);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
|
|
|
|
|
|
let grips: Vec<crate::ui::overlay::GripMarker> =
|
|
|
|
|
|
if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() {
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
// Overlays project through the active tile's camera, so
|
|
|
|
|
|
// they must use the active tile's screen rectangle (with
|
|
|
|
|
|
// its canvas offset) — not the whole canvas — or they
|
|
|
|
|
|
// land in the wrong place in a tiled layout.
|
|
|
|
|
|
// Inside a floating viewport the pane is the viewport's own
|
|
|
|
|
|
// rect + camera; otherwise the active model tile.
|
|
|
|
|
|
let edit_frame = tab.scene.viewport_edit_frame((vw, vh));
|
|
|
|
|
|
let bounds = match &edit_frame {
|
|
|
|
|
|
Some((_, full)) => *full,
|
|
|
|
|
|
None => tab.scene.active_model_tile_bounds(vw, vh),
|
|
|
|
|
|
};
|
|
|
|
|
|
let sel_h = tab.selected_handle;
|
2026-07-08 17:53:05 +03:00
|
|
|
|
// The Current Vertex the Properties panel is focused on:
|
|
|
|
|
|
// mark that grip hot so the navigated vertex is visible in
|
|
|
|
|
|
// the drawing. Only for a single selected polyline, whose
|
|
|
|
|
|
// vertex grips are ids 0..n. (Properties vertex stepper)
|
|
|
|
|
|
let current_vertex_grip: Option<usize> = sel_h.and_then(|h| {
|
|
|
|
|
|
matches!(
|
|
|
|
|
|
tab.scene.document.get_entity(h),
|
|
|
|
|
|
Some(acadrust::EntityType::LwPolyline(_))
|
|
|
|
|
|
| Some(acadrust::EntityType::Polyline2D(_))
|
|
|
|
|
|
)
|
|
|
|
|
|
.then_some(tab.properties.prop_vertex)
|
|
|
|
|
|
});
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// In-viewport grips are model-space; project them with the
|
|
|
|
|
|
// viewport camera so they sit on the wire the GPU draws.
|
|
|
|
|
|
// Paper entities use the 2-D paper transform; the model tab
|
|
|
|
|
|
// uses the model camera.
|
|
|
|
|
|
let screen_grips = if let Some((cam, _)) = &edit_frame {
|
|
|
|
|
|
grips_to_screen_rte(
|
|
|
|
|
|
&tab.selected_grips,
|
|
|
|
|
|
cam.view_proj_rte(bounds),
|
|
|
|
|
|
cam.eye(),
|
|
|
|
|
|
bounds,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else if is_paper {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
let aspect = if vh > 0.0 { vw / vh } else { 1.0 };
|
|
|
|
|
|
let half_h = cam.ortho_size();
|
|
|
|
|
|
let half_w = half_h * aspect;
|
|
|
|
|
|
let tx = cam.target.x as f32;
|
|
|
|
|
|
let ty = cam.target.y as f32;
|
|
|
|
|
|
drop(cam);
|
|
|
|
|
|
grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
grips_to_screen(&tab.selected_grips, &cam, bounds)
|
|
|
|
|
|
};
|
|
|
|
|
|
screen_grips
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.filter(|(_, screen, _, _, _)| {
|
|
|
|
|
|
screen.x.is_finite()
|
|
|
|
|
|
&& screen.y.is_finite()
|
|
|
|
|
|
&& screen.x >= -bounds.width
|
|
|
|
|
|
&& screen.x <= bounds.width * 2.0
|
|
|
|
|
|
&& screen.y >= -bounds.height
|
|
|
|
|
|
&& screen.y <= bounds.height * 2.0
|
|
|
|
|
|
})
|
|
|
|
|
|
.map(|(grip_id, screen, _is_midpoint, shape, dir)| {
|
|
|
|
|
|
let is_hot = tab
|
|
|
|
|
|
.active_grip
|
|
|
|
|
|
.as_ref()
|
2026-07-08 17:53:05 +03:00
|
|
|
|
.map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id)
|
|
|
|
|
|
|| Some(grip_id) == current_vertex_grip;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
crate::ui::overlay::GripMarker {
|
|
|
|
|
|
pos: screen,
|
|
|
|
|
|
shape,
|
|
|
|
|
|
is_hot,
|
|
|
|
|
|
dir,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
// Active tile rectangle (canvas-offset included) so grid / UCS
|
|
|
|
|
|
// icon / crosshair project through the active pane's camera at
|
|
|
|
|
|
// the correct place and scale.
|
|
|
|
|
|
let vp_bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
|
|
|
|
|
|
|
|
|
|
|
// The UCS icon shows the active pane's UCS tripod: the model view, or
|
|
|
|
|
|
// (inside a floating viewport) projected through the viewport camera
|
|
|
|
|
|
// at the viewport's rect so it tracks the in-viewport UCS.
|
|
|
|
|
|
// Rotation-only projection (view_proj_rte): the icon shows axis
|
|
|
|
|
|
// DIRECTIONS only, so the full view_proj's huge UTM translation would
|
|
|
|
|
|
// cancel catastrophically in f32 and make the tripod jitter.
|
2026-06-27 11:50:11 +03:00
|
|
|
|
let ucs_icons: Vec<crate::ui::overlay::UcsIconParams> = if !self.show_ucs_icon {
|
|
|
|
|
|
vec![]
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else if let Some((vp_cam, full)) = tab.scene.viewport_edit_frame((vw, vh)) {
|
|
|
|
|
|
let (_, ux, uy, uz) = tab.ucs_xform().axes();
|
2026-06-27 01:12:45 +03:00
|
|
|
|
let origin_screen = self.ucs_icon_at_origin.then(|| {
|
|
|
|
|
|
vp_cam
|
|
|
|
|
|
.project(tab.ucs_origin_world(), full)
|
|
|
|
|
|
.map(|p| iced::Point::new(full.x + p.x, full.y + p.y))
|
|
|
|
|
|
}).flatten();
|
2026-06-27 11:50:11 +03:00
|
|
|
|
vec![crate::ui::overlay::UcsIconParams {
|
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
|
|
|
|
view_proj: vp_cam.view_proj_rte(full),
|
|
|
|
|
|
bounds: full,
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
axes: (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()),
|
2026-06-27 01:12:45 +03:00
|
|
|
|
origin_screen,
|
|
|
|
|
|
hover: self.ucs_icon_hover,
|
|
|
|
|
|
selected: self.ucs_icon_selected,
|
2026-06-27 11:50:11 +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
|
|
|
|
} else if !is_paper {
|
2026-06-27 11:50:11 +03:00
|
|
|
|
// One icon per Model pane — each at its own UCS origin, projected
|
|
|
|
|
|
// through that pane's camera at its pane rect. Only the active
|
|
|
|
|
|
// pane carries the interactive hover / selected grips.
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let (_, ux, uy, uz) = tab.ucs_xform().axes();
|
2026-06-27 11:50:11 +03:00
|
|
|
|
let origin_w = tab.ucs_origin_world();
|
|
|
|
|
|
let active = tab.scene.active_model_tile.get();
|
|
|
|
|
|
let live = tab.scene.camera.borrow().clone();
|
|
|
|
|
|
tab.scene
|
|
|
|
|
|
.model_tiles
|
|
|
|
|
|
.borrow()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.enumerate()
|
|
|
|
|
|
.map(|(i, t)| {
|
|
|
|
|
|
let b = iced::Rectangle {
|
|
|
|
|
|
x: t.rect.x * vw,
|
|
|
|
|
|
y: t.rect.y * vh,
|
|
|
|
|
|
width: (t.rect.width * vw).max(1.0),
|
|
|
|
|
|
height: (t.rect.height * vh).max(1.0),
|
|
|
|
|
|
};
|
|
|
|
|
|
let cam = if i == active { live.clone() } else { t.camera.clone() };
|
|
|
|
|
|
let origin_screen = self.ucs_icon_at_origin.then(|| {
|
|
|
|
|
|
cam.project(origin_w, b)
|
|
|
|
|
|
.map(|p| iced::Point::new(b.x + p.x, b.y + p.y))
|
|
|
|
|
|
}).flatten();
|
|
|
|
|
|
crate::ui::overlay::UcsIconParams {
|
|
|
|
|
|
view_proj: cam.view_proj_rte(b),
|
|
|
|
|
|
bounds: b,
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
axes: (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()),
|
2026-06-27 11:50:11 +03:00
|
|
|
|
origin_screen,
|
|
|
|
|
|
hover: i == active && self.ucs_icon_hover,
|
|
|
|
|
|
selected: i == active && self.ucs_icon_selected,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.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
|
|
|
|
} else {
|
2026-06-27 11:50:11 +03:00
|
|
|
|
vec![]
|
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
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// OST tracking points → screen positions, projected relative-to-eye
|
|
|
|
|
|
// so they stay precise at UTM-scale coordinates (the full
|
2026-07-03 20:34:20 +03:00
|
|
|
|
// view-projection cancels catastrophically in f32). Must project
|
|
|
|
|
|
// through the active pane's camera at its rect (canvas offset
|
|
|
|
|
|
// included) — like the grips and UCS icon above — or the marker
|
|
|
|
|
|
// lands in the wrong place (tiled layout) or off-screen (floating
|
|
|
|
|
|
// viewport). Without the `ob.x/ob.y` offset it silently vanishes.
|
|
|
|
|
|
// Shared OTRACK projection basis: the active pane's camera + rect
|
|
|
|
|
|
// (canvas offset included), matching the grips / UCS icon above.
|
|
|
|
|
|
let otrack_proj: Option<(glam::Mat4, glam::DVec3, iced::Rectangle)> =
|
2026-07-05 13:24:43 +03:00
|
|
|
|
if self.snapper.alignment_active() {
|
2026-07-03 20:34:20 +03:00
|
|
|
|
Some(
|
|
|
|
|
|
if let Some((vp_cam, full)) = tab.scene.viewport_edit_frame((vw, vh)) {
|
|
|
|
|
|
(vp_cam.view_proj_rte(full), vp_cam.eye(), full)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
(cam.view_proj_rte(vp_bounds), cam.eye(), vp_bounds)
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let ost_project =
|
|
|
|
|
|
|w: glam::Vec3, view_rot: glam::Mat4, eye: glam::DVec3, ob: iced::Rectangle| {
|
|
|
|
|
|
let ndc = view_rot.project_point3((w.as_dvec3() - eye).as_vec3());
|
|
|
|
|
|
iced::Point::new(
|
|
|
|
|
|
ob.x + (ndc.x + 1.0) * 0.5 * ob.width,
|
|
|
|
|
|
ob.y + (1.0 - ndc.y) * 0.5 * ob.height,
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
let ost_points: Vec<crate::ui::overlay::OstTrackPoint> =
|
|
|
|
|
|
if let Some((view_rot, eye, ob)) = otrack_proj {
|
|
|
|
|
|
self.snapper
|
|
|
|
|
|
.tracking_points
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter_map(|&wp| {
|
|
|
|
|
|
let s = ost_project(wp, view_rot, eye, ob);
|
|
|
|
|
|
(s.x.is_finite() && s.y.is_finite())
|
|
|
|
|
|
.then_some(crate::ui::overlay::OstTrackPoint { screen: s })
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
};
|
|
|
|
|
|
// The active alignment vector: a dashed guide from the acquired
|
|
|
|
|
|
// tracking point through the locked cursor, so the user sees the
|
|
|
|
|
|
// extension / tracking line they are snapped to (#219).
|
|
|
|
|
|
let otrack_line: Option<(iced::Point, iced::Point)> =
|
|
|
|
|
|
match (otrack_proj, self.otrack_active) {
|
|
|
|
|
|
(Some((view_rot, eye, ob)), Some((base, _dir))) => {
|
|
|
|
|
|
let b = ost_project(base, view_rot, eye, ob);
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let a = ost_project(tab.last_cursor_world.as_vec3(), view_rot, eye, ob);
|
2026-07-03 20:34:20 +03:00
|
|
|
|
(b.x.is_finite() && a.x.is_finite()).then_some((b, a))
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
};
|
2026-07-05 14:50:33 +03:00
|
|
|
|
// The acquired Parallel-snap reference, marked on its line (#277).
|
|
|
|
|
|
let parallel_ref_marker: Option<iced::Point> =
|
|
|
|
|
|
match (otrack_proj, self.snapper.parallel_ref) {
|
|
|
|
|
|
(Some((view_rot, eye, ob)), Some((_, pt))) => {
|
|
|
|
|
|
let s = ost_project(pt, view_rot, eye, ob);
|
|
|
|
|
|
(s.x.is_finite() && s.y.is_finite()).then_some(s)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
|
2026-06-27 11:50:11 +03:00
|
|
|
|
// Model-space pane dividers (none in paper / single-pane layouts).
|
|
|
|
|
|
let dividers = if !is_paper {
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
tab.scene.model_pane_dividers(vw, vh)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-27 11:50:11 +03:00
|
|
|
|
// Pane move (drag-to-swap) visuals: source pane rect + the drop
|
|
|
|
|
|
// target pane under the cursor.
|
|
|
|
|
|
let (pane_move_rect, pane_drop_rect) = match self.pane_move_from {
|
|
|
|
|
|
Some(from) if !is_paper => {
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let cursor = tab.scene.selection.borrow().last_move_pos;
|
|
|
|
|
|
let tiles = tab.scene.model_tiles.borrow();
|
|
|
|
|
|
let px = |t: &crate::scene::ModelTile| iced::Rectangle {
|
|
|
|
|
|
x: t.rect.x * vw,
|
|
|
|
|
|
y: t.rect.y * vh,
|
|
|
|
|
|
width: t.rect.width * vw,
|
|
|
|
|
|
height: t.rect.height * vh,
|
|
|
|
|
|
};
|
|
|
|
|
|
let src = tiles.get(from).map(px);
|
|
|
|
|
|
let drop = cursor.and_then(|c| {
|
|
|
|
|
|
tiles.iter().enumerate().find(|(i, t)| {
|
|
|
|
|
|
*i != from
|
|
|
|
|
|
&& c.x >= t.rect.x * vw
|
|
|
|
|
|
&& c.x < (t.rect.x + t.rect.width) * vw
|
|
|
|
|
|
&& c.y >= t.rect.y * vh
|
|
|
|
|
|
&& c.y < (t.rect.y + t.rect.height) * vh
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
(src, drop.map(|(_, t)| px(t)))
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => (None, None),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-01 13:10:39 +03:00
|
|
|
|
let hover_locked = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.hover_highlight
|
|
|
|
|
|
.map(|h| tab.scene.is_layer_locked(h))
|
|
|
|
|
|
.unwrap_or(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
|
|
|
|
crate::ui::overlay::selection_overlay(
|
|
|
|
|
|
sel,
|
|
|
|
|
|
snap_info,
|
2026-07-01 19:39:21 +03:00
|
|
|
|
snap_ext_base,
|
2026-07-03 17:46:08 +03:00
|
|
|
|
snap_ext_base2,
|
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
|
|
|
|
grips,
|
2026-06-27 11:50:11 +03:00
|
|
|
|
ucs_icons,
|
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
|
|
|
|
ost_points,
|
2026-07-03 20:34:20 +03:00
|
|
|
|
otrack_line,
|
2026-07-05 14:50:33 +03:00
|
|
|
|
parallel_ref_marker,
|
2026-07-10 14:58:48 +03:00
|
|
|
|
// ViewCube hover region matches the drawn cube — gone when hidden.
|
|
|
|
|
|
!is_paper && viewcube_visible,
|
2026-06-27 11:50:11 +03:00
|
|
|
|
dividers,
|
|
|
|
|
|
pane_move_rect,
|
|
|
|
|
|
pane_drop_rect,
|
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
|
|
|
|
tab.pan_mode,
|
2026-07-01 00:45:23 +03:00
|
|
|
|
self.ribbon.open_dropdown.is_some(),
|
2026-07-01 13:10:39 +03:00
|
|
|
|
hover_locked,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let viewport_mouse = mouse_area(container(
|
|
|
|
|
|
iced::widget::Space::new().width(Fill).height(Fill),
|
|
|
|
|
|
))
|
|
|
|
|
|
.on_move(Message::ViewportMove)
|
|
|
|
|
|
.on_press(Message::ViewportLeftPress)
|
|
|
|
|
|
.on_release(Message::ViewportLeftRelease)
|
|
|
|
|
|
.on_right_press(Message::ViewportRightPress)
|
|
|
|
|
|
.on_right_release(Message::ViewportRightRelease)
|
|
|
|
|
|
.on_middle_press(Message::ViewportMiddlePress)
|
|
|
|
|
|
.on_middle_release(Message::ViewportMiddleRelease)
|
|
|
|
|
|
.on_scroll(Message::ViewportScroll)
|
|
|
|
|
|
.on_exit(Message::ViewportExit);
|
|
|
|
|
|
|
|
|
|
|
|
let bg_color = if is_paper {
|
|
|
|
|
|
// Desk color — matches the DESK constant in paper_canvas.rs.
|
|
|
|
|
|
Color {
|
|
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.24,
|
|
|
|
|
|
b: 0.28,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tab.bg_color
|
|
|
|
|
|
.map(|[r, g, b, a]| Color { r, g, b, a })
|
|
|
|
|
|
.unwrap_or(Color {
|
2026-06-27 11:50:11 +03:00
|
|
|
|
// Default model background: RGB (33, 40, 48).
|
|
|
|
|
|
r: 33.0 / 255.0,
|
|
|
|
|
|
g: 40.0 / 255.0,
|
|
|
|
|
|
b: 48.0 / 255.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
|
|
|
|
a: 1.0,
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Dynamic input overlay — editable boxes near the cursor, one per
|
|
|
|
|
|
// quantity the active command is asking for (X/Y, or polar
|
|
|
|
|
|
// distance+angle, or a single distance/angle). TAB moves focus
|
|
|
|
|
|
// between boxes; typing locks a box to a fixed value while the
|
|
|
|
|
|
// rest keep tracking the cursor. The field set is maintained in
|
|
|
|
|
|
// `tab.dyn_fields` by `sync_dyn_fields`.
|
|
|
|
|
|
// A pick step (object selection) has no input box, but still shows
|
|
|
|
|
|
// its prompt ("Select first object …") near the cursor as a hint.
|
|
|
|
|
|
let dyn_picks_object = tab
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|c| c.needs_entity_pick() || c.needs_structure_point_pick())
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let dyn_input_overlay: Option<Element<'_, Message>> =
|
|
|
|
|
|
if self.dyn_input
|
|
|
|
|
|
&& tab.active_cmd.is_some()
|
|
|
|
|
|
&& (!tab.dyn_fields.is_empty() || dyn_picks_object)
|
|
|
|
|
|
{
|
|
|
|
|
|
let w = tab.last_cursor_world;
|
|
|
|
|
|
let base = self.last_point;
|
|
|
|
|
|
// A command may drive a typed scalar by mouse (e.g. a
|
|
|
|
|
|
// perpendicular distance to a picked object); show that live
|
|
|
|
|
|
// value in the box until the user types over it.
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let live = tab.active_cmd.as_ref().and_then(|c| c.dyn_live_value(w));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let boxes: Vec<crate::ui::overlay::DynBox> = tab
|
|
|
|
|
|
.dyn_fields
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.enumerate()
|
|
|
|
|
|
.map(|(idx, f)| {
|
|
|
|
|
|
let value = match (&f.buffer, live) {
|
|
|
|
|
|
(Some(b), _) => b.clone(),
|
|
|
|
|
|
// An angle step with a command-supplied live value
|
|
|
|
|
|
// (ARC span / direction) shows it in degrees.
|
|
|
|
|
|
(None, Some(lv)) if f.component == DynComponent::Angle => {
|
|
|
|
|
|
format!("{lv:.1}")
|
|
|
|
|
|
}
|
|
|
|
|
|
(None, Some(lv))
|
|
|
|
|
|
if matches!(
|
|
|
|
|
|
f.component,
|
|
|
|
|
|
DynComponent::Scalar | DynComponent::Distance
|
|
|
|
|
|
) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
format!("{lv:.4}")
|
|
|
|
|
|
}
|
2026-07-04 22:27:59 +03:00
|
|
|
|
_ => dyn_component_value(
|
|
|
|
|
|
f,
|
|
|
|
|
|
w,
|
|
|
|
|
|
base,
|
|
|
|
|
|
&tab.ucs_xform(),
|
|
|
|
|
|
self.dyn_user_reshaped,
|
|
|
|
|
|
),
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
};
|
|
|
|
|
|
crate::ui::overlay::DynBox {
|
|
|
|
|
|
label: f.role.label().to_string(),
|
|
|
|
|
|
value,
|
|
|
|
|
|
active: idx == tab.dyn_active,
|
|
|
|
|
|
locked: f.locked(),
|
|
|
|
|
|
role: f.role,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let prompt = tab
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|c| c.prompt())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
Some(crate::ui::overlay::dynamic_input_overlay(
|
|
|
|
|
|
tab.last_cursor_screen,
|
|
|
|
|
|
tab.last_point_screen,
|
|
|
|
|
|
tab.dyn_ref_screen,
|
|
|
|
|
|
tab.dyn_guide,
|
|
|
|
|
|
boxes,
|
|
|
|
|
|
prompt,
|
|
|
|
|
|
))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let mut viewport_stack = if tab.is_start {
|
|
|
|
|
|
// Start tab: only the welcome widget over a flat background.
|
|
|
|
|
|
// Skip every drawing-only overlay (selection markers, snap info,
|
|
|
|
|
|
// mouse-area capturing draw clicks, viewcube, nav toolbar, …).
|
|
|
|
|
|
stack![container(viewport_3d)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(bg_color)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
} else if is_paper {
|
|
|
|
|
|
// Paper layout: the GPU shader renders everything — the desk is the
|
|
|
|
|
|
// container background, the white sheet + paper entities + borders
|
|
|
|
|
|
// come from the full-canvas top-locked "sheet" viewport, and the
|
|
|
|
|
|
// floating content viewports overlay it (same path as model space).
|
|
|
|
|
|
const DESK: Color = Color {
|
|
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.24,
|
|
|
|
|
|
b: 0.28,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
stack![
|
|
|
|
|
|
container(grid_overlay)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(DESK)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
viewport_3d,
|
|
|
|
|
|
selection_overlay,
|
|
|
|
|
|
viewport_mouse,
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
stack![
|
|
|
|
|
|
container(grid_overlay)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(bg_color)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
viewport_3d,
|
|
|
|
|
|
selection_overlay,
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-27 11:50:11 +03:00
|
|
|
|
// Per-pane input pane_grid goes ABOVE the crosshair overlay so it
|
|
|
|
|
|
// receives mouse events (the overlay's `Hidden` cursor would otherwise
|
|
|
|
|
|
// starve any layer beneath it). The controls bar is pushed on top of it.
|
|
|
|
|
|
if let Some(input) = model_input_layer {
|
|
|
|
|
|
viewport_stack = viewport_stack.push(input);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Model-space render-mode picker, top-left. Sits ABOVE the
|
|
|
|
|
|
// viewport mouse_area so clicks inside its bounds reach it
|
|
|
|
|
|
// instead of the shader behind it; `opaque` stops them bubbling
|
|
|
|
|
|
// further. Outside the chip the Fill container is transparent so
|
|
|
|
|
|
// viewport drawing / selection is unaffected. In a paper layout
|
|
|
|
|
|
// the active viewport gets its own picker (below) instead.
|
|
|
|
|
|
if !is_paper && !tab.is_start {
|
2026-07-10 14:58:48 +03:00
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let rect = tab.scene.active_model_tile_bounds(vw, vh);
|
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
|
|
|
|
// Unified control chip: split buttons + render-mode picker +
|
|
|
|
|
|
// grid / grid-snap toggles, for the active Model tile.
|
|
|
|
|
|
let bar = viewport_controls(
|
|
|
|
|
|
tab.render_mode,
|
|
|
|
|
|
self.show_grid,
|
|
|
|
|
|
self.snapper.grid_snap(),
|
|
|
|
|
|
true,
|
|
|
|
|
|
tab.scene.model_tiles.borrow().len(),
|
|
|
|
|
|
);
|
2026-07-10 14:58:48 +03:00
|
|
|
|
// Adaptive: DensitySwap measures the bar's real width every frame
|
|
|
|
|
|
// (reported into `render_bar_w`, which the ViewCube reads to decide
|
|
|
|
|
|
// overlap) and swaps it for an empty spacer only when it no longer
|
|
|
|
|
|
// fits the tile. The fixed-width container bounds that fit decision
|
|
|
|
|
|
// to the tile, not the whole canvas.
|
|
|
|
|
|
let adaptive: Element<'_, Message> = DensitySwap::new(vec![
|
|
|
|
|
|
iced::widget::opaque(bar),
|
|
|
|
|
|
Space::new()
|
|
|
|
|
|
.width(iced::Length::Fixed(0.0))
|
|
|
|
|
|
.height(iced::Length::Fixed(0.0))
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
])
|
|
|
|
|
|
.report_width0(self.render_bar_w.clone())
|
|
|
|
|
|
.into();
|
|
|
|
|
|
// Position the bar at the active model tile's top-left corner so it
|
|
|
|
|
|
// follows the active panel in a tiled layout (full canvas when a
|
|
|
|
|
|
// single tile fills the window). Leading Spaces offset it.
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let bar_layer = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(rect.y.max(0.0))),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(rect.x.max(0.0))),
|
2026-07-10 14:58:48 +03:00
|
|
|
|
container(adaptive).width(iced::Length::Fixed(rect.width.max(1.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
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(bar_layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Active paper-space viewport overlays: a render-mode picker in
|
|
|
|
|
|
// its top-left corner and a ViewCube hit area in its top-right,
|
|
|
|
|
|
// both layered ABOVE the viewport mouse_area so they receive
|
|
|
|
|
|
// clicks (the shader viewport sits below it). Positioned with
|
|
|
|
|
|
// leading Spaces sized to the viewport's screen rectangle.
|
|
|
|
|
|
let active_vp_rect: Option<iced::Rectangle> = if is_paper && !tab.is_start {
|
|
|
|
|
|
tab.scene.active_viewport.and_then(|h| {
|
|
|
|
|
|
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
tab.scene.viewport_screen_rect(h, (cw, ch))
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(rect) = active_vp_rect {
|
|
|
|
|
|
// Clip the outline to the visible canvas. Clamping only the origin
|
|
|
|
|
|
// (max(0.0)) while keeping the full width/height shifted the whole
|
|
|
|
|
|
// outline inward when the viewport ran off the top/left edge, so
|
|
|
|
|
|
// its drawn border no longer matched the real viewport — clicks
|
|
|
|
|
|
// that looked outside landed in (and activated) another viewport.
|
|
|
|
|
|
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let x = rect.x.max(0.0);
|
|
|
|
|
|
let y = rect.y.max(0.0);
|
|
|
|
|
|
let vw = ((rect.x + rect.width).min(cw) - x).max(1.0);
|
|
|
|
|
|
let vh = ((rect.y + rect.height).min(ch) - y).max(1.0);
|
|
|
|
|
|
// Highlight the active viewport with a 2-px border so its
|
|
|
|
|
|
// boundary is always visible over the GPU shader.
|
|
|
|
|
|
const VP_BORDER: Color = Color {
|
|
|
|
|
|
r: 0.18,
|
|
|
|
|
|
g: 0.52,
|
|
|
|
|
|
b: 0.95,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
let border_frame = container(
|
|
|
|
|
|
Space::new()
|
|
|
|
|
|
.width(iced::Length::Fixed(vw))
|
|
|
|
|
|
.height(iced::Length::Fixed(vh)),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
border: iced::Border {
|
|
|
|
|
|
color: VP_BORDER,
|
|
|
|
|
|
width: 2.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
let border_layer = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(y)),
|
|
|
|
|
|
row![Space::new().width(iced::Length::Fixed(x)), border_frame,],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(border_layer);
|
|
|
|
|
|
|
|
|
|
|
|
let vp_mode = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.active_viewport_render_mode()
|
|
|
|
|
|
.unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D);
|
2026-07-10 14:58:48 +03:00
|
|
|
|
// Adaptive (same as model): the picker measures its real width into
|
|
|
|
|
|
// `render_bar_w` and swaps to an empty spacer only when the viewport
|
|
|
|
|
|
// can't hold it; the ViewCube reads that width to decide overlap.
|
|
|
|
|
|
let bar = viewport_controls(
|
|
|
|
|
|
vp_mode,
|
|
|
|
|
|
self.show_grid,
|
|
|
|
|
|
self.snapper.grid_snap(),
|
|
|
|
|
|
false,
|
|
|
|
|
|
0,
|
|
|
|
|
|
);
|
|
|
|
|
|
let adaptive: Element<'_, Message> = DensitySwap::new(vec![
|
|
|
|
|
|
iced::widget::opaque(bar),
|
|
|
|
|
|
Space::new()
|
|
|
|
|
|
.width(iced::Length::Fixed(0.0))
|
|
|
|
|
|
.height(iced::Length::Fixed(0.0))
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
])
|
|
|
|
|
|
.report_width0(self.render_bar_w.clone())
|
|
|
|
|
|
.into();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let picker_layer = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(y + 4.0)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(x + 4.0)),
|
2026-07-10 14:58:48 +03:00
|
|
|
|
container(adaptive).width(iced::Length::Fixed(rect.width.max(1.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
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(picker_layer);
|
|
|
|
|
|
|
2026-07-10 14:58:48 +03:00
|
|
|
|
// Hide the ViewCube first — before the render bar — when they collide.
|
|
|
|
|
|
if viewcube_visible {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
|
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
|
|
|
|
|
|
|
let controls = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(cube_y)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(cube_x)),
|
|
|
|
|
|
viewcube_nav_controls(),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(controls);
|
|
|
|
|
|
|
|
|
|
|
|
let ucs_current = tab
|
|
|
|
|
|
.active_ucs
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|u| u.name.clone())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
let ucs_names: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.ucss
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|u| u.name.clone())
|
|
|
|
|
|
.filter(|n| !n.is_empty())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let picker = column![
|
2026-06-27 01:56:17 +03:00
|
|
|
|
Space::new().height(iced::Length::Fixed(cube_y + VIEWCUBE_HIT_SIZE + 6.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
|
|
|
|
row![
|
|
|
|
|
|
Space::new()
|
2026-06-27 01:56:17 +03:00
|
|
|
|
.width(iced::Length::Fixed(cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5)),
|
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
|
|
|
|
iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(picker);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 14:58:48 +03:00
|
|
|
|
if viewcube_visible && !is_paper {
|
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
|
|
|
|
// Place the ViewCube hit area in the active model tile's top-right
|
|
|
|
|
|
// corner so it tracks the active panel in a tiled layout. The hit
|
|
|
|
|
|
// test in update.rs already maps clicks through the active tile.
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let rect = tab.scene.active_model_tile_bounds(vw, vh);
|
|
|
|
|
|
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
|
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
|
|
|
|
|
|
|
// Cube hit area + nav controls (home / roll / nudge) as one layer.
|
|
|
|
|
|
let controls = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(cube_y)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(cube_x)),
|
|
|
|
|
|
viewcube_nav_controls(),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(controls);
|
|
|
|
|
|
|
|
|
|
|
|
// WCS / named-UCS selector under the cube.
|
|
|
|
|
|
let ucs_current = tab
|
|
|
|
|
|
.active_ucs
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|u| u.name.clone())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
let ucs_names: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.ucss
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|u| u.name.clone())
|
|
|
|
|
|
.filter(|n| !n.is_empty())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let picker = column![
|
2026-06-27 01:56:17 +03:00
|
|
|
|
Space::new().height(iced::Length::Fixed(cube_y + VIEWCUBE_HIT_SIZE + 6.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
|
|
|
|
row![
|
2026-06-27 01:56:17 +03:00
|
|
|
|
Space::new().width(iced::Length::Fixed(cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5)),
|
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
|
|
|
|
iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(picker);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(dyn_ol) = dyn_input_overlay {
|
|
|
|
|
|
if !tab.is_start {
|
|
|
|
|
|
viewport_stack = viewport_stack.push(dyn_ol);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Multi-functional grip popup (Phase 2). One bordered container
|
|
|
|
|
|
// wraps a column of borderless item buttons so the popup reads
|
|
|
|
|
|
// as a single widget instead of stacked tiles.
|
|
|
|
|
|
if let Some(popup) = self.grip_popup.as_ref() {
|
|
|
|
|
|
if !tab.is_start {
|
|
|
|
|
|
// Size the row to the widest label so the selection
|
|
|
|
|
|
// highlight fills the whole row instead of just the
|
|
|
|
|
|
// text glyphs. ~7 px per character at size 12 + the
|
|
|
|
|
|
// horizontal padding (10 + 10).
|
|
|
|
|
|
let max_len = popup
|
|
|
|
|
|
.items
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|i| i.label.chars().count())
|
|
|
|
|
|
.max()
|
|
|
|
|
|
.unwrap_or(8) as f32;
|
|
|
|
|
|
let row_w = max_len * 7.0 + 24.0;
|
|
|
|
|
|
let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w));
|
|
|
|
|
|
for (idx, item) in popup.items.iter().enumerate() {
|
|
|
|
|
|
let is_sel = idx == popup.selected;
|
|
|
|
|
|
let label = item.label;
|
|
|
|
|
|
let btn = button(text(label).size(12).color(Color::WHITE))
|
|
|
|
|
|
.on_press(Message::GripMenuPick(idx))
|
|
|
|
|
|
.padding([3, 10])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.style(move |_: &Theme, status| iced::widget::button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match (is_sel, status) {
|
|
|
|
|
|
(true, _) => Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.45,
|
|
|
|
|
|
b: 0.95,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
(_, iced::widget::button::Status::Hovered) => Color {
|
|
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.22,
|
|
|
|
|
|
b: 0.22,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
col = col.push(btn);
|
|
|
|
|
|
}
|
|
|
|
|
|
let menu_panel = container(col)
|
|
|
|
|
|
.padding(2)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
|
r: 0.10,
|
|
|
|
|
|
g: 0.10,
|
|
|
|
|
|
b: 0.10,
|
|
|
|
|
|
a: 0.95,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.40,
|
|
|
|
|
|
g: 0.40,
|
|
|
|
|
|
b: 0.40,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
// Offset the menu by 12 px so the cursor doesn't land on
|
|
|
|
|
|
// the first item immediately, matching the right-click
|
|
|
|
|
|
// context menu's "panel below the click point" feel.
|
|
|
|
|
|
let anchor = iced::Point::new(popup.anchor.x + 12.0, popup.anchor.y + 12.0);
|
|
|
|
|
|
viewport_stack =
|
|
|
|
|
|
viewport_stack.push(position_canvas_overlay(anchor, menu_panel.into()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Dynamic-block visibility-state dropdown.
|
|
|
|
|
|
if let Some(popup) = self.visibility_popup.as_ref() {
|
|
|
|
|
|
if !tab.is_start {
|
|
|
|
|
|
let max_len = popup
|
|
|
|
|
|
.items
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.chars().count())
|
|
|
|
|
|
.max()
|
|
|
|
|
|
.unwrap_or(4) as f32;
|
|
|
|
|
|
// +2 chars for the leading "✓ " / " " marker column.
|
|
|
|
|
|
let row_w = (max_len + 2.0) * 7.0 + 24.0;
|
|
|
|
|
|
let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w));
|
|
|
|
|
|
for (idx, name) in popup.items.iter().enumerate() {
|
|
|
|
|
|
let is_cur = popup.current == Some(idx);
|
|
|
|
|
|
let mark: Element<'_, Message> = if is_cur {
|
|
|
|
|
|
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, Color::WHITE)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Space::new().width(11).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
let btn = button(
|
|
|
|
|
|
row![
|
|
|
|
|
|
container(mark).width(16),
|
|
|
|
|
|
text(name).size(12).color(Color::WHITE),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(2)
|
|
|
|
|
|
.align_y(iced::Center),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::VisibilityPick(idx))
|
|
|
|
|
|
.padding([3, 10])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.style(move |_: &Theme, status| iced::widget::button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
iced::widget::button::Status::Hovered => Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.45,
|
|
|
|
|
|
b: 0.95,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
col = col.push(btn);
|
|
|
|
|
|
}
|
|
|
|
|
|
let panel = container(iced::widget::scrollable(col).height(iced::Length::Shrink))
|
|
|
|
|
|
.max_height(360.0)
|
|
|
|
|
|
.padding(2)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
|
r: 0.10,
|
|
|
|
|
|
g: 0.10,
|
|
|
|
|
|
b: 0.10,
|
|
|
|
|
|
a: 0.95,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.40,
|
|
|
|
|
|
g: 0.40,
|
|
|
|
|
|
b: 0.40,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
let anchor = iced::Point::new(popup.anchor.x + 12.0, popup.anchor.y + 12.0);
|
|
|
|
|
|
viewport_stack =
|
|
|
|
|
|
viewport_stack.push(position_canvas_overlay(anchor, panel.into()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Paper-space context actions: a right-edge vertical toolbar
|
|
|
|
|
|
// (viewport / page setup / plot) instead of a contextual ribbon tab.
|
|
|
|
|
|
if is_paper && !tab.is_start {
|
|
|
|
|
|
if let Some(tb) = crate::ui::side_toolbar::view(
|
|
|
|
|
|
&crate::modules::layout::paper_space_tools(),
|
|
|
|
|
|
) {
|
|
|
|
|
|
viewport_stack = viewport_stack.push(tb);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// In-place block edit (REFEDIT): right-edge toolbar with Save / Discard
|
|
|
|
|
|
// so the edit can be finished by clicking. (#136)
|
|
|
|
|
|
if tab.refedit_session.is_some() && !tab.is_start {
|
|
|
|
|
|
if let Some(tb) = crate::ui::side_toolbar::view(
|
|
|
|
|
|
&crate::modules::draw::modify::refedit::refedit_tools(),
|
|
|
|
|
|
) {
|
|
|
|
|
|
viewport_stack = viewport_stack.push(tb);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 13:03:04 +03:00
|
|
|
|
// BEDIT block editor: right-edge Save Block / Discard toolbar (#261).
|
|
|
|
|
|
if tab.block_edit.is_some() && !tab.is_start {
|
|
|
|
|
|
if let Some(tb) = crate::ui::side_toolbar::view(
|
|
|
|
|
|
&crate::modules::draw::modify::block_edit::block_edit_tools(),
|
|
|
|
|
|
) {
|
|
|
|
|
|
viewport_stack = viewport_stack.push(tb);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// Quick Properties: compact floating property panel on selection,
|
|
|
|
|
|
// anchored at the canvas top-left so it doesn't track the cursor.
|
|
|
|
|
|
if self.quick_properties && !tab.is_start {
|
|
|
|
|
|
if let Some(panel) = tab.properties.quick_view() {
|
|
|
|
|
|
viewport_stack = viewport_stack
|
|
|
|
|
|
.push(position_canvas_overlay(iced::Point::new(12.0, 12.0), panel));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Frame-budget HUD (Phase 5.3): toggle with the PERF command. Shows
|
|
|
|
|
|
// the cost of the most recent wire re-tessellation — the work avoided
|
|
|
|
|
|
// by a warm wire cache — so render-path changes can be compared
|
|
|
|
|
|
// PR-to-PR. Reads ~0 ms while panning/zooming on a hit cache.
|
|
|
|
|
|
if self.perf_hud && !tab.is_start {
|
|
|
|
|
|
let s = &tab.scene;
|
|
|
|
|
|
let label = format!(
|
|
|
|
|
|
"tess {:.1} ms · {} wires · epoch {}",
|
|
|
|
|
|
s.last_tess_ms.get(),
|
|
|
|
|
|
s.last_tess_wires.get(),
|
|
|
|
|
|
s.geometry_epoch,
|
|
|
|
|
|
);
|
|
|
|
|
|
let panel = container(text(label).size(12).color(Color {
|
|
|
|
|
|
r: 0.6,
|
|
|
|
|
|
g: 1.0,
|
|
|
|
|
|
b: 0.6,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
.padding(6)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
|
r: 0.08,
|
|
|
|
|
|
g: 0.08,
|
|
|
|
|
|
b: 0.08,
|
|
|
|
|
|
a: 0.85,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.35,
|
|
|
|
|
|
g: 0.35,
|
|
|
|
|
|
b: 0.35,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
viewport_stack = viewport_stack.push(position_canvas_overlay(
|
|
|
|
|
|
iced::Point::new(12.0, 40.0),
|
|
|
|
|
|
panel.into(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Selection-cycling list box: pick among overlapping objects.
|
|
|
|
|
|
if let Some((pt, cands)) = &self.cycle_candidates {
|
|
|
|
|
|
if !tab.is_start {
|
|
|
|
|
|
let items: Vec<(acadrust::Handle, String)> = cands
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter_map(|&h| {
|
|
|
|
|
|
tab.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.get_entity(h)
|
|
|
|
|
|
.map(|e| (h, crate::entities::traits::entity_type_name(e).to_string()))
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if !items.is_empty() {
|
|
|
|
|
|
viewport_stack = viewport_stack
|
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
|
|
|
|
.push(crate::ui::popup::cycle_popup::cycle_popup_overlay(*pt, items));
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Right-click context menu. Lives inside the viewport stack so
|
|
|
|
|
|
// the cursor position (canvas-relative) anchors the menu under
|
|
|
|
|
|
// the cursor instead of drifting into window-relative space.
|
|
|
|
|
|
if !tab.is_start {
|
|
|
|
|
|
let (ctx_pos, draworder_open) = {
|
|
|
|
|
|
let sel = tab.scene.selection.borrow();
|
|
|
|
|
|
(sel.context_menu, sel.draworder_submenu)
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(p) = ctx_pos {
|
|
|
|
|
|
let has_cmd = tab.active_cmd.is_some();
|
|
|
|
|
|
let has_selection = !tab.scene.selected.is_empty();
|
|
|
|
|
|
let isolation_active = tab.scene.is_isolation_active();
|
|
|
|
|
|
let last_cmds: Vec<String> = self
|
|
|
|
|
|
.command_line
|
2026-06-27 16:16:08 +03:00
|
|
|
|
.recent_commands
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
.iter()
|
|
|
|
|
|
.rev()
|
|
|
|
|
|
.take(3)
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
viewport_stack = viewport_stack.push(viewport_context_menu_overlay(
|
|
|
|
|
|
p,
|
|
|
|
|
|
has_cmd,
|
|
|
|
|
|
has_selection,
|
|
|
|
|
|
isolation_active,
|
|
|
|
|
|
last_cmds,
|
|
|
|
|
|
draworder_open,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// In-place MText editor (toolbar + text area), anchored at the
|
|
|
|
|
|
// insertion-point click.
|
|
|
|
|
|
if !tab.is_start {
|
|
|
|
|
|
let canvas = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
if let Some(ed) = &self.mtext_editor {
|
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
|
.collect();
|
2026-07-19 15:47:09 +03:00
|
|
|
|
viewport_stack = viewport_stack.push(mtext_editor_overlay(
|
|
|
|
|
|
ed,
|
|
|
|
|
|
styles,
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
self.modal_offset,
|
|
|
|
|
|
self.modal_resize,
|
|
|
|
|
|
));
|
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.text_inline {
|
|
|
|
|
|
viewport_stack = viewport_stack.push(text_inline_overlay(ed, canvas));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 20:53:11 +03:00
|
|
|
|
// The Start tab shows its own three-panel page (Recent · Welcome ·
|
|
|
|
|
|
// Supporters) in place of the viewport, so the left properties slot is
|
|
|
|
|
|
// empty there.
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let properties_el: Element<'_, Message> = if tab.is_start {
|
2026-07-05 20:53:11 +03:00
|
|
|
|
Space::new().into()
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else if self.show_properties && !self.clean_screen {
|
2026-07-05 21:02:05 +03:00
|
|
|
|
// On a narrow window the panel collapses to a vertical "Properties"
|
|
|
|
|
|
// bar to free width for the viewport; clicking the bar expands it.
|
|
|
|
|
|
let narrow = self.win_size.0 < 1000.0;
|
|
|
|
|
|
let bar = || collapse_bar("Properties", Message::TogglePropertiesBar);
|
|
|
|
|
|
if narrow && !self.props_expanded {
|
|
|
|
|
|
bar()
|
|
|
|
|
|
} else if narrow {
|
|
|
|
|
|
row![bar(), tab.properties.view()].into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tab.properties.view()
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
Space::new().into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Command-line sits as a bottom-centre overlay on top of the
|
|
|
|
|
|
// viewport stack rather than as a separate row in the main
|
|
|
|
|
|
// column — frees up vertical space when no command is active
|
|
|
|
|
|
// and keeps the input close to where the cursor is drawing.
|
|
|
|
|
|
// Autocomplete shows only when no command is collecting its
|
|
|
|
|
|
// own input (otherwise typed prefixes are coordinates / values).
|
|
|
|
|
|
let allow_autocomplete = tab.active_cmd.is_none();
|
|
|
|
|
|
// Dynamic input captures keystrokes when its fields are showing,
|
|
|
|
|
|
// so the command-line field must release focus / its on_input.
|
|
|
|
|
|
// The MText preview also captures keystrokes (typing edits it), so the
|
|
|
|
|
|
// command line must likewise release its on_input there.
|
|
|
|
|
|
let dyn_capturing =
|
|
|
|
|
|
(self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty())
|
|
|
|
|
|
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
|
|
|
|
|
|| self.text_inline.is_some();
|
|
|
|
|
|
let command_line_overlay =
|
2026-07-01 18:17:46 +03:00
|
|
|
|
iced::widget::container(self.command_line.view(
|
|
|
|
|
|
allow_autocomplete,
|
|
|
|
|
|
dyn_capturing,
|
|
|
|
|
|
&self.history_content,
|
|
|
|
|
|
))
|
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
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.align_x(iced::alignment::Horizontal::Center)
|
|
|
|
|
|
.align_y(iced::alignment::Vertical::Bottom)
|
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
|
top: 0.0,
|
|
|
|
|
|
right: 0.0,
|
|
|
|
|
|
bottom: 2.0,
|
|
|
|
|
|
left: 0.0,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let center_stack = iced::widget::stack![
|
|
|
|
|
|
row![properties_el, viewport_stack].width(Fill).height(Fill),
|
|
|
|
|
|
command_line_overlay,
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
let main_ui = container({
|
|
|
|
|
|
// Clean-screen mode drops the ribbon for a full-canvas view; the
|
|
|
|
|
|
// status bar stays so the mode can be toggled back off.
|
|
|
|
|
|
let mut col = column![];
|
|
|
|
|
|
if !self.clean_screen {
|
|
|
|
|
|
col = col.push(self.ribbon.view(
|
|
|
|
|
|
is_paper,
|
fix(ribbon): highlight follows each tool's mechanism; start-tab dimming (#355)
Tool highlights used to stick: every click set active_tool, but only
interactive commands and a hand-maintained per-modal list ever cleared
it, so one-shots (Cleanup/AUDIT, view actions) and unlisted dialogs
(Customization/ALIASEDIT, CUI, Plugin Manager…) stayed blue forever.
Now the rule is mechanical: state toggles light from state only (the
click highlight drops immediately), interactive commands stay lit
while running, dialog openers stay lit until the modal closes (generic
clear replaces the per-modal list), and one-shots never keep it — the
dispatch site turns the highlight off when nothing stayed running.
On the Start tab, tools whose command the start gate refuses now render
dimmed (grey icon + label) across all button styles, dropdowns and the
quick-access strip; start_allowed() is the single shared authority.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 00:13:48 +03:00
|
|
|
|
self.tabs[self.active_tab].is_start,
|
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[self.active_tab].history.undo_stack.len(),
|
|
|
|
|
|
self.tabs[self.active_tab].history.redo_stack.len(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.show_file_tabs {
|
|
|
|
|
|
col = col.push(doc_tab_bar(&self.tabs, self.active_tab));
|
|
|
|
|
|
}
|
|
|
|
|
|
col.push(center_stack)
|
|
|
|
|
|
.push({
|
|
|
|
|
|
let is_model = tab.scene.current_layout == "Model";
|
|
|
|
|
|
let scale_pill_enabled = is_model
|
|
|
|
|
|
|| tab.scene.active_viewport.is_some()
|
|
|
|
|
|
|| tab.scene.has_selected_viewport();
|
|
|
|
|
|
// The cursor is tracked in local render space; re-add the
|
|
|
|
|
|
// model-space world offset so the readout shows true
|
|
|
|
|
|
// drawing coordinates (paper space carries no offset), then
|
|
|
|
|
|
// report it in the active UCS — the readout follows the
|
|
|
|
|
|
// user's coordinate system, not raw WCS (no-op without UCS).
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let to_readout = |p: glam::DVec3| {
|
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
|
|
|
|
// The readout follows the active pane's UCS — model space
|
|
|
|
|
|
// or inside a floating viewport (no-op without a UCS).
|
|
|
|
|
|
if tab.editing_model_space() {
|
2026-07-10 15:54:57 +03:00
|
|
|
|
tab.ucs_xform().to_ucs(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
|
|
|
|
} else {
|
2026-07-10 15:54:57 +03:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-07-10 15:54:57 +03:00
|
|
|
|
let cursor_coord = to_readout(tab.last_cursor_world);
|
|
|
|
|
|
// The last picked point (same UCS as the cursor) drives the
|
|
|
|
|
|
// static ($COORDS 0) and polar ($COORDS 2) readouts.
|
|
|
|
|
|
let last_coord = self.last_point.map(to_readout);
|
|
|
|
|
|
let coords_mode = tab.scene.document.header.coords_mode;
|
|
|
|
|
|
let picking = tab.active_cmd.is_some();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.status_bar.view(
|
|
|
|
|
|
&self.snapper,
|
|
|
|
|
|
self.snap_popup_open,
|
|
|
|
|
|
self.ortho_mode,
|
|
|
|
|
|
self.polar_mode,
|
|
|
|
|
|
self.polar_increment_deg,
|
2026-07-09 21:35:47 +03:00
|
|
|
|
self.polar_popup_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
|
|
|
|
self.dyn_input,
|
|
|
|
|
|
self.snapper.otrack_enabled,
|
2026-07-14 13:03:04 +03:00
|
|
|
|
{
|
|
|
|
|
|
// In a BEDIT block editor, show the block as an extra
|
|
|
|
|
|
// active space tab alongside Model/layouts. (#261)
|
|
|
|
|
|
let mut names = tab.scene.layout_names();
|
|
|
|
|
|
if let Some(be) = &tab.block_edit {
|
|
|
|
|
|
names.push(be.block_name.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
names
|
|
|
|
|
|
},
|
|
|
|
|
|
tab.block_edit
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|be| be.block_name.clone())
|
|
|
|
|
|
.unwrap_or_else(|| tab.scene.current_layout.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_rename_state.as_ref(),
|
|
|
|
|
|
tab.scene.first_viewport_scale(),
|
|
|
|
|
|
tab.scene.viewport_count(),
|
|
|
|
|
|
tab.scene.active_viewport.is_some(),
|
|
|
|
|
|
self.show_layout_tabs,
|
|
|
|
|
|
tab.scene.annotation_scale,
|
|
|
|
|
|
self.scale_popup_open,
|
|
|
|
|
|
scale_pill_enabled,
|
|
|
|
|
|
tab.scene.document.header.lineweight_display,
|
|
|
|
|
|
cursor_coord,
|
2026-07-10 15:54:57 +03:00
|
|
|
|
coords_mode,
|
|
|
|
|
|
last_coord,
|
|
|
|
|
|
picking,
|
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.clean_screen,
|
|
|
|
|
|
tab.scene.document.header.insertion_units,
|
|
|
|
|
|
self.units_popup_open,
|
|
|
|
|
|
tab.scene.is_isolation_active(),
|
|
|
|
|
|
tab.scene.transparency_display,
|
|
|
|
|
|
self.quick_properties,
|
|
|
|
|
|
tab.scene.selection_filter_active(),
|
|
|
|
|
|
self.selection_cycling,
|
|
|
|
|
|
&self.statusbar_config,
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
})
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
|
r: 0.11,
|
|
|
|
|
|
g: 0.11,
|
|
|
|
|
|
b: 0.11,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
})),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
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
|
|
|
|
let win = self.win_size;
|
|
|
|
|
|
let sb_pill = crate::ui::wrap_bar::dropdown_bounds;
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let snap_layer: Element<'_, Message> = if self.snap_popup_open {
|
fix(ui): open the OSNAP settings popup near its button, not the corner
The snap popup used a hardcoded 4 px right offset, pinning it to the
window's right edge — but the Osnap button sits well to the left, with up
to ten pills after it (Space, Scale, Units, Transparency, Isolate, …).
Anchor it by the summed width of the pills that follow Osnap, honouring
the user's per-pill visibility. iced gives the view no real layout
metrics, so the pill widths are estimated (icon pills fixed, labelled
pills approximate); the popup now opens by the button instead of the
corner.
Closes #248
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:03:11 +03:00
|
|
|
|
crate::ui::popup::snap_popup::snap_popup_overlay(
|
|
|
|
|
|
&self.snapper,
|
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
|
|
|
|
sb_pill(crate::ui::statusbar::SB_OSNAP_ID),
|
|
|
|
|
|
win,
|
fix(ui): open the OSNAP settings popup near its button, not the corner
The snap popup used a hardcoded 4 px right offset, pinning it to the
window's right edge — but the Osnap button sits well to the left, with up
to ten pills after it (Space, Scale, Units, Transparency, Isolate, …).
Anchor it by the summed width of the pills that follow Osnap, honouring
the user's per-pill visibility. iced gives the view no real layout
metrics, so the pill widths are estimated (icon pills fixed, labelled
pills approximate); the popup now opens by the button instead of the
corner.
Closes #248
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:03:11 +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
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let scale_layer: Element<'_, Message> = if self.scale_popup_open {
|
|
|
|
|
|
let is_model = tab.scene.current_layout == "Model";
|
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
|
|
|
|
crate::ui::popup::scale_popup::scale_popup_overlay(
|
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
|
|
|
|
is_model,
|
|
|
|
|
|
tab.scene.annotation_scale,
|
|
|
|
|
|
tab.scene.first_viewport_scale(),
|
|
|
|
|
|
tab.scene.scale_list(),
|
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
|
|
|
|
sb_pill(crate::ui::statusbar::SB_SCALE_ID),
|
|
|
|
|
|
win,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-09 21:35:47 +03:00
|
|
|
|
let polar_layer: Element<'_, Message> = if self.polar_popup_open {
|
|
|
|
|
|
crate::ui::popup::polar_popup::polar_popup_overlay(
|
|
|
|
|
|
self.polar_increment_deg,
|
|
|
|
|
|
&self.polar_custom_input,
|
|
|
|
|
|
sb_pill(crate::ui::statusbar::SB_POLAR_ID),
|
|
|
|
|
|
win,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let statusbar_menu_layer: Element<'_, Message> = if self.statusbar_menu_open {
|
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
|
|
|
|
crate::ui::statusbar::statusbar_menu::statusbar_menu_overlay(
|
|
|
|
|
|
&self.statusbar_config,
|
|
|
|
|
|
sb_pill(crate::ui::statusbar::SB_MENU_ID),
|
|
|
|
|
|
win,
|
|
|
|
|
|
)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-27 14:24:47 +03:00
|
|
|
|
let layout_list_layer: Element<'_, Message> = if self.layout_list_open && !tab.is_start {
|
|
|
|
|
|
crate::ui::statusbar::statusbar_menu::layout_list_overlay(
|
|
|
|
|
|
&tab.scene.layout_names(),
|
|
|
|
|
|
&tab.scene.current_layout,
|
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
|
|
|
|
sb_pill(crate::ui::statusbar::SB_LAYOUTLIST_ID),
|
|
|
|
|
|
win,
|
2026-06-27 14:24:47 +03:00
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let units_layer: Element<'_, Message> = if self.units_popup_open {
|
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
|
|
|
|
crate::ui::popup::units_popup::units_popup_overlay(
|
|
|
|
|
|
tab.scene.document.header.insertion_units,
|
|
|
|
|
|
sb_pill(crate::ui::statusbar::SB_UNITS_ID),
|
|
|
|
|
|
win,
|
|
|
|
|
|
)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let isolate_layer: Element<'_, Message> = if self.isolate_popup_open {
|
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
|
|
|
|
crate::ui::popup::isolate_popup::isolate_popup_overlay(
|
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
|
|
|
|
!tab.scene.selected.is_empty(),
|
|
|
|
|
|
tab.scene.is_isolation_active(),
|
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
|
|
|
|
sb_pill(crate::ui::statusbar::SB_ISOLATE_ID),
|
|
|
|
|
|
win,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let sel_filter_layer: Element<'_, Message> = if self.selection_filter_popup_open {
|
|
|
|
|
|
let types: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.entity_type_names_in_layout()
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
|
.collect();
|
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
|
|
|
|
crate::ui::popup::selection_filter_popup::selection_filter_popup_overlay(
|
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
|
|
|
|
types,
|
|
|
|
|
|
&tab.scene.selection_filter,
|
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
|
|
|
|
sb_pill(crate::ui::statusbar::SB_FILTER_ID),
|
|
|
|
|
|
win,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let dropdown_layer: Element<'_, Message> = self
|
|
|
|
|
|
.ribbon
|
|
|
|
|
|
.dropdown_overlay(
|
|
|
|
|
|
&history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack),
|
|
|
|
|
|
&history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack),
|
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,
|
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
|
|
|
|
)
|
|
|
|
|
|
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into());
|
|
|
|
|
|
|
|
|
|
|
|
let layout_ctx_layer: Element<'_, Message> = if let Some(name) = &self.layout_context_menu {
|
2026-07-21 10:38:17 +03:00
|
|
|
|
layout_context_menu_overlay(name, win)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect {
|
|
|
|
|
|
let types = tab.scene.entity_type_names_in_layout();
|
|
|
|
|
|
let properties = tab.scene.qselect_properties(state.type_filter.as_deref());
|
|
|
|
|
|
qselect_overlay(state, &types, &properties)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let open_progress_layer: Element<'_, Message> = if let Some(p) = &self.opening {
|
refactor: group ui/ statusbar and window subdirs
Continue tidying the flat ui/ root:
- ui/statusbar/: statusbar.rs becomes statusbar/mod.rs (its
crate::ui::statusbar::* paths are preserved) and absorbs
statusbar_config.rs + statusbar_menu.rs.
- ui/window/: the standalone manager/dialog windows move here —
about, layout_manager, page_setup, plugin_manager, shortcuts,
layers, update_notice, open_progress. The crate::ui::LayerPanel
re-export is repointed so callers are unchanged.
Call sites repointed to the new paths; no compat re-exports beyond the
existing LayerPanel one. ui/ root drops from 21 to 10 flat files with
five subdirs (ribbon, style, popup, statusbar, window).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 23:52:26 +03:00
|
|
|
|
crate::ui::window::open_progress::view(p, 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
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let composed = stack![
|
|
|
|
|
|
main_ui,
|
|
|
|
|
|
snap_layer,
|
|
|
|
|
|
scale_layer,
|
2026-07-09 21:35:47 +03:00
|
|
|
|
polar_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
|
|
|
|
statusbar_menu_layer,
|
2026-06-27 14:24:47 +03:00
|
|
|
|
layout_list_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
|
|
|
|
units_layer,
|
|
|
|
|
|
isolate_layer,
|
|
|
|
|
|
sel_filter_layer,
|
|
|
|
|
|
dropdown_layer,
|
|
|
|
|
|
layout_ctx_layer,
|
|
|
|
|
|
qselect_layer,
|
|
|
|
|
|
open_progress_layer,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// ── In-canvas modal dialogs (Plan B) ───────────────────────────────
|
|
|
|
|
|
// Former pop-up windows render as overlays here, so they work on both
|
|
|
|
|
|
// the native (single main window) and web builds.
|
|
|
|
|
|
let base: Element<'_, Message> = match self.modal_content() {
|
|
|
|
|
|
Some(content) => {
|
2026-07-19 15:47:09 +03:00
|
|
|
|
crate::ui::modal::modal(
|
|
|
|
|
|
composed,
|
2026-07-21 23:00:36 +03:00
|
|
|
|
self.modal_title(),
|
|
|
|
|
|
// Content width — outer size minus the frame padding.
|
|
|
|
|
|
self.modal_outer_size().map(|s| s.0 - 20.0).unwrap_or(420.0),
|
2026-07-19 15:47:09 +03:00
|
|
|
|
content,
|
|
|
|
|
|
Message::CloseModal,
|
|
|
|
|
|
self.modal_offset,
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
None => composed.into(),
|
|
|
|
|
|
};
|
|
|
|
|
|
// The colour picker is a nested modal: it stacks over whichever dialog
|
|
|
|
|
|
// (style editor, properties, …) requested it.
|
|
|
|
|
|
if self.color_pick_target.is_some() {
|
|
|
|
|
|
crate::ui::modal::modal(
|
|
|
|
|
|
base,
|
2026-07-21 23:00:36 +03:00
|
|
|
|
"Select Color",
|
|
|
|
|
|
420.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
|
|
|
|
iced::widget::container(crate::ui::color_select::color_grid_window(
|
|
|
|
|
|
Message::ColorWindowPick,
|
|
|
|
|
|
))
|
|
|
|
|
|
.width(iced::Length::Fixed(420.0))
|
|
|
|
|
|
.height(iced::Length::Fixed(470.0)),
|
|
|
|
|
|
Message::CloseColorPicker,
|
|
|
|
|
|
iced::Vector::ZERO,
|
2026-07-19 15:47:09 +03:00
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
base
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Outer pixel size (content + title-bar/padding chrome) of the active
|
|
|
|
|
|
/// modal, used to clamp drag so it cannot be pushed off-screen. Mirrors the
|
|
|
|
|
|
/// `sized(..)` dimensions in [`Self::modal_content`]; keep the two in sync.
|
|
|
|
|
|
/// `None` has no active modal. About (content-sized) uses a safe estimate.
|
|
|
|
|
|
pub(crate) fn modal_outer_size(&self) -> Option<(f32, f32)> {
|
|
|
|
|
|
use super::ModalKind::*;
|
|
|
|
|
|
// Title bar (~26) + spacing (6) + frame padding (10·2) → ~52 vertical;
|
|
|
|
|
|
// frame padding → ~20 horizontal.
|
|
|
|
|
|
const EXTRA_W: f32 = 20.0;
|
|
|
|
|
|
const EXTRA_H: f32 = 52.0;
|
|
|
|
|
|
let (w, h) = match self.active_modal? {
|
|
|
|
|
|
About => (440, 360),
|
|
|
|
|
|
Shortcuts => (720, 520),
|
|
|
|
|
|
PluginManager => (520, 460),
|
|
|
|
|
|
UpdateNotice => (560, 460),
|
|
|
|
|
|
Layers => (900, 360),
|
2026-07-09 12:25:46 +03:00
|
|
|
|
Plot => (760, 540),
|
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
|
|
|
|
LayoutManager => (640, 320),
|
|
|
|
|
|
Plotstyle => (780, 540),
|
|
|
|
|
|
TextStyle => (860, 480),
|
|
|
|
|
|
MlStyle => (620, 420),
|
|
|
|
|
|
TableStyle => (620, 420),
|
|
|
|
|
|
MLeaderStyle => (560, 560),
|
|
|
|
|
|
DimStyle => (720, 560),
|
|
|
|
|
|
AssocPrompt => (440, 210),
|
|
|
|
|
|
Unsaved => (420, 160),
|
2026-06-29 11:47:37 +03:00
|
|
|
|
AecDropWarning => (480, 230),
|
2026-07-11 09:12:36 +03:00
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
SaveDialog => (420, 200),
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
SaveDialog => (420, 150),
|
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
|
|
|
|
PointStyle => (360, 470),
|
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
|
|
|
|
AttributeEditor => (640, 500),
|
2026-07-01 17:23:32 +03:00
|
|
|
|
LayerDeleteWarning => (440, 200),
|
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
|
|
|
|
Aliases => (480, 520),
|
2026-07-13 10:39:14 +03:00
|
|
|
|
ScaleManager => (520, 360),
|
2026-07-13 22:38:29 +03:00
|
|
|
|
AnnoObjectScale => (360, 420),
|
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-19 15:47:09 +03:00
|
|
|
|
// Include the user's corner-resize growth so the drag clamp tracks the
|
|
|
|
|
|
// dialog's actual footprint.
|
|
|
|
|
|
Some((
|
|
|
|
|
|
w as f32 + EXTRA_W + self.modal_resize.x,
|
|
|
|
|
|
h as f32 + EXTRA_H + self.modal_resize.y,
|
|
|
|
|
|
))
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl OpenCADStudio {
|
|
|
|
|
|
pub fn subscription(&self) -> Subscription<Message> {
|
|
|
|
|
|
use iced::event;
|
|
|
|
|
|
// Only request per-frame ticks while something on screen is animating
|
|
|
|
|
|
// (currently just the open-progress indicator). Without this gate the
|
|
|
|
|
|
// app burned 2-3% CPU continuously redrawing an unchanged view.
|
|
|
|
|
|
// See #18.
|
|
|
|
|
|
let needs_frames = self.opening.is_some();
|
|
|
|
|
|
let frames = if needs_frames {
|
|
|
|
|
|
window::frames().map(Message::Tick)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
|
|
|
|
|
// While the command-line overlay is still displaying any
|
|
|
|
|
|
// recently-pushed history entry, re-render every frame so the
|
|
|
|
|
|
// entry disappears at the moment its visible window expires.
|
|
|
|
|
|
// The subscription auto-stops once no entry is fresh enough
|
|
|
|
|
|
// (typically within a few seconds of the last command).
|
|
|
|
|
|
let history_tick = if self.command_line.has_visible_history() {
|
|
|
|
|
|
window::frames().map(Message::Tick)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
|
|
|
|
|
// While the cursor sits over a grip, request animation frames
|
|
|
|
|
|
// so the multi-functional popup opens even when the user keeps
|
|
|
|
|
|
// the mouse perfectly still — `ViewportMove` alone would never
|
|
|
|
|
|
// fire again. Auto-stops once the hover clears or the popup is
|
|
|
|
|
|
// already open.
|
|
|
|
|
|
let grip_dwell = if self.grip_hover.is_some() && self.grip_popup.is_none() {
|
|
|
|
|
|
window::frames().map(|_| Message::GripDwellTick)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
|
|
|
|
|
// While a rollover pick is queued, drive ticks so it fires the
|
|
|
|
|
|
// moment the cursor has been still for the dwell window — without
|
|
|
|
|
|
// this `ViewportMove` alone never re-fires once the user stops.
|
|
|
|
|
|
let hover_dwell = if self.hover_dwell.is_some() {
|
|
|
|
|
|
window::frames().map(|_| Message::HoverDwellTick)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
2026-07-05 09:48:49 +02:00
|
|
|
|
// Interaction LOD: while the view is (or just was) navigating, keep
|
|
|
|
|
|
// requesting frames a little past the settle point. Panning itself is
|
|
|
|
|
|
// driven by input events, but once the cursor stops no event would fire
|
|
|
|
|
|
// the one full-quality frame that re-renders hatches — this tick does,
|
|
|
|
|
|
// then the scene-render cache holds it and the subscription auto-stops.
|
|
|
|
|
|
let nav_settle = if self.tabs[self.active_tab].scene.is_settling() {
|
|
|
|
|
|
window::frames().map(Message::Tick)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::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
|
|
|
|
// Blink the MText preview caret while the editor is open.
|
|
|
|
|
|
let caret_blink = if self.mtext_editor.is_some() {
|
|
|
|
|
|
iced::time::every(std::time::Duration::from_millis(530))
|
|
|
|
|
|
.map(|_| Message::MTextCaretBlink)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
|
|
|
|
|
// Web: poll for per-script fonts that a drawing's text needs but hasn't
|
|
|
|
|
|
// fetched yet. Cheap — `PollWebFonts` is a no-op when nothing is
|
|
|
|
|
|
// pending. Native has system fonts, so no polling. (#141)
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
let web_fonts =
|
|
|
|
|
|
iced::time::every(std::time::Duration::from_millis(300)).map(|_| Message::PollWebFonts);
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
let web_fonts = Subscription::none();
|
2026-06-29 13:17:40 +03:00
|
|
|
|
// Periodic autosave to a `.sv$` recovery file (native only). SAVETIME is
|
|
|
|
|
|
// the interval in minutes; 0 disables it.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
let autosave = if self.savetime_min > 0 {
|
|
|
|
|
|
iced::time::every(std::time::Duration::from_secs(self.savetime_min as u64 * 60))
|
|
|
|
|
|
.map(|_| Message::AutoSave)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
let autosave = Subscription::none();
|
feat(open): open drawings in the running editor
Double-clicking a drawing started a whole second copy of the app. Now it
lands as a tab in the editor already open, and that window comes forward.
The election is the bind itself: every GUI launch tries a deterministic
per-user loopback port, and the OS grants it to exactly one process and
releases it on any death, SIGKILL included. Nothing to reap, no PID to
probe, no stale lock after a crash. The winner serves the port from an
iced subscription; the loser checks it is talking to a matching editor,
hands the paths over and exits. Every surprise — no answer, a stranger on
the port, a timeout — boots a normal window, i.e. the old behaviour: the
feature can degrade, but it cannot lose the file.
The gate in main is positional, after every headless mode has returned.
The plugin runner is this same binary re-spawning itself, so a flag list
would rot the first time a mode is added; a position cannot.
Handing a drawing over that is already open switches to its tab instead
of loading a second copy, comparing resolved paths so a symlink or a
`..` hop is still recognised as the same file.
`opening` is a single slot that a second open would overwrite, and
on_file_opened drops any result arriving once it is clear — so drawings
handed over at once would silently vanish. They queue in pending_opens,
drained from every path that clears the slot, error and cancel included.
Exec becomes %F so one launch takes the whole selection; the CLI takes a
list and boot fans it out through the same door. --new-instance forces a
separate process; --read-only / --script / --new always get their own.
Not reused: the automation server. It dispatches arbitrary commands and
writes arbitrary paths, and its open replaces the active tab's document
in place — publishing that on a port every local process can reach would
be unforced privilege escalation. Only its JSON-lines framing is shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:18:16 +03:00
|
|
|
|
// Drawings handed over by a second launch (single instance). Inert in a
|
|
|
|
|
|
// process that lost the port election, so it costs nothing there.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
let single_instance = crate::io::single_instance::subscribe().map(Message::OpenExternal);
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
let single_instance = Subscription::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
|
|
|
|
iced::Subscription::batch([
|
|
|
|
|
|
frames,
|
|
|
|
|
|
history_tick,
|
|
|
|
|
|
grip_dwell,
|
|
|
|
|
|
hover_dwell,
|
2026-07-05 09:48:49 +02:00
|
|
|
|
nav_settle,
|
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
|
|
|
|
caret_blink,
|
|
|
|
|
|
web_fonts,
|
2026-06-29 13:17:40 +03:00
|
|
|
|
autosave,
|
feat(open): open drawings in the running editor
Double-clicking a drawing started a whole second copy of the app. Now it
lands as a tab in the editor already open, and that window comes forward.
The election is the bind itself: every GUI launch tries a deterministic
per-user loopback port, and the OS grants it to exactly one process and
releases it on any death, SIGKILL included. Nothing to reap, no PID to
probe, no stale lock after a crash. The winner serves the port from an
iced subscription; the loser checks it is talking to a matching editor,
hands the paths over and exits. Every surprise — no answer, a stranger on
the port, a timeout — boots a normal window, i.e. the old behaviour: the
feature can degrade, but it cannot lose the file.
The gate in main is positional, after every headless mode has returned.
The plugin runner is this same binary re-spawning itself, so a flag list
would rot the first time a mode is added; a position cannot.
Handing a drawing over that is already open switches to its tab instead
of loading a second copy, comparing resolved paths so a symlink or a
`..` hop is still recognised as the same file.
`opening` is a single slot that a second open would overwrite, and
on_file_opened drops any result arriving once it is clear — so drawings
handed over at once would silently vanish. They queue in pending_opens,
drained from every path that clears the slot, error and cancel included.
Exec becomes %F so one launch takes the whole selection; the CLI takes a
list and boot fans it out through the same door. --new-instance forces a
separate process; --read-only / --script / --new always get their own.
Not reused: the automation server. It dispatches arbitrary commands and
writes arbitrary paths, and its open replaces the active tab's document
in place — publishing that on a port every local process can reach would
be unforced privilege escalation. Only its JSON-lines framing is shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:18:16 +03:00
|
|
|
|
single_instance,
|
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
|
|
|
|
event::listen_with(|ev, status, win_id| {
|
|
|
|
|
|
use iced::event::Status;
|
|
|
|
|
|
match ev {
|
|
|
|
|
|
iced::Event::Window(window::Event::CloseRequested) => {
|
|
|
|
|
|
Some(Message::WindowCloseRequested(win_id))
|
|
|
|
|
|
}
|
|
|
|
|
|
iced::Event::Window(window::Event::Closed) => {
|
|
|
|
|
|
Some(Message::OsWindowClosed(win_id))
|
|
|
|
|
|
}
|
|
|
|
|
|
iced::Event::Window(window::Event::Resized(sz)) => {
|
|
|
|
|
|
Some(Message::WindowResized(sz.width as f32, sz.height as f32))
|
|
|
|
|
|
}
|
|
|
|
|
|
iced::Event::Keyboard(keyboard::Event::ModifiersChanged(m)) => {
|
2026-07-01 17:44:19 +03:00
|
|
|
|
// `command()` is Cmd on macOS, Ctrl elsewhere — the
|
|
|
|
|
|
// platform multi-select modifier for the layer list.
|
|
|
|
|
|
Some(Message::SetModifiers {
|
|
|
|
|
|
shift: m.shift(),
|
|
|
|
|
|
ctrl: m.command() || m.control(),
|
|
|
|
|
|
})
|
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
|
|
|
|
}
|
|
|
|
|
|
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
|
|
|
|
|
key,
|
2026-07-03 09:52:35 +03:00
|
|
|
|
physical_key,
|
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
|
|
|
|
modifiers,
|
|
|
|
|
|
text,
|
|
|
|
|
|
..
|
|
|
|
|
|
}) => {
|
2026-06-27 09:33:08 -07:00
|
|
|
|
// Platform-aware accelerator modifier: Cmd on macOS,
|
|
|
|
|
|
// Ctrl on Windows/Linux. Using `command()` rather than
|
|
|
|
|
|
// `control()` makes Cmd+C/V/S/Z etc. work on Mac, per
|
|
|
|
|
|
// the platform's keyboard conventions.
|
|
|
|
|
|
let accel = modifiers.command();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let shift = modifiers.shift();
|
|
|
|
|
|
// Any key that produces a printable glyph types it,
|
|
|
|
|
|
// even when its logical key resolves to navigation
|
|
|
|
|
|
// (NumLock-on Numpad8 / Numpad2 arrive as
|
|
|
|
|
|
// ArrowUp / ArrowDown but still carry text "8" /
|
|
|
|
|
|
// "2"). Checked before the Arrow / history arms so
|
|
|
|
|
|
// those numpad digits aren't swallowed as history
|
|
|
|
|
|
// navigation. Whitespace / control text (Space,
|
|
|
|
|
|
// Enter, Tab) falls through to the named handlers.
|
2026-07-03 09:52:35 +03:00
|
|
|
|
// The numpad decimal key emits the OS-layout separator
|
|
|
|
|
|
// (a comma on German/European layouts), which the
|
|
|
|
|
|
// coordinate parser rejects. Force it to a decimal point
|
|
|
|
|
|
// from the physical key, independent of layout.
|
|
|
|
|
|
if !accel
|
|
|
|
|
|
&& status == Status::Ignored
|
|
|
|
|
|
&& matches!(
|
|
|
|
|
|
physical_key,
|
|
|
|
|
|
keyboard::key::Physical::Code(
|
|
|
|
|
|
keyboard::key::Code::NumpadDecimal
|
|
|
|
|
|
| keyboard::key::Code::NumpadComma
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Some(Message::CommandAppendChar(".".to_string()));
|
|
|
|
|
|
}
|
2026-06-27 09:33:08 -07:00
|
|
|
|
if !accel && status == Status::Ignored {
|
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(t) = text.as_deref() {
|
|
|
|
|
|
if !t.is_empty()
|
|
|
|
|
|
&& t.chars().all(|c| !c.is_control() && !c.is_whitespace())
|
|
|
|
|
|
{
|
|
|
|
|
|
return Some(Message::CommandAppendChar(t.to_string()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
match key {
|
|
|
|
|
|
// Space is a literal space inside the MText preview
|
|
|
|
|
|
// but finalises a command otherwise; the handler
|
|
|
|
|
|
// decides based on editor state.
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Space)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandSpace)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Enter)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandFinalize)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Escape) => {
|
|
|
|
|
|
Some(Message::CommandEscape)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Delete)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::DeleteSelected)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Backspace)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandBackspace)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Tab)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::DynTabNext)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowUp)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandHistoryPrev)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowDown)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandHistoryNext)
|
|
|
|
|
|
}
|
|
|
|
|
|
// Caret movement in the MText preview (no-op
|
|
|
|
|
|
// otherwise; these arrows are unused elsewhere).
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowLeft)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::MTextCaretMove(-1))
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowRight)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::MTextCaretMove(1))
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F3) => {
|
|
|
|
|
|
Some(Message::ToggleSnapEnabled)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F7) => {
|
|
|
|
|
|
Some(Message::ToggleGrid)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F8) => {
|
|
|
|
|
|
Some(Message::ToggleOrtho)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F9) => {
|
|
|
|
|
|
Some(Message::ToggleGridSnap)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F10) => {
|
|
|
|
|
|
Some(Message::TogglePolar)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F11) => {
|
|
|
|
|
|
Some(Message::ToggleOTrack)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F12) => {
|
|
|
|
|
|
Some(Message::ToggleDynInput)
|
|
|
|
|
|
}
|
2026-06-27 09:33:08 -07:00
|
|
|
|
keyboard::Key::Character(c) if accel => match c.as_str() {
|
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
|
|
|
|
"n" => Some(Message::TabNew),
|
|
|
|
|
|
"o" => Some(Message::OpenFile),
|
|
|
|
|
|
"s" if !shift => Some(Message::SaveFile),
|
|
|
|
|
|
"s" if shift => Some(Message::SaveAs),
|
|
|
|
|
|
"z" if !shift => Some(Message::Undo),
|
|
|
|
|
|
"z" if shift => Some(Message::Redo),
|
|
|
|
|
|
"y" => Some(Message::Redo),
|
2026-07-03 16:06:10 +03:00
|
|
|
|
// Ctrl/Cmd+A: select all layer rows when the
|
|
|
|
|
|
// Layer Manager is open, else all objects. The
|
|
|
|
|
|
// update handler branches on the active modal.
|
|
|
|
|
|
"a" if status == Status::Ignored => {
|
|
|
|
|
|
Some(Message::SelectAllShortcut)
|
|
|
|
|
|
}
|
2026-07-01 18:17:46 +03:00
|
|
|
|
// Clipboard accelerators defer to a focused
|
|
|
|
|
|
// text widget: when the command-line history
|
|
|
|
|
|
// editor (or any text field) captures Ctrl+C/
|
|
|
|
|
|
// X/V it copies/cuts/pastes its own text and
|
|
|
|
|
|
// marks the event Captured, so we must NOT also
|
|
|
|
|
|
// fire the drawing's COPYCLIP/CUTCLIP/paste.
|
|
|
|
|
|
// Only when nothing captured (the drawing has
|
|
|
|
|
|
// focus, status Ignored) do these run. (#232)
|
|
|
|
|
|
"c" if status == Status::Ignored => {
|
|
|
|
|
|
Some(Message::Command("COPYCLIP".to_string()))
|
|
|
|
|
|
}
|
|
|
|
|
|
"x" if status == Status::Ignored => {
|
|
|
|
|
|
Some(Message::Command("CUTCLIP".to_string()))
|
|
|
|
|
|
}
|
|
|
|
|
|
"v" if status == Status::Ignored => Some(Message::PasteShortcut),
|
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
|
|
|
|
_ => None,
|
|
|
|
|
|
},
|
|
|
|
|
|
// Printable glyphs are already handled by the
|
|
|
|
|
|
// text guard above the match; anything reaching
|
|
|
|
|
|
// here is a non-typing key we don't bind.
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
|
|
|
|
|
])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn focus_cmd_input(&self) -> Task<Message> {
|
|
|
|
|
|
iced::widget::operation::focus(iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
|
|
|
|
|
|
const BAR_BG: Color = Color {
|
|
|
|
|
|
r: 0.13,
|
|
|
|
|
|
g: 0.13,
|
|
|
|
|
|
b: 0.13,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TAB_ACTIVE: Color = Color {
|
|
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.22,
|
|
|
|
|
|
b: 0.22,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TAB_HOVER: Color = Color {
|
|
|
|
|
|
r: 0.18,
|
|
|
|
|
|
g: 0.18,
|
|
|
|
|
|
b: 0.18,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TAB_INACTIVE: Color = Color {
|
|
|
|
|
|
r: 0.13,
|
|
|
|
|
|
g: 0.13,
|
|
|
|
|
|
b: 0.13,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const ACCENT: Color = Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.90,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TEXT_ACTIVE: Color = Color::WHITE;
|
|
|
|
|
|
const TEXT_INACTIVE: Color = Color {
|
|
|
|
|
|
r: 0.60,
|
|
|
|
|
|
g: 0.60,
|
|
|
|
|
|
b: 0.60,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const CLOSE_HOVER: Color = Color {
|
|
|
|
|
|
r: 0.70,
|
|
|
|
|
|
g: 0.22,
|
|
|
|
|
|
b: 0.22,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BORDER_COLOR: Color = Color {
|
|
|
|
|
|
r: 0.25,
|
|
|
|
|
|
g: 0.25,
|
|
|
|
|
|
b: 0.25,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-05 16:44:44 +03:00
|
|
|
|
// Document tabs live in a flex-wrap flow so they spill onto lower rows when
|
|
|
|
|
|
// there are more tabs than the width can hold on one line.
|
|
|
|
|
|
let mut items: Vec<Element<'_, Message>> = Vec::new();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
|
|
|
|
|
|
for (idx, tab) in tabs.iter().enumerate() {
|
|
|
|
|
|
let is_active = idx == active_tab;
|
|
|
|
|
|
let name = crate::ui::text_util::elide(&tab.tab_display_name(), 24);
|
|
|
|
|
|
let title_inner: Element<'_, Message> = if tab.dirty {
|
|
|
|
|
|
row![
|
|
|
|
|
|
crate::ui::icons::tinted(
|
|
|
|
|
|
crate::ui::icons::DOT,
|
|
|
|
|
|
7.0,
|
|
|
|
|
|
Color {
|
|
|
|
|
|
r: 0.90,
|
|
|
|
|
|
g: 0.75,
|
|
|
|
|
|
b: 0.30,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
text(name).size(12),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(5)
|
|
|
|
|
|
.align_y(iced::Center)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
text(name).size(12).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let title_btn = button(title_inner)
|
|
|
|
|
|
.on_press(Message::TabSwitch(idx))
|
|
|
|
|
|
.padding([5, 12])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match (is_active, status) {
|
|
|
|
|
|
(true, _) => TAB_ACTIVE,
|
|
|
|
|
|
(false, button::Status::Hovered) => TAB_HOVER,
|
|
|
|
|
|
_ => TAB_INACTIVE,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: if is_active {
|
|
|
|
|
|
TEXT_ACTIVE
|
|
|
|
|
|
} else {
|
|
|
|
|
|
TEXT_INACTIVE
|
|
|
|
|
|
},
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: if is_active {
|
|
|
|
|
|
ACCENT
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Color::TRANSPARENT
|
|
|
|
|
|
},
|
|
|
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Start tab is fixed — no close button. Every other tab gets a close.
|
|
|
|
|
|
let row_inner: Row<'_, Message> = if tab.is_start {
|
|
|
|
|
|
row![title_btn].spacing(0).align_y(iced::Center)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let close_btn = button(crate::ui::icons::tinted(
|
|
|
|
|
|
crate::ui::icons::CLOSE,
|
|
|
|
|
|
10.0,
|
|
|
|
|
|
Color {
|
|
|
|
|
|
r: 0.55,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.55,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
))
|
|
|
|
|
|
.on_press(Message::TabClose(idx))
|
|
|
|
|
|
.padding([3, 5])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => CLOSE_HOVER,
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
if is_active {
|
|
|
|
|
|
TAB_ACTIVE
|
|
|
|
|
|
} else {
|
|
|
|
|
|
TAB_INACTIVE
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
row![title_btn, close_btn].spacing(0).align_y(iced::Center)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-05 16:44:44 +03:00
|
|
|
|
items.push(
|
|
|
|
|
|
container(row_inner)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: if is_active {
|
|
|
|
|
|
BORDER_COLOR
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Color::TRANSPARENT
|
|
|
|
|
|
},
|
|
|
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
|
radius: 0.0.into(),
|
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-05 16:44:44 +03:00
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.into(),
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let new_btn = button(text("+").size(14).color(Color {
|
|
|
|
|
|
r: 0.65,
|
|
|
|
|
|
g: 0.65,
|
|
|
|
|
|
b: 0.65,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
.on_press(Message::TabNew)
|
|
|
|
|
|
.padding([4, 10])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => TAB_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-05 16:44:44 +03:00
|
|
|
|
items.push(new_btn.into());
|
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-05 16:44:44 +03:00
|
|
|
|
container(WrapFlow::new(items).spacing_x(0.0).row_h(30.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
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(BAR_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER_COLOR,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.padding([0, 2])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Layout context-menu overlay ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
// ── Canvas-relative overlay positioning ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Wraps `panel` in a column+row of `Space` widgets so it sits at
|
|
|
|
|
|
/// canvas-relative coordinates `(anchor.x, anchor.y)`. `panel` is wrapped
|
|
|
|
|
|
/// in `iced::widget::opaque` so mouse events on the panel itself do not
|
|
|
|
|
|
/// fall through to the viewport mouse area underneath; outside-click
|
|
|
|
|
|
/// dismissal is the caller's responsibility (handled via `ViewportLeftPress`
|
|
|
|
|
|
/// in `update.rs`, identical to how the multi-functional grip popup is
|
|
|
|
|
|
/// dismissed). Pushed into `viewport_stack` so the anchor is interpreted
|
|
|
|
|
|
/// in canvas-relative space, not window-relative.
|
|
|
|
|
|
// ── Start / Welcome page ──────────────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Renders in place of the model-space viewport when the active tab is the
|
|
|
|
|
|
// fixed Start tab (`DocumentTab::is_start`). English-only by design — this
|
|
|
|
|
|
// is the public welcome screen and stays consistent across locales.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The page picks up the application icon's red-brown (#B03020) as a tint so
|
|
|
|
|
|
// it visually belongs to OpenCADStudio without overpowering the dark workspace.
|
|
|
|
|
|
|
|
|
|
|
|
const BRAND: Color = Color {
|
|
|
|
|
|
r: 0.690,
|
|
|
|
|
|
g: 0.188,
|
|
|
|
|
|
b: 0.125,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}; // #B03020
|
|
|
|
|
|
const BRAND_DARK: Color = Color {
|
|
|
|
|
|
r: 0.45,
|
|
|
|
|
|
g: 0.12,
|
|
|
|
|
|
b: 0.08,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-27 11:50:11 +03:00
|
|
|
|
/// Transparent input layer for one Model pane: a `mouse_area` filling the pane
|
|
|
|
|
|
/// that emits pane-tagged viewport events (`idx` = the pane's tile index). The
|
|
|
|
|
|
/// handlers offset the pane-local point to canvas coords and focus the pane.
|
|
|
|
|
|
fn pane_mouse_area<'a>(idx: usize) -> Element<'a, Message> {
|
|
|
|
|
|
mouse_area(container(Space::new().width(Fill).height(Fill)))
|
|
|
|
|
|
.on_move(move |p| Message::PaneMove(idx, p))
|
|
|
|
|
|
.on_press(Message::PanePress(idx))
|
|
|
|
|
|
.on_release(Message::PaneRelease(idx))
|
|
|
|
|
|
.on_right_press(Message::PaneRightPress(idx))
|
|
|
|
|
|
.on_right_release(Message::PaneRightRelease(idx))
|
|
|
|
|
|
.on_middle_press(Message::PaneMiddlePress(idx))
|
|
|
|
|
|
.on_middle_release(Message::PaneMiddleRelease(idx))
|
|
|
|
|
|
.on_scroll(move |d| Message::PaneScroll(idx, d))
|
|
|
|
|
|
.on_exit(Message::ViewportExit)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 21:02:05 +03:00
|
|
|
|
/// Canvas that draws a label rotated 90° (for a collapsed panel's bar).
|
|
|
|
|
|
struct VBarLabel {
|
|
|
|
|
|
text: String,
|
|
|
|
|
|
color: Color,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl canvas::Program<Message> for VBarLabel {
|
|
|
|
|
|
type State = ();
|
|
|
|
|
|
|
|
|
|
|
|
fn draw(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
_state: &(),
|
|
|
|
|
|
renderer: &iced::Renderer,
|
|
|
|
|
|
_theme: &Theme,
|
|
|
|
|
|
bounds: iced::Rectangle,
|
|
|
|
|
|
_cursor: iced::advanced::mouse::Cursor,
|
|
|
|
|
|
) -> Vec<canvas::Geometry> {
|
|
|
|
|
|
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
|
|
|
|
|
frame.with_save(|frame| {
|
|
|
|
|
|
frame.translate(iced::Vector::new(bounds.width / 2.0, bounds.height / 2.0));
|
|
|
|
|
|
frame.rotate(iced::Radians(std::f32::consts::FRAC_PI_2));
|
|
|
|
|
|
frame.fill_text(canvas::Text {
|
|
|
|
|
|
content: self.text.clone(),
|
|
|
|
|
|
position: iced::Point::ORIGIN,
|
|
|
|
|
|
color: self.color,
|
|
|
|
|
|
size: iced::Pixels(13.0),
|
|
|
|
|
|
align_x: iced::advanced::text::Alignment::Center,
|
|
|
|
|
|
align_y: iced::alignment::Vertical::Center,
|
|
|
|
|
|
shaping: iced::advanced::text::Shaping::Advanced,
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
vec![frame.into_geometry()]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A collapsed panel rendered as a tall narrow bar with its name written along
|
|
|
|
|
|
/// it, rotated 90°. Pressing it emits `on_press`.
|
|
|
|
|
|
pub(super) fn collapse_bar<'a>(name: &str, on_press: Message) -> Element<'a, Message> {
|
|
|
|
|
|
const LABEL: Color = Color {
|
|
|
|
|
|
r: 0.72,
|
|
|
|
|
|
g: 0.72,
|
|
|
|
|
|
b: 0.74,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BAR_BG: Color = Color {
|
|
|
|
|
|
r: 0.13,
|
|
|
|
|
|
g: 0.13,
|
|
|
|
|
|
b: 0.14,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BAR_BORDER: Color = Color {
|
|
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.22,
|
|
|
|
|
|
b: 0.24,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let label = canvas(VBarLabel {
|
|
|
|
|
|
text: name.to_string(),
|
|
|
|
|
|
color: LABEL,
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
mouse_area(
|
|
|
|
|
|
container(label)
|
|
|
|
|
|
.width(iced::Length::Fixed(26.0))
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(BAR_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BAR_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
.interaction(iced::mouse::Interaction::Pointer)
|
|
|
|
|
|
.on_press(on_press)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 20:53:11 +03:00
|
|
|
|
pub(super) fn start_page_view<'a>(
|
|
|
|
|
|
patrons: &'a [(String, i64)],
|
2026-07-21 10:07:21 +03:00
|
|
|
|
videos: &'a [crate::videos::VideoEntry],
|
|
|
|
|
|
videos_loading: bool,
|
|
|
|
|
|
video_thumbs: &'a std::collections::HashMap<String, iced::widget::image::Handle>,
|
2026-07-05 20:53:11 +03:00
|
|
|
|
recents: &'a [std::path::PathBuf],
|
2026-07-10 23:03:23 +03:00
|
|
|
|
thumbs: &'a std::collections::HashMap<
|
|
|
|
|
|
std::path::PathBuf,
|
|
|
|
|
|
Option<iced::widget::image::Handle>,
|
|
|
|
|
|
>,
|
2026-07-06 01:06:01 +03:00
|
|
|
|
recent_limit: usize,
|
|
|
|
|
|
recent_limit_input: &'a str,
|
2026-07-05 20:53:11 +03:00
|
|
|
|
avail_w: f32,
|
|
|
|
|
|
active: super::StartSection,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
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
|
|
|
|
const TEXT: Color = Color {
|
|
|
|
|
|
r: 0.94,
|
|
|
|
|
|
g: 0.93,
|
|
|
|
|
|
b: 0.92,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const CARD_BG: Color = Color {
|
|
|
|
|
|
r: 0.12,
|
|
|
|
|
|
g: 0.12,
|
|
|
|
|
|
b: 0.13,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const CARD_BORDER: Color = Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.20,
|
|
|
|
|
|
b: 0.22,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-05 20:53:11 +03:00
|
|
|
|
let headline = text("Open CAD Studio").size(40).color(BRAND);
|
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
|
|
|
|
|
|
|
|
|
|
// Plain outlined button (Open / New / Help / Contribute).
|
|
|
|
|
|
let outline_btn = |label: &'static str, msg: Message| {
|
|
|
|
|
|
button(text(label).size(14).color(TEXT))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.padding([10, 22])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => Color {
|
|
|
|
|
|
r: 0.18,
|
|
|
|
|
|
g: 0.18,
|
|
|
|
|
|
b: 0.20,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => Color {
|
|
|
|
|
|
r: 0.13,
|
|
|
|
|
|
g: 0.13,
|
|
|
|
|
|
b: 0.15,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.30,
|
|
|
|
|
|
g: 0.30,
|
|
|
|
|
|
b: 0.33,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Donate — the prominent call-to-action. Solid brand fill, white text.
|
|
|
|
|
|
let donate_btn = {
|
|
|
|
|
|
button(
|
|
|
|
|
|
row![
|
|
|
|
|
|
crate::ui::icons::tinted(crate::ui::icons::HEART, 14.0, Color::WHITE),
|
|
|
|
|
|
text("Donate").size(14).color(Color::WHITE),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(5)
|
|
|
|
|
|
.align_y(iced::Center),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::RibbonToolClick {
|
|
|
|
|
|
tool_id: "DONATE".to_string(),
|
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("DONATE".to_string()),
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([12, 28])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => BRAND_DARK,
|
|
|
|
|
|
_ => BRAND,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BRAND_DARK,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
shadow: iced::Shadow {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.0,
|
|
|
|
|
|
g: 0.0,
|
|
|
|
|
|
b: 0.0,
|
|
|
|
|
|
a: 0.4,
|
|
|
|
|
|
},
|
|
|
|
|
|
offset: iced::Vector::new(0.0, 2.0),
|
|
|
|
|
|
blur_radius: 6.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-05 20:53:11 +03:00
|
|
|
|
let primary_row = WrapFlow::new(vec![
|
|
|
|
|
|
outline_btn("New Drawing", Message::TabNew).into(),
|
|
|
|
|
|
outline_btn("Open File…", Message::OpenFile).into(),
|
|
|
|
|
|
donate_btn.into(),
|
|
|
|
|
|
])
|
|
|
|
|
|
.spacing_x(12.0)
|
|
|
|
|
|
.row_h(48.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
|
|
|
|
|
|
|
|
|
|
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
|
2026-07-05 20:53:11 +03:00
|
|
|
|
let mut secondary_items: Vec<Element<'a, Message>> = vec![
|
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
|
|
|
|
outline_btn(
|
|
|
|
|
|
"Send Feedback",
|
|
|
|
|
|
Message::RibbonToolClick {
|
|
|
|
|
|
tool_id: "REPORT".to_string(),
|
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("REPORT".to_string()),
|
|
|
|
|
|
},
|
2026-07-05 20:53:11 +03:00
|
|
|
|
)
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
outline_btn("Plugins", Message::PluginManagerOpen).into(),
|
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
|
|
|
|
];
|
|
|
|
|
|
// The web build is already in the browser, so only the desktop offers a
|
|
|
|
|
|
// link to the web version.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
// Bright ribbon blue (matches the active-tool accent), filled.
|
2026-07-05 20:53:11 +03:00
|
|
|
|
secondary_items.push(
|
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
|
|
|
|
button(text("OCS Web").size(14).color(Color::WHITE))
|
|
|
|
|
|
.on_press(Message::RibbonToolClick {
|
|
|
|
|
|
tool_id: "WEBVERSION".to_string(),
|
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("WEBVERSION".to_string()),
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([10, 22])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => Color {
|
|
|
|
|
|
r: 0.15,
|
|
|
|
|
|
g: 0.45,
|
|
|
|
|
|
b: 0.78,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.90,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.90,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
2026-07-05 20:53:11 +03:00
|
|
|
|
})
|
|
|
|
|
|
.into(),
|
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-05 20:53:11 +03:00
|
|
|
|
let secondary_row = WrapFlow::new(secondary_items)
|
|
|
|
|
|
.spacing_x(12.0)
|
|
|
|
|
|
.row_h(44.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
|
|
|
|
|
|
|
|
|
|
// Buttons sit on a transparent container with a large, brand-tinted
|
|
|
|
|
|
// ambient shadow (offset = 0, big blur) — produces a soft halo behind
|
|
|
|
|
|
// the action row, matching the Thunderbird coloured-glow look against
|
|
|
|
|
|
// the dark page.
|
|
|
|
|
|
let primary_glow = container(primary_row)
|
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
|
top: 4.0,
|
|
|
|
|
|
right: 8.0,
|
|
|
|
|
|
bottom: 4.0,
|
|
|
|
|
|
left: 8.0,
|
|
|
|
|
|
})
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color::TRANSPARENT)),
|
|
|
|
|
|
shadow: iced::Shadow {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: BRAND.r,
|
|
|
|
|
|
g: BRAND.g,
|
|
|
|
|
|
b: BRAND.b,
|
|
|
|
|
|
a: 0.45,
|
|
|
|
|
|
},
|
|
|
|
|
|
offset: iced::Vector::ZERO,
|
|
|
|
|
|
blur_radius: 80.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let content = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(28.0)),
|
|
|
|
|
|
container(headline).center_x(Fill),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(22.0)),
|
|
|
|
|
|
container(primary_glow).center_x(Fill),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(10.0)),
|
|
|
|
|
|
container(secondary_row).center_x(Fill),
|
2026-07-21 10:07:21 +03:00
|
|
|
|
Space::new().height(Fill),
|
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
|
|
|
|
]
|
|
|
|
|
|
.spacing(0)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
// Page background reverts to plain dark — the glow alone provides the
|
|
|
|
|
|
// brand colour cue, the rest of the page stays neutral so it reads as
|
|
|
|
|
|
// "workspace area" not "advertising banner".
|
|
|
|
|
|
const PAGE_BG: Color = Color {
|
|
|
|
|
|
r: 0.08,
|
|
|
|
|
|
g: 0.08,
|
|
|
|
|
|
b: 0.085,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
2026-07-05 20:53:11 +03:00
|
|
|
|
// Three parts — Recent Files · Welcome · Supporters. They sit side by side
|
|
|
|
|
|
// while they fit; when the page is too narrow it becomes a tab bar with the
|
|
|
|
|
|
// Welcome tab open by default.
|
|
|
|
|
|
let recent_w = 280.0f32;
|
2026-07-21 10:07:21 +03:00
|
|
|
|
let videos_w = 300.0f32;
|
2026-07-05 20:53:11 +03:00
|
|
|
|
let sup_w = 240.0f32;
|
|
|
|
|
|
let welcome_min = 480.0f32;
|
|
|
|
|
|
let avail = (avail_w - 16.0).max(0.0); // minus the page's l/r padding
|
2026-07-21 10:07:21 +03:00
|
|
|
|
// The videos rail overlays the welcome column's left side, and the welcome
|
|
|
|
|
|
// content is CENTRED in that column — so side-by-side only works while the
|
|
|
|
|
|
// centred `welcome_min` block still leaves a rail-sized gap on its left:
|
|
|
|
|
|
// the column must fit welcome_min plus a videos_w + spacing margin on BOTH
|
|
|
|
|
|
// sides (centring is symmetric). Below that, the centre content would
|
|
|
|
|
|
// slide over the rail — switch to the tabbed layout instead.
|
|
|
|
|
|
let fits_all = avail
|
|
|
|
|
|
>= recent_w + sup_w + 2.0 * 16.0 + welcome_min + 2.0 * (videos_w + 16.0);
|
2026-07-05 20:53:11 +03:00
|
|
|
|
|
2026-07-10 23:03:23 +03:00
|
|
|
|
let recent = recent_files_panel(recents, thumbs, recent_limit, recent_limit_input);
|
2026-07-05 20:53:11 +03:00
|
|
|
|
let welcome = container(content).width(Fill).height(Fill);
|
|
|
|
|
|
|
2026-07-21 10:07:21 +03:00
|
|
|
|
// Tutorial-videos rail: the official playlist, fetched at boot (cached on
|
|
|
|
|
|
// disk) — thumbnail card + title per video, click opens the browser.
|
|
|
|
|
|
let videos_panel: Element<'a, Message> = {
|
|
|
|
|
|
const NAME_COLOR: Color = Color {
|
|
|
|
|
|
r: 0.80,
|
|
|
|
|
|
g: 0.80,
|
|
|
|
|
|
b: 0.82,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
// Inner width = panel 300 − padding 2×16 − scrollbar gutter 14.
|
|
|
|
|
|
const THUMB_H: f32 = (300.0 - 32.0 - 14.0) * 9.0 / 16.0;
|
|
|
|
|
|
let mut list = column![text("Tutorials").size(15).color(TEXT)]
|
|
|
|
|
|
.spacing(10)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
// Keep the scrollbar off the thumbnails.
|
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
|
right: 14.0,
|
|
|
|
|
|
..iced::Padding::ZERO
|
|
|
|
|
|
});
|
|
|
|
|
|
for v in videos {
|
|
|
|
|
|
let mut card = column![].spacing(6).width(Fill);
|
|
|
|
|
|
if let Some(handle) = video_thumbs.get(&v.id) {
|
|
|
|
|
|
card = card.push(
|
|
|
|
|
|
container(
|
|
|
|
|
|
iced::widget::image(handle.clone())
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(iced::Length::Fixed(THUMB_H))
|
|
|
|
|
|
.content_fit(iced::ContentFit::Cover),
|
|
|
|
|
|
)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(iced::Length::Fixed(THUMB_H))
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: CARD_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.clip(true),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
card = card.push(text(v.title.clone()).size(12).color(NAME_COLOR));
|
|
|
|
|
|
list = list.push(
|
|
|
|
|
|
mouse_area(card)
|
|
|
|
|
|
.interaction(iced::mouse::Interaction::Pointer)
|
|
|
|
|
|
.on_press(Message::OpenUrl(crate::videos::watch_url(&v.id))),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if videos.is_empty() {
|
|
|
|
|
|
let note = if videos_loading {
|
|
|
|
|
|
"Loading videos…"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"Videos load from the internet."
|
|
|
|
|
|
};
|
|
|
|
|
|
list = list.push(text(note).size(12).color(NAME_COLOR));
|
|
|
|
|
|
}
|
|
|
|
|
|
let playlist_btn = mouse_area(
|
|
|
|
|
|
container(
|
|
|
|
|
|
text("Open playlist on YouTube").size(12).color(Color::WHITE),
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding([6, 10])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.center_x(Fill)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
|
r: 0.85,
|
|
|
|
|
|
g: 0.15,
|
|
|
|
|
|
b: 0.15,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
.interaction(iced::mouse::Interaction::Pointer)
|
|
|
|
|
|
.on_press(Message::OpenUrl(crate::videos::PLAYLIST_URL.to_string()));
|
|
|
|
|
|
container(column![
|
|
|
|
|
|
iced::widget::scrollable(list).height(Fill),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(12.0)),
|
|
|
|
|
|
playlist_btn,
|
|
|
|
|
|
])
|
|
|
|
|
|
.width(iced::Length::Fixed(videos_w))
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.padding(16)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(CARD_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: CARD_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 8.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-02 01:00:22 +03:00
|
|
|
|
// Right rail: Patreon supporters, fetched at boot. When the list is empty
|
|
|
|
|
|
// (no token configured / offline) only the "Support on Patreon" button
|
|
|
|
|
|
// shows, so the rail always invites support.
|
|
|
|
|
|
let supporters: Element<'a, Message> = {
|
|
|
|
|
|
const NAME_COLOR: Color = Color {
|
|
|
|
|
|
r: 0.78,
|
|
|
|
|
|
g: 0.78,
|
|
|
|
|
|
b: 0.80,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
let mut list = column![
|
|
|
|
|
|
text("Supporters").size(15).color(TEXT),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(12.0)),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(6)
|
|
|
|
|
|
.width(Fill);
|
|
|
|
|
|
for (name, cents) in patrons {
|
|
|
|
|
|
// Cents → the campaign currency's main unit. Symbol assumed "$";
|
|
|
|
|
|
// adjust if the campaign bills in another currency.
|
|
|
|
|
|
let amount = format!("${:.2}", *cents as f64 / 100.0);
|
|
|
|
|
|
list = list.push(
|
|
|
|
|
|
iced::widget::row![
|
|
|
|
|
|
text(name).size(12).color(NAME_COLOR).width(Fill),
|
|
|
|
|
|
text(amount).size(12).color(NAME_COLOR),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(6),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
let support_btn = mouse_area(
|
|
|
|
|
|
container(
|
2026-07-06 01:06:01 +03:00
|
|
|
|
iced::widget::row![
|
|
|
|
|
|
crate::ui::icons::tinted(crate::ui::icons::HEART, 13.0, Color::WHITE),
|
|
|
|
|
|
text("Support on Patreon").size(12).color(Color::WHITE),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(6)
|
|
|
|
|
|
.align_y(iced::Center),
|
2026-07-02 01:00:22 +03:00
|
|
|
|
)
|
|
|
|
|
|
.padding([6, 10])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.center_x(Fill)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
|
r: 0.90,
|
|
|
|
|
|
g: 0.28,
|
|
|
|
|
|
b: 0.30,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
.interaction(iced::mouse::Interaction::Pointer)
|
|
|
|
|
|
.on_press(Message::OpenUrl(
|
|
|
|
|
|
"https://patreon.com/HakanSeven12".to_string(),
|
|
|
|
|
|
));
|
|
|
|
|
|
container(column![
|
|
|
|
|
|
iced::widget::scrollable(list).height(Fill),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(12.0)),
|
|
|
|
|
|
support_btn,
|
|
|
|
|
|
])
|
|
|
|
|
|
.width(iced::Length::Fixed(240.0))
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.padding(20)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(CARD_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: CARD_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 8.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-05 20:53:11 +03:00
|
|
|
|
let body: Element<'a, Message> = if fits_all {
|
2026-07-21 10:07:21 +03:00
|
|
|
|
// The videos rail OVERLAYS the welcome column's empty left side
|
|
|
|
|
|
// instead of participating in the row: the welcome content keeps the
|
|
|
|
|
|
// same width and centre as before, so the action buttons don't move.
|
|
|
|
|
|
let base = iced::widget::row![recent, welcome, supporters].spacing(16);
|
|
|
|
|
|
let rail = container(videos_panel)
|
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
|
left: recent_w + 16.0,
|
|
|
|
|
|
..iced::Padding::ZERO
|
|
|
|
|
|
})
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
iced::widget::stack![base, rail].into()
|
2026-07-05 20:53:11 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
let tab_btn = |label: &'static str, section: super::StartSection| {
|
|
|
|
|
|
let is_active = active == section;
|
|
|
|
|
|
button(text(label).size(14))
|
|
|
|
|
|
.on_press(Message::StartSectionSelect(section))
|
|
|
|
|
|
.padding([8, 18])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match (is_active, status) {
|
|
|
|
|
|
(true, _) => Color {
|
|
|
|
|
|
r: 0.18,
|
|
|
|
|
|
g: 0.18,
|
|
|
|
|
|
b: 0.20,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
(false, button::Status::Hovered) => Color {
|
|
|
|
|
|
r: 0.14,
|
|
|
|
|
|
g: 0.14,
|
|
|
|
|
|
b: 0.16,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: if is_active {
|
|
|
|
|
|
TEXT
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Color {
|
|
|
|
|
|
r: 0.62,
|
|
|
|
|
|
g: 0.62,
|
|
|
|
|
|
b: 0.62,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: if is_active { BRAND } else { Color::TRANSPARENT },
|
|
|
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
let tab_bar = iced::widget::row![
|
|
|
|
|
|
tab_btn("Recent Files", super::StartSection::Recent),
|
2026-07-21 10:07:21 +03:00
|
|
|
|
tab_btn("Videos", super::StartSection::Videos),
|
2026-07-05 20:53:11 +03:00
|
|
|
|
tab_btn("Welcome", super::StartSection::Welcome),
|
|
|
|
|
|
tab_btn("Supporters", super::StartSection::Supporters),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(6);
|
|
|
|
|
|
let section_body: Element<'a, Message> = match active {
|
|
|
|
|
|
super::StartSection::Recent => container(recent)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.center_x(Fill)
|
|
|
|
|
|
.into(),
|
2026-07-21 10:07:21 +03:00
|
|
|
|
super::StartSection::Videos => container(videos_panel)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.center_x(Fill)
|
|
|
|
|
|
.into(),
|
2026-07-05 20:53:11 +03:00
|
|
|
|
super::StartSection::Welcome => welcome.into(),
|
|
|
|
|
|
super::StartSection::Supporters => container(supporters)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.center_x(Fill)
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
};
|
|
|
|
|
|
column![
|
|
|
|
|
|
container(tab_bar).center_x(Fill),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(12.0)),
|
|
|
|
|
|
section_body,
|
|
|
|
|
|
]
|
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
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
2026-07-05 20:53:11 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
container(body)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(PAGE_BG)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
|
top: 16.0,
|
|
|
|
|
|
right: 8.0,
|
|
|
|
|
|
bottom: 16.0,
|
|
|
|
|
|
left: 8.0,
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Recent Documents panel (Start tab left rail) ──────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Slots into the same `row![properties_el, viewport_stack]` position the
|
|
|
|
|
|
// Properties panel normally occupies, but only when the active tab is the
|
|
|
|
|
|
// Start tab. The list is restored from disk at boot and re-saved on every
|
|
|
|
|
|
// open — entries persist across sessions.
|
2026-07-06 01:06:01 +03:00
|
|
|
|
pub(super) fn recent_files_panel<'a>(
|
|
|
|
|
|
recents: &'a [std::path::PathBuf],
|
2026-07-10 23:03:23 +03:00
|
|
|
|
thumbs: &'a std::collections::HashMap<
|
|
|
|
|
|
std::path::PathBuf,
|
|
|
|
|
|
Option<iced::widget::image::Handle>,
|
|
|
|
|
|
>,
|
2026-07-06 01:06:01 +03:00
|
|
|
|
limit: usize,
|
|
|
|
|
|
limit_input: &'a str,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
|
// Card chrome matches the Supporters rail (the canonical start-page card).
|
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
|
|
|
|
const PANEL_BG: Color = Color {
|
2026-07-06 01:06:01 +03:00
|
|
|
|
r: 0.12,
|
|
|
|
|
|
g: 0.12,
|
|
|
|
|
|
b: 0.13,
|
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
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const PANEL_BORDER: Color = Color {
|
2026-07-06 01:06:01 +03:00
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.20,
|
|
|
|
|
|
b: 0.22,
|
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
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const ITEM_HOVER: Color = Color {
|
|
|
|
|
|
r: 0.16,
|
|
|
|
|
|
g: 0.16,
|
|
|
|
|
|
b: 0.18,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TEXT: Color = Color {
|
2026-07-06 01:06:01 +03:00
|
|
|
|
r: 0.94,
|
|
|
|
|
|
g: 0.93,
|
|
|
|
|
|
b: 0.92,
|
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
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const MUTED: Color = Color {
|
|
|
|
|
|
r: 0.60,
|
|
|
|
|
|
g: 0.60,
|
|
|
|
|
|
b: 0.62,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-06 01:06:01 +03:00
|
|
|
|
// Title mirrors the Supporters rail: size 15 in the bright text colour,
|
|
|
|
|
|
// followed by a 12px gap before the content.
|
|
|
|
|
|
let title = text("Recent Documents").size(15).color(TEXT);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
|
|
|
|
|
|
let body: Element<'a, Message> = if recents.is_empty() {
|
2026-07-06 01:06:01 +03:00
|
|
|
|
container(text("Files you open will show up here.").size(12).color(MUTED))
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
2026-07-06 01:06:01 +03:00
|
|
|
|
// Right padding reserves a gutter for the scrollbar so it doesn't sit on
|
|
|
|
|
|
// top of the row's ✕ remove button.
|
|
|
|
|
|
let mut col = column![].spacing(0).padding(iced::Padding {
|
|
|
|
|
|
right: 12.0,
|
|
|
|
|
|
..iced::Padding::ZERO
|
|
|
|
|
|
});
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
for path in recents {
|
|
|
|
|
|
let name = path
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| path.to_string_lossy().into_owned());
|
|
|
|
|
|
let dir = path
|
|
|
|
|
|
.parent()
|
|
|
|
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
2026-07-10 23:03:23 +03:00
|
|
|
|
// Leading DWG preview thumbnail (fixed box keeps rows aligned even
|
|
|
|
|
|
// when a file has no readable preview).
|
|
|
|
|
|
let thumb: Element<'a, Message> = match thumbs.get(path).and_then(|o| o.as_ref()) {
|
|
|
|
|
|
Some(h) => container(
|
|
|
|
|
|
iced::widget::image(h.clone())
|
|
|
|
|
|
.content_fit(iced::ContentFit::Contain)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
)
|
|
|
|
|
|
.width(46)
|
|
|
|
|
|
.height(34)
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
None => iced::widget::Space::new().width(46).height(34).into(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let path_for_open = path.clone();
|
|
|
|
|
|
let open_btn = button(
|
2026-07-10 23:03:23 +03:00
|
|
|
|
row![
|
|
|
|
|
|
thumb,
|
|
|
|
|
|
column![
|
|
|
|
|
|
text(crate::ui::text_util::elide(&name, 28))
|
|
|
|
|
|
.size(12)
|
|
|
|
|
|
.color(TEXT),
|
|
|
|
|
|
text(crate::ui::text_util::elide(&dir, 38))
|
|
|
|
|
|
.size(10)
|
|
|
|
|
|
.color(MUTED),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(2),
|
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-10 23:03:23 +03:00
|
|
|
|
.spacing(8)
|
|
|
|
|
|
.align_y(iced::Center),
|
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
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::OpenRecent(path_for_open))
|
|
|
|
|
|
.padding([6, 12])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => ITEM_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let path_for_remove = path.clone();
|
|
|
|
|
|
let remove_btn = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 11.0, MUTED))
|
|
|
|
|
|
.on_press(Message::RecentRemove(path_for_remove))
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => Color {
|
|
|
|
|
|
r: 0.45,
|
|
|
|
|
|
g: 0.15,
|
|
|
|
|
|
b: 0.15,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: MUTED,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
col = col.push(row![open_btn, remove_btn].spacing(0).align_y(iced::Center));
|
|
|
|
|
|
}
|
2026-07-06 01:06:01 +03:00
|
|
|
|
iced::widget::scrollable(col).height(Fill).into()
|
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-06 01:06:01 +03:00
|
|
|
|
// Footer: how many recent files to keep — [-] [editable count] [+] with the
|
|
|
|
|
|
// max shown. The count box takes keyboard input (applied on Enter); the
|
|
|
|
|
|
// update handler clamps to [MIN, MAX] and persists (see `set_recent_limit`),
|
|
|
|
|
|
// so an over-max entry snaps to the max.
|
|
|
|
|
|
const STEP: usize = 5;
|
|
|
|
|
|
let step_style = |_: &Theme, status: button::Status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => ITEM_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: PANEL_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
};
|
|
|
|
|
|
// +/- step from whatever is currently shown in the box (mid-edit included).
|
|
|
|
|
|
let shown = limit_input.parse::<usize>().unwrap_or(limit);
|
|
|
|
|
|
let count_box = iced::widget::text_input("", limit_input)
|
|
|
|
|
|
.on_input(Message::RecentLimitInput)
|
|
|
|
|
|
.on_submit(Message::SetRecentLimit(shown))
|
|
|
|
|
|
.size(13)
|
|
|
|
|
|
.padding([2, 6])
|
|
|
|
|
|
.width(iced::Length::Fixed(46.0));
|
|
|
|
|
|
let limit_row = row![
|
|
|
|
|
|
text("Keep recent files").size(11).color(MUTED).width(Fill),
|
|
|
|
|
|
button(crate::ui::icons::tinted(crate::ui::icons::MINUS, 11.0, TEXT))
|
|
|
|
|
|
.on_press(Message::SetRecentLimit(shown.saturating_sub(STEP)))
|
|
|
|
|
|
.padding([3, 6])
|
|
|
|
|
|
.style(step_style),
|
|
|
|
|
|
count_box,
|
|
|
|
|
|
button(crate::ui::icons::tinted(crate::ui::icons::PLUS, 11.0, TEXT))
|
|
|
|
|
|
.on_press(Message::SetRecentLimit(shown + STEP))
|
|
|
|
|
|
.padding([3, 6])
|
|
|
|
|
|
.style(step_style),
|
|
|
|
|
|
text(format!("/ {}", super::recent::RECENT_MAX))
|
|
|
|
|
|
.size(11)
|
|
|
|
|
|
.color(MUTED),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(6)
|
|
|
|
|
|
.align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
|
|
container(
|
|
|
|
|
|
column![
|
|
|
|
|
|
title,
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(12.0)),
|
|
|
|
|
|
body,
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(12.0)),
|
|
|
|
|
|
limit_row,
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
)
|
|
|
|
|
|
.width(iced::Length::Fixed(280.0))
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.padding(20)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: PANEL_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 8.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.into()
|
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
|
|
|
|
}
|
|
|
|
|
|
|