forked from 1b-agent/opencadstudio-upstream
Four bugs prevented viewports from working like AutoCAD model-space windows: 1. Dead-code double-click block: `!is_down2` was always false because `is_down2 = sel.left_down` is captured while the button is still held (before the selection block clears it). Changed to `is_down` so the double-click handler actually fires. 2. Inverted pan direction: pan_active_viewport used `-=` on the delta returned by screen_delta_to_world, which is the same delta that cam.pan *adds* to its target — causing reversed drag. Changed to `+=`. 3. Zoom not cursor-centered: zoom_active_viewport now accepts an optional paper-space cursor position and adjusts view_target so the model point under the cursor stays fixed (same as cam.zoom_about_point). 4. ESC did not exit MSPACE: CommandEscape now calls ExitViewport when active_viewport is set and no command is running. Also added MS/MSPACE and PSPACE command aliases, and MspaceCommand / PspaceCommand messages so MS auto-enters the first viewport. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
344 lines
13 KiB
Rust
344 lines
13 KiB
Rust
mod document;
|
|
mod helpers;
|
|
mod history;
|
|
mod properties;
|
|
mod layers;
|
|
mod commands;
|
|
mod cmd_result;
|
|
mod view;
|
|
mod update;
|
|
|
|
use document::DocumentTab;
|
|
|
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
|
use acadrust::CadDocument;
|
|
use crate::modules::ModuleEvent;
|
|
use crate::scene::CubeRegion;
|
|
use crate::snap::Snapper;
|
|
use crate::ui::{AppMenu, CommandLine, Ribbon, StatusBar};
|
|
|
|
use iced::time::Instant;
|
|
use iced::window;
|
|
use iced::{mouse, Point, Task, Theme};
|
|
|
|
pub(super) const POLY_START_DELAY_MS: u128 = 150;
|
|
pub(super) const VARIES_LABEL: &str = "*VARIES*";
|
|
|
|
// ── Application state ──────────────────────────────────────────────────────
|
|
|
|
pub(super) struct H7CAD {
|
|
start: Instant,
|
|
tabs: Vec<DocumentTab>,
|
|
active_tab: usize,
|
|
tab_counter: usize,
|
|
ribbon: Ribbon,
|
|
app_menu: AppMenu,
|
|
command_line: CommandLine,
|
|
status_bar: StatusBar,
|
|
cursor_pos: Point,
|
|
vp_size: (f32, f32),
|
|
snapper: Snapper,
|
|
snap_popup_open: bool,
|
|
/// Whether Tangent snap was enabled before a tangent-pick command started.
|
|
pre_cmd_tangent: Option<bool>,
|
|
/// Orthogonal drawing constraint (F8): constrains picks to 0°/90°/180°/270°.
|
|
ortho_mode: bool,
|
|
/// Polar tracking (F10): constrains picks to 45° angle increments.
|
|
polar_mode: bool,
|
|
/// Show grid lines in the viewport (F7).
|
|
show_grid: bool,
|
|
/// Last point committed by a drawing command — used as ortho/polar base.
|
|
last_point: Option<glam::Vec3>,
|
|
/// OS window Id for the floating Layer Properties Manager (None when closed).
|
|
layer_window: Option<window::Id>,
|
|
/// OS window Id of the primary application window.
|
|
main_window: Option<window::Id>,
|
|
/// In-memory clipboard: cloned entities waiting to be pasted.
|
|
clipboard: Vec<acadrust::EntityType>,
|
|
/// Centroid of the clipboard entities (XZ plane, Y-up).
|
|
clipboard_centroid: glam::Vec3,
|
|
/// Which layout tab has its context menu open (None = closed).
|
|
layout_context_menu: Option<String>,
|
|
/// Inline rename state: (original_name, current_edit_value).
|
|
layout_rename_state: Option<(String, String)>,
|
|
/// Timestamp of the previous viewport left-click release (for double-click detection).
|
|
last_vp_click_time: Option<Instant>,
|
|
/// Screen position of the previous viewport left-click release.
|
|
last_vp_click_pos: Option<Point>,
|
|
/// Page Setup overlay open/closed.
|
|
page_setup_open: bool,
|
|
/// Editable paper width buffer for the Page Setup panel (string while typing).
|
|
page_setup_w: String,
|
|
/// Editable paper height buffer for the Page Setup panel (string while typing).
|
|
page_setup_h: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Message {
|
|
Tick(Instant),
|
|
OpenFile,
|
|
FileOpened(Result<(String, PathBuf, CadDocument), String>),
|
|
SaveFile,
|
|
SaveAs,
|
|
PickedSavePath(Option<PathBuf>),
|
|
ClearScene,
|
|
SetWireframe(bool),
|
|
/// Switch camera projection: true = Orthographic, false = Perspective.
|
|
SetProjection(bool),
|
|
/// Select a ribbon module tab by index.
|
|
RibbonSelectTab(usize),
|
|
/// A ribbon tool button was clicked — highlights the tool and dispatches its event.
|
|
RibbonToolClick {
|
|
tool_id: String,
|
|
event: ModuleEvent,
|
|
},
|
|
// ── Application menu ──────────────────────────────────────────────────
|
|
ToggleAppMenu,
|
|
CloseAppMenu,
|
|
/// Close the menu and immediately dispatch a CAD command.
|
|
CloseAppMenuAndRun(String),
|
|
AppMenuSearch(String),
|
|
// ── Document tabs ──────────────────────────────────────────────────────
|
|
/// Create a new empty document tab.
|
|
TabNew,
|
|
/// Switch to the given tab index.
|
|
TabSwitch(usize),
|
|
/// Close the given tab index.
|
|
TabClose(usize),
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
CommandInput(String),
|
|
CommandSubmit,
|
|
Command(String),
|
|
ToggleLayers,
|
|
LayerToggleVisible(usize),
|
|
LayerToggleLock(usize),
|
|
LayerToggleFreeze(usize),
|
|
LayerNew,
|
|
LayerDelete,
|
|
LayerSetCurrent,
|
|
LayerSelect(usize),
|
|
LayerRenameStart(usize),
|
|
LayerRenameEdit(String),
|
|
LayerColorPickerToggle(usize),
|
|
LayerColorMorePalette,
|
|
LayerColorSet(u8),
|
|
LayerLinetypeSet(String),
|
|
LayerLineweightSet(LineWeight),
|
|
LayerTransparencyEdit(usize, String),
|
|
LayerRenameCommit,
|
|
CursorMoved(Point),
|
|
ViewportClick,
|
|
ViewportMove(Point),
|
|
ViewportLeftPress,
|
|
ViewportLeftRelease,
|
|
ViewportRightPress,
|
|
ViewportRightRelease,
|
|
ViewportMiddlePress,
|
|
ViewportMiddleRelease,
|
|
ViewportScroll(mouse::ScrollDelta),
|
|
ViewportExit,
|
|
ViewCubeSnap(CubeRegion),
|
|
WindowResized(f32, f32),
|
|
/// Enter pressed globally — finalises the active command (no text-input involvement).
|
|
CommandFinalize,
|
|
/// Escape pressed globally — cancels the active command.
|
|
CommandEscape,
|
|
/// Toggle the global snap on/off (OSNAP button body click).
|
|
ToggleSnapEnabled,
|
|
/// Toggle grid-snap on/off — F9 / SNAP status-bar button.
|
|
ToggleGridSnap,
|
|
/// Toggle grid display in the viewport — F7 / GRID status-bar button.
|
|
ToggleGrid,
|
|
/// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button.
|
|
ToggleOrtho,
|
|
/// Toggle polar-angle constraint (45° increments) — F10 / POLAR status-bar button.
|
|
TogglePolar,
|
|
/// Toggle an individual snap mode (from popup row click).
|
|
ToggleSnap(crate::snap::SnapType),
|
|
/// Open / close the OSNAP popup (▾ arrow click).
|
|
ToggleSnapPopup,
|
|
/// Close the OSNAP popup (click-catcher outside the panel).
|
|
CloseSnapPopup,
|
|
/// Enable all snap modes.
|
|
SnapSelectAll,
|
|
/// Disable all snap modes.
|
|
SnapClearAll,
|
|
/// Toggle a ribbon dropdown open/closed.
|
|
ToggleRibbonDropdown(String),
|
|
/// Close any open ribbon dropdown (click-catcher outside the panel).
|
|
CloseRibbonDropdown,
|
|
/// User selected a specific item from a ribbon dropdown.
|
|
DropdownSelectItem {
|
|
dropdown_id: &'static str,
|
|
cmd: &'static str,
|
|
},
|
|
/// Delete key — erase all currently selected entities.
|
|
DeleteSelected,
|
|
Undo,
|
|
Redo,
|
|
UndoMany(usize),
|
|
RedoMany(usize),
|
|
// ── Ribbon ────────────────────────────────────────────────────────────
|
|
/// User selected a layer from the layer combobox in the ribbon.
|
|
RibbonLayerChanged(String),
|
|
/// User changed the active color in the Properties toolbar.
|
|
RibbonColorChanged(AcadColor),
|
|
/// Toggle the full ACI palette inside the ribbon color picker.
|
|
RibbonColorPaletteToggle,
|
|
/// User changed the active linetype in the Properties toolbar.
|
|
RibbonLinetypeChanged(String),
|
|
/// User changed the active lineweight in the Properties toolbar.
|
|
RibbonLineweightChanged(LineWeight),
|
|
|
|
// ── Properties panel ──────────────────────────────────────────────────
|
|
/// User selected a layer from the layer pick_list in the Properties panel.
|
|
PropLayerChanged(String),
|
|
PropSelectionGroupChanged(crate::ui::properties::SelectionGroup),
|
|
/// User picked a color from the Properties color picker.
|
|
PropColorChanged(AcadColor),
|
|
/// User selected a lineweight from the Properties pick_list.
|
|
PropLwChanged(LineWeight),
|
|
/// User selected a linetype from the linetype pick_list.
|
|
PropLinetypeChanged(String),
|
|
/// User toggled a boolean property (e.g. Invisible).
|
|
PropBoolToggle(&'static str),
|
|
/// User selected a hatch pattern from the pattern pick_list in Properties.
|
|
PropHatchPatternChanged(String),
|
|
/// User selected a generic choice field in the Properties panel.
|
|
PropGeomChoiceChanged {
|
|
field: &'static str,
|
|
value: String,
|
|
},
|
|
/// User is typing in an editable geometry field (live buffer update).
|
|
PropGeomInput {
|
|
field: &'static str,
|
|
value: String,
|
|
},
|
|
/// User committed a geometry/common field edit (Enter pressed).
|
|
PropGeomCommit(&'static str),
|
|
/// Toggle the inline color picker dropdown open/closed.
|
|
PropColorPickerToggle,
|
|
/// Toggle the full ACI colour palette expansion.
|
|
PropColorPaletteToggle,
|
|
/// Enter the model-space editing mode inside the given viewport (MSPACE).
|
|
EnterViewport(acadrust::Handle),
|
|
/// Exit MSPACE and return to paper-space editing (PSPACE).
|
|
ExitViewport,
|
|
/// MS command: enter MSPACE for the first available viewport.
|
|
MspaceCommand,
|
|
/// PS command: exit MSPACE (PSPACE).
|
|
PspaceCommand,
|
|
/// Switch to a named layout ("Model" or paper space layout name).
|
|
LayoutSwitch(String),
|
|
/// Create a new paper space layout.
|
|
LayoutCreate,
|
|
/// Delete the named paper space layout (Model cannot be deleted).
|
|
LayoutDelete(String),
|
|
/// Begin inline rename for the given layout tab.
|
|
LayoutRenameStart(String),
|
|
/// Live-update the rename text input buffer.
|
|
LayoutRenameEdit(String),
|
|
/// Commit the rename (Enter pressed in the text input).
|
|
LayoutRenameCommit,
|
|
/// Cancel an in-progress rename (Escape).
|
|
LayoutRenameCancel,
|
|
/// Open the right-click context menu for the given layout tab.
|
|
LayoutContextMenu(String),
|
|
/// Close the layout context menu.
|
|
LayoutContextMenuClose,
|
|
/// A window was closed by the OS (e.g. the user clicked the title-bar ✕).
|
|
OsWindowClosed(window::Id),
|
|
/// No-op — used as a fallback when a TabEvent has no host mapping.
|
|
Noop,
|
|
// ── Page Setup ────────────────────────────────────────────────────────
|
|
/// Open the Page Setup panel for the current layout.
|
|
PageSetupOpen,
|
|
/// Close (cancel) the Page Setup panel without applying changes.
|
|
PageSetupClose,
|
|
/// Live-edit of the paper width field.
|
|
PageSetupWidthEdit(String),
|
|
/// Live-edit of the paper height field.
|
|
PageSetupHeightEdit(String),
|
|
/// Apply the changes entered in Page Setup.
|
|
PageSetupCommit,
|
|
// ── Plot / Export ─────────────────────────────────────────────────────
|
|
/// Show the SVG save-file dialog and trigger export.
|
|
PlotExport,
|
|
/// Callback after the user picks (or cancels) the export path.
|
|
PlotExportPath(Option<std::path::PathBuf>),
|
|
}
|
|
|
|
impl H7CAD {
|
|
fn new() -> Self {
|
|
let first_tab = DocumentTab::new_drawing(1);
|
|
let mut app = Self {
|
|
start: Instant::now(),
|
|
tabs: vec![first_tab],
|
|
active_tab: 0,
|
|
tab_counter: 1,
|
|
ribbon: Ribbon::new(),
|
|
app_menu: AppMenu::new(),
|
|
command_line: CommandLine::new(),
|
|
status_bar: StatusBar::new(),
|
|
cursor_pos: Point::ORIGIN,
|
|
vp_size: (1280.0, 720.0),
|
|
snapper: Snapper::default(),
|
|
snap_popup_open: false,
|
|
pre_cmd_tangent: None,
|
|
ortho_mode: false,
|
|
polar_mode: false,
|
|
show_grid: false,
|
|
last_point: None,
|
|
layer_window: None,
|
|
main_window: None,
|
|
clipboard: Vec::new(),
|
|
clipboard_centroid: glam::Vec3::ZERO,
|
|
layout_context_menu: None,
|
|
layout_rename_state: None,
|
|
last_vp_click_time: None,
|
|
last_vp_click_pos: None,
|
|
page_setup_open: false,
|
|
page_setup_w: String::new(),
|
|
page_setup_h: String::new(),
|
|
};
|
|
app.sync_ribbon_layers();
|
|
app
|
|
}
|
|
|
|
/// Boot function for `iced::daemon`: returns initial state plus a task that
|
|
/// opens the primary application window.
|
|
fn boot() -> (Self, Task<Message>) {
|
|
use helpers::build_window_icon;
|
|
let state = Self::new();
|
|
let (id, open_task) = window::open(window::Settings {
|
|
maximized: true,
|
|
icon: window::icon::from_rgba(build_window_icon(), 32, 32).ok(),
|
|
..Default::default()
|
|
});
|
|
let mut s = state;
|
|
s.main_window = Some(id);
|
|
let task = open_task.map(|_| Message::Noop);
|
|
(s, task)
|
|
}
|
|
}
|
|
|
|
use std::path::PathBuf;
|
|
|
|
pub fn run() -> iced::Result {
|
|
iced::daemon(H7CAD::boot, H7CAD::update, H7CAD::view)
|
|
.subscription(H7CAD::subscription)
|
|
.title(|state: &H7CAD, window_id: window::Id| {
|
|
if Some(window_id) == state.layer_window {
|
|
"Layer Properties Manager".to_string()
|
|
} else if let Some(tab) = state.tabs.get(state.active_tab) {
|
|
let dot = if tab.dirty { "● " } else { "" };
|
|
let name = tab.tab_display_name();
|
|
format!("{}H7CAD — {}", dot, name)
|
|
} else {
|
|
"H7CAD".to_string()
|
|
}
|
|
})
|
|
.theme(Theme::Dark)
|
|
.run()
|
|
}
|