4523 lines
191 KiB
Rust
4523 lines
191 KiB
Rust
|
|
mod command;
|
|||
|
|
mod entities;
|
|||
|
|
mod io;
|
|||
|
|
mod linetypes;
|
|||
|
|
mod modules;
|
|||
|
|
mod patterns;
|
|||
|
|
mod scene;
|
|||
|
|
mod snap;
|
|||
|
|
mod ui;
|
|||
|
|
|
|||
|
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
|||
|
|
use acadrust::{CadDocument, EntityType, Handle};
|
|||
|
|
use command::{CadCommand, CmdResult};
|
|||
|
|
use modules::ModuleEvent;
|
|||
|
|
use scene::dispatch;
|
|||
|
|
use scene::grip::{find_hit_grip, grips_to_screen, GripEdit};
|
|||
|
|
use scene::object::GripApply;
|
|||
|
|
use snap::{SnapResult, Snapper};
|
|||
|
|
use std::collections::HashSet;
|
|||
|
|
use std::path::PathBuf;
|
|||
|
|
|
|||
|
|
use scene::{CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX};
|
|||
|
|
use ui::{overlay, AppMenu, CommandLine, LayerPanel, PropertiesPanel, Ribbon, StatusBar};
|
|||
|
|
|
|||
|
|
use iced::time::Instant;
|
|||
|
|
use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row};
|
|||
|
|
use iced::window;
|
|||
|
|
use iced::{mouse, Background, Border, Color, Element, Fill, Point, Subscription, Task, Theme};
|
|||
|
|
|
|||
|
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
|||
|
|
const POLY_START_DELAY_MS: u128 = 150;
|
|||
|
|
const VARIES_LABEL: &str = "*VARIES*";
|
|||
|
|
|
|||
|
|
/// Parse a typed coordinate string into a world-space Vec3.
|
|||
|
|
/// Accepts "x,y" → Vec3(x, 0, y) (XZ drawing plane, Y-up)
|
|||
|
|
/// "x,y,z"→ Vec3(x, y, z) (full 3D)
|
|||
|
|
/// Separators: comma or semicolon; decimal point or decimal comma.
|
|||
|
|
fn parse_coord(text: &str) -> Option<glam::Vec3> {
|
|||
|
|
// Split on comma or semicolon, ignore spaces.
|
|||
|
|
let parts: Vec<f32> = text
|
|||
|
|
.split(|c| c == ',' || c == ';')
|
|||
|
|
.map(|s| s.trim().replace(',', "."))
|
|||
|
|
.filter_map(|s| s.parse().ok())
|
|||
|
|
.collect();
|
|||
|
|
match parts.as_slice() {
|
|||
|
|
[x, y] => Some(glam::Vec3::new(*x, *y, 0.0)),
|
|||
|
|
[x, y, z] => Some(glam::Vec3::new(*x, *y, *z)),
|
|||
|
|
_ => None,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn angle_close(a: f32, b: f32, tol: f32) -> bool {
|
|||
|
|
let diff = (a - b).rem_euclid(std::f32::consts::TAU);
|
|||
|
|
let diff = if diff > std::f32::consts::PI {
|
|||
|
|
diff - std::f32::consts::TAU
|
|||
|
|
} else {
|
|||
|
|
diff
|
|||
|
|
};
|
|||
|
|
diff.abs() < tol
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn main() -> 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()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Per-document tab state ─────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
struct DocumentTab {
|
|||
|
|
scene: Scene,
|
|||
|
|
current_path: Option<PathBuf>,
|
|||
|
|
dirty: bool,
|
|||
|
|
tab_title: String,
|
|||
|
|
properties: PropertiesPanel,
|
|||
|
|
layers: LayerPanel,
|
|||
|
|
active_cmd: Option<Box<dyn CadCommand>>,
|
|||
|
|
last_cmd: Option<String>,
|
|||
|
|
snap_result: Option<SnapResult>,
|
|||
|
|
active_grip: Option<GripEdit>,
|
|||
|
|
selected_grips: Vec<scene::GripDef>,
|
|||
|
|
selected_handle: Option<Handle>,
|
|||
|
|
wireframe: bool,
|
|||
|
|
visual_style: String,
|
|||
|
|
last_cursor_world: glam::Vec3,
|
|||
|
|
history: HistoryState,
|
|||
|
|
active_layer: String,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl DocumentTab {
|
|||
|
|
fn new_drawing(n: usize) -> Self {
|
|||
|
|
let mut scene = Scene::new();
|
|||
|
|
linetypes::populate_document(&mut scene.document);
|
|||
|
|
Self {
|
|||
|
|
scene,
|
|||
|
|
current_path: None,
|
|||
|
|
dirty: false,
|
|||
|
|
tab_title: format!("Drawing{}", n),
|
|||
|
|
properties: PropertiesPanel::empty(),
|
|||
|
|
layers: LayerPanel::default(),
|
|||
|
|
active_cmd: None,
|
|||
|
|
last_cmd: None,
|
|||
|
|
snap_result: None,
|
|||
|
|
active_grip: None,
|
|||
|
|
selected_grips: vec![],
|
|||
|
|
selected_handle: None,
|
|||
|
|
wireframe: false,
|
|||
|
|
visual_style: "Shaded".into(),
|
|||
|
|
last_cursor_world: glam::Vec3::ZERO,
|
|||
|
|
history: HistoryState::default(),
|
|||
|
|
active_layer: "0".to_string(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn tab_display_name(&self) -> String {
|
|||
|
|
match &self.current_path {
|
|||
|
|
Some(p) => p
|
|||
|
|
.file_name()
|
|||
|
|
.unwrap_or_default()
|
|||
|
|
.to_string_lossy()
|
|||
|
|
.to_string(),
|
|||
|
|
None => self.tab_title.clone(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Clone)]
|
|||
|
|
struct HistorySnapshot {
|
|||
|
|
document: CadDocument,
|
|||
|
|
current_layout: String,
|
|||
|
|
selected: Vec<Handle>,
|
|||
|
|
dirty: bool,
|
|||
|
|
label: String,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Default)]
|
|||
|
|
struct HistoryState {
|
|||
|
|
undo_stack: Vec<HistorySnapshot>,
|
|||
|
|
redo_stack: Vec<HistorySnapshot>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Application state ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[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,
|
|||
|
|
/// Emitted when the main application window has been opened (daemon boot).
|
|||
|
|
MainWindowOpened(window::Id),
|
|||
|
|
/// Emitted when the layer window has been successfully opened.
|
|||
|
|
LayerWindowOpened(window::Id),
|
|||
|
|
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(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(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,
|
|||
|
|
/// Switch to a named layout ("Model" or paper space layout name).
|
|||
|
|
LayoutSwitch(String),
|
|||
|
|
/// Create a new paper space layout.
|
|||
|
|
LayoutCreate,
|
|||
|
|
/// 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,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
};
|
|||
|
|
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>) {
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn update(&mut self, msg: Message) -> Task<Message> {
|
|||
|
|
match msg {
|
|||
|
|
Message::Tick(t) => {
|
|||
|
|
self.tabs[self.active_tab].scene.update(t - self.start);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::OpenFile => Task::perform(io::pick_and_open(), Message::FileOpened),
|
|||
|
|
|
|||
|
|
Message::FileOpened(Ok((name, path, doc))) => {
|
|||
|
|
let entity_count = doc.entities().count();
|
|||
|
|
self.command_line
|
|||
|
|
.push_output(&format!("Opened \"{name}\" — {entity_count} entities"));
|
|||
|
|
self.app_menu.push_recent(path.clone());
|
|||
|
|
|
|||
|
|
// Eğer aktif sekme boşsa (kaydedilmemiş, yeni çizim, entity yok) dosyayı oraya yükle.
|
|||
|
|
// Aksi hâlde yeni bir sekme aç.
|
|||
|
|
let current_is_empty = {
|
|||
|
|
let t = &self.tabs[self.active_tab];
|
|||
|
|
t.current_path.is_none()
|
|||
|
|
&& !t.dirty
|
|||
|
|
&& self.tabs[self.active_tab].scene.document.entities().count() == 0
|
|||
|
|
};
|
|||
|
|
let i = if current_is_empty {
|
|||
|
|
self.active_tab
|
|||
|
|
} else {
|
|||
|
|
self.tab_counter += 1;
|
|||
|
|
let new_tab = DocumentTab::new_drawing(self.tab_counter);
|
|||
|
|
self.tabs.push(new_tab);
|
|||
|
|
let idx = self.tabs.len() - 1;
|
|||
|
|
self.active_tab = idx;
|
|||
|
|
idx
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
self.tabs[i].current_path = Some(path);
|
|||
|
|
self.tabs[i].scene.document = doc;
|
|||
|
|
self.tabs[i].scene.populate_hatches_from_document();
|
|||
|
|
self.tabs[i].scene.selected = std::collections::HashSet::new();
|
|||
|
|
self.tabs[i].scene.preview_wires = vec![];
|
|||
|
|
self.tabs[i].scene.current_layout = "Model".to_string();
|
|||
|
|
// Merge standard linetypes not already present in the file.
|
|||
|
|
linetypes::populate_document(&mut self.tabs[i].scene.document);
|
|||
|
|
self.tabs[i].properties = PropertiesPanel::empty();
|
|||
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|||
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
self.tabs[i].scene.fit_all();
|
|||
|
|
self.tabs[i].dirty = false;
|
|||
|
|
self.tabs[i].history = HistoryState::default();
|
|||
|
|
self.refresh_selected_grips();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::FileOpened(Err(e)) => {
|
|||
|
|
if e != "Cancelled" {
|
|||
|
|
self.command_line.push_error(&format!("Open failed: {e}"));
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::SaveFile => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if let Some(path) = &self.tabs[i].current_path {
|
|||
|
|
let path = path.clone();
|
|||
|
|
match io::save(&self.tabs[i].scene.document, &path) {
|
|||
|
|
Ok(()) => {
|
|||
|
|
self.command_line
|
|||
|
|
.push_output(&format!("Saved: {}", path.display()));
|
|||
|
|
self.tabs[i].dirty = false;
|
|||
|
|
}
|
|||
|
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
return Task::perform(io::pick_save_path(), Message::PickedSavePath);
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::SaveAs => Task::perform(io::pick_save_path(), Message::PickedSavePath),
|
|||
|
|
|
|||
|
|
Message::PickedSavePath(Some(path)) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
match io::save(&self.tabs[i].scene.document, &path) {
|
|||
|
|
Ok(()) => {
|
|||
|
|
self.command_line
|
|||
|
|
.push_output(&format!("Saved: {}", path.display()));
|
|||
|
|
self.tabs[i].current_path = Some(path);
|
|||
|
|
self.tabs[i].dirty = false;
|
|||
|
|
}
|
|||
|
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PickedSavePath(None) => Task::none(),
|
|||
|
|
|
|||
|
|
Message::ClearScene => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.push_undo_snapshot(i, "NEW");
|
|||
|
|
self.tabs[i].scene.clear();
|
|||
|
|
linetypes::populate_document(&mut self.tabs[i].scene.document);
|
|||
|
|
self.tabs[i].properties = PropertiesPanel::empty();
|
|||
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|||
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|||
|
|
self.command_line
|
|||
|
|
.push_output("Scene cleared. Standard linetypes loaded.");
|
|||
|
|
self.tabs[i].current_path = None;
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::SetWireframe(w) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].wireframe = w;
|
|||
|
|
self.ribbon.set_wireframe(w);
|
|||
|
|
self.tabs[i].visual_style = if w {
|
|||
|
|
"Wireframe".into()
|
|||
|
|
} else {
|
|||
|
|
"Shaded".into()
|
|||
|
|
};
|
|||
|
|
self.command_line.push_output(if w {
|
|||
|
|
"Visual style: Wireframe"
|
|||
|
|
} else {
|
|||
|
|
"Visual style: Shaded"
|
|||
|
|
});
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::SetProjection(ortho) => {
|
|||
|
|
use crate::scene::Projection;
|
|||
|
|
let proj = if ortho {
|
|||
|
|
Projection::Orthographic
|
|||
|
|
} else {
|
|||
|
|
Projection::Perspective
|
|||
|
|
};
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].scene.camera.borrow_mut().projection = proj;
|
|||
|
|
self.tabs[i].scene.camera_generation += 1;
|
|||
|
|
self.ribbon.set_ortho(ortho);
|
|||
|
|
self.command_line.push_output(if ortho {
|
|||
|
|
"Projection: Orthographic"
|
|||
|
|
} else {
|
|||
|
|
"Projection: Perspective"
|
|||
|
|
});
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::RibbonSelectTab(idx) => {
|
|||
|
|
self.ribbon.select(idx);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::RibbonToolClick { tool_id, event } => {
|
|||
|
|
self.ribbon.activate_tool(&tool_id);
|
|||
|
|
match event {
|
|||
|
|
ModuleEvent::Command(cmd) => return self.dispatch_command(&cmd),
|
|||
|
|
ModuleEvent::OpenFileDialog => {
|
|||
|
|
self.command_line
|
|||
|
|
.push_info("Open DWG/DXF: not yet implemented.");
|
|||
|
|
}
|
|||
|
|
ModuleEvent::ClearModels => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].scene.clear();
|
|||
|
|
self.tabs[i].properties = PropertiesPanel::empty();
|
|||
|
|
self.command_line.push_output("Scene cleared.");
|
|||
|
|
}
|
|||
|
|
ModuleEvent::SetWireframe(w) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].wireframe = w;
|
|||
|
|
self.ribbon.set_wireframe(w);
|
|||
|
|
self.tabs[i].visual_style = if w {
|
|||
|
|
"Wireframe".into()
|
|||
|
|
} else {
|
|||
|
|
"Shaded".into()
|
|||
|
|
};
|
|||
|
|
self.command_line.push_output(if w {
|
|||
|
|
"Visual style: Wireframe"
|
|||
|
|
} else {
|
|||
|
|
"Visual style: Shaded"
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
ModuleEvent::ToggleLayers => {
|
|||
|
|
return Task::done(Message::ToggleLayers);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Application menu ──────────────────────────────────────────
|
|||
|
|
Message::ToggleAppMenu => {
|
|||
|
|
self.app_menu.toggle();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::CloseAppMenu => {
|
|||
|
|
self.app_menu.close();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::CloseAppMenuAndRun(cmd) => {
|
|||
|
|
self.app_menu.close();
|
|||
|
|
self.dispatch_command(&cmd.clone())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::AppMenuSearch(s) => {
|
|||
|
|
self.app_menu.search = s;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Document tabs ─────────────────────────────────────────────
|
|||
|
|
Message::TabNew => {
|
|||
|
|
self.tab_counter += 1;
|
|||
|
|
let new_tab = DocumentTab::new_drawing(self.tab_counter);
|
|||
|
|
self.tabs.push(new_tab);
|
|||
|
|
self.active_tab = self.tabs.len() - 1;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::TabSwitch(idx) => {
|
|||
|
|
if idx < self.tabs.len() {
|
|||
|
|
self.active_tab = idx;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::TabClose(idx) => {
|
|||
|
|
if self.tabs.len() == 1 {
|
|||
|
|
// Only one tab — reset it to a fresh drawing instead of closing
|
|||
|
|
self.tab_counter += 1;
|
|||
|
|
self.tabs[0] = DocumentTab::new_drawing(self.tab_counter);
|
|||
|
|
self.active_tab = 0;
|
|||
|
|
} else {
|
|||
|
|
self.tabs.remove(idx);
|
|||
|
|
if self.active_tab >= self.tabs.len() {
|
|||
|
|
self.active_tab = self.tabs.len() - 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
Message::CommandInput(s) => {
|
|||
|
|
self.command_line.input = s;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::CommandSubmit => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if self.tabs[i].active_cmd.is_some() {
|
|||
|
|
let text = self.command_line.input.trim().to_string();
|
|||
|
|
self.command_line.input.clear();
|
|||
|
|
|
|||
|
|
// 1. Command explicitly owns text input (e.g. TTR radius prompt).
|
|||
|
|
if self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|c| c.wants_text_input())
|
|||
|
|
.unwrap_or(false)
|
|||
|
|
{
|
|||
|
|
if let Some(result) = self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_mut()
|
|||
|
|
.and_then(|c| c.on_text_input(&text))
|
|||
|
|
{
|
|||
|
|
return self.apply_cmd_result(result);
|
|||
|
|
}
|
|||
|
|
// Step accepted but no final result yet — show updated prompt and refresh preview.
|
|||
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|||
|
|
if let Some(p) = prompt {
|
|||
|
|
self.command_line.push_info(&p);
|
|||
|
|
}
|
|||
|
|
let pt = self.tabs[i].last_cursor_world;
|
|||
|
|
let previews = self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_mut()
|
|||
|
|
.map(|c| c.on_preview_wires(pt))
|
|||
|
|
.unwrap_or_default();
|
|||
|
|
self.tabs[i].scene.set_preview_wires(previews);
|
|||
|
|
return self.focus_cmd_input();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. Empty input → Enter (finish / cancel).
|
|||
|
|
if text.is_empty() {
|
|||
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter());
|
|||
|
|
if let Some(r) = result {
|
|||
|
|
return self.apply_cmd_result(r);
|
|||
|
|
}
|
|||
|
|
return Task::none();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. Coordinate input "x,y" or "x,y,z" → on_point.
|
|||
|
|
if let Some(pt) = parse_coord(&text) {
|
|||
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
|||
|
|
if let Some(r) = result {
|
|||
|
|
return self.apply_cmd_result(r);
|
|||
|
|
}
|
|||
|
|
return Task::none();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. Numeric input (e.g. radius, angle, scale factor) → on_text_input.
|
|||
|
|
if let Some(result) = self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_mut()
|
|||
|
|
.and_then(|c| c.on_text_input(&text))
|
|||
|
|
{
|
|||
|
|
return self.apply_cmd_result(result);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. Nothing matched — show hint.
|
|||
|
|
self.command_line.push_error(&format!(
|
|||
|
|
"Expected coordinates (x,y) or a number, got: \"{text}\""
|
|||
|
|
));
|
|||
|
|
return self.focus_cmd_input();
|
|||
|
|
}
|
|||
|
|
if let Some(cmd) = self.command_line.submit() {
|
|||
|
|
return self.dispatch_command(&cmd);
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::CommandFinalize => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if self.tabs[i].active_cmd.is_some() {
|
|||
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter());
|
|||
|
|
if let Some(r) = result {
|
|||
|
|
return self.apply_cmd_result(r);
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
} else if let Some(cmd) = self.tabs[i].last_cmd.clone() {
|
|||
|
|
self.dispatch_command(&cmd)
|
|||
|
|
} else {
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::CommandEscape => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if self.tabs[i].active_cmd.is_some() {
|
|||
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_escape());
|
|||
|
|
if let Some(r) = result {
|
|||
|
|
return self.apply_cmd_result(r);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// No active command → Escape deselects everything.
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
self.refresh_properties();
|
|||
|
|
// Also cancel any in-progress selection box.
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.box_anchor = None;
|
|||
|
|
sel.box_current = None;
|
|||
|
|
sel.box_crossing = false;
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::Command(cmd) => self.dispatch_command(&cmd),
|
|||
|
|
|
|||
|
|
Message::ToggleLayers => {
|
|||
|
|
if let Some(id) = self.layer_window.take() {
|
|||
|
|
// Window already open — close it.
|
|||
|
|
window::close(id)
|
|||
|
|
} else {
|
|||
|
|
// Sync layer/linetype data before opening so the panel is up to date.
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
// Open a new OS window for the Layer Properties Manager.
|
|||
|
|
let (id, task) = window::open(window::Settings {
|
|||
|
|
size: iced::Size::new(900.0, 360.0),
|
|||
|
|
resizable: true,
|
|||
|
|
..Default::default()
|
|||
|
|
});
|
|||
|
|
self.layer_window = Some(id);
|
|||
|
|
task.map(|_| Message::Noop)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::MainWindowOpened(id) => {
|
|||
|
|
self.main_window = Some(id);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerWindowOpened(_id) => Task::none(),
|
|||
|
|
|
|||
|
|
Message::OsWindowClosed(id) => {
|
|||
|
|
if self.main_window == Some(id) {
|
|||
|
|
// Main window closed — exit the process entirely.
|
|||
|
|
return iced::exit();
|
|||
|
|
}
|
|||
|
|
if self.layer_window == Some(id) {
|
|||
|
|
self.layer_window = None;
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerToggleVisible(idx) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if idx < self.tabs[i].layers.layers.len() {
|
|||
|
|
self.push_undo_snapshot(i, "LAYER OFF/ON");
|
|||
|
|
let l = &mut self.tabs[i].layers.layers[idx];
|
|||
|
|
l.visible = !l.visible;
|
|||
|
|
let name = l.name.clone();
|
|||
|
|
let on = l.visible;
|
|||
|
|
self.tabs[i].scene.toggle_layer_visibility(&name);
|
|||
|
|
self.command_line.push_output(&format!(
|
|||
|
|
"Layer \"{}\" {}",
|
|||
|
|
name,
|
|||
|
|
if on { "on" } else { "off" }
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerToggleLock(idx) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if idx < self.tabs[i].layers.layers.len() {
|
|||
|
|
self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK");
|
|||
|
|
let l = &mut self.tabs[i].layers.layers[idx];
|
|||
|
|
l.locked = !l.locked;
|
|||
|
|
let name = l.name.clone();
|
|||
|
|
let locked = l.locked;
|
|||
|
|
self.tabs[i].scene.toggle_layer_lock(&name);
|
|||
|
|
self.command_line.push_output(&format!(
|
|||
|
|
"Layer \"{}\" {}",
|
|||
|
|
name,
|
|||
|
|
if locked { "locked" } else { "unlocked" }
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerToggleFreeze(idx) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if idx < self.tabs[i].layers.layers.len() {
|
|||
|
|
self.push_undo_snapshot(i, "LAYER FREEZE");
|
|||
|
|
let l = &mut self.tabs[i].layers.layers[idx];
|
|||
|
|
l.frozen = !l.frozen;
|
|||
|
|
let name = l.name.clone();
|
|||
|
|
let frozen = l.frozen;
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|||
|
|
if frozen { dl.freeze(); } else { dl.thaw(); }
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerNew => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
// Find unique name
|
|||
|
|
let mut n = 1;
|
|||
|
|
let new_name = loop {
|
|||
|
|
let candidate = format!("Layer{}", n);
|
|||
|
|
if !self.tabs[i].scene.document.layers.contains(&candidate) {
|
|||
|
|
break candidate;
|
|||
|
|
}
|
|||
|
|
n += 1;
|
|||
|
|
};
|
|||
|
|
self.push_undo_snapshot(i, "LAYER NEW");
|
|||
|
|
use acadrust::tables::layer::Layer as DocLayer;
|
|||
|
|
let _ = self.tabs[i].scene.document.layers.add(DocLayer::new(&new_name));
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|||
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|||
|
|
// Select and start editing the new layer
|
|||
|
|
let new_idx = self.tabs[i].layers.layers.iter()
|
|||
|
|
.position(|l| l.name == new_name);
|
|||
|
|
if let Some(idx) = new_idx {
|
|||
|
|
self.tabs[i].layers.selected = Some(idx);
|
|||
|
|
self.tabs[i].layers.editing = Some(idx);
|
|||
|
|
self.tabs[i].layers.edit_buf = new_name.clone();
|
|||
|
|
}
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerDelete => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|||
|
|
let name = self.tabs[i].layers.layers.get(idx)
|
|||
|
|
.map(|l| l.name.clone())
|
|||
|
|
.unwrap_or_default();
|
|||
|
|
if name == "0" { return Task::none(); }
|
|||
|
|
self.push_undo_snapshot(i, "LAYER DELETE");
|
|||
|
|
self.tabs[i].scene.document.layers.remove(&name);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|||
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|||
|
|
self.tabs[i].layers.selected = None;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerSetCurrent => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|||
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|||
|
|
let name = layer.name.clone();
|
|||
|
|
self.tabs[i].active_layer = name.clone();
|
|||
|
|
self.tabs[i].layers.current_layer = name.clone();
|
|||
|
|
self.ribbon.active_layer = name;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerSelect(idx) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
// Commit any pending rename first
|
|||
|
|
if self.tabs[i].layers.editing.is_some() {
|
|||
|
|
return Task::done(Message::LayerRenameCommit);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].layers.selected = Some(idx);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerRenameStart(idx) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].layers.selected = Some(idx);
|
|||
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|||
|
|
self.tabs[i].layers.edit_buf = layer.name.clone();
|
|||
|
|
}
|
|||
|
|
self.tabs[i].layers.editing = Some(idx);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerRenameEdit(s) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].layers.edit_buf = s;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerRenameCommit => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let editing_idx = self.tabs[i].layers.editing.take();
|
|||
|
|
if let Some(idx) = editing_idx {
|
|||
|
|
let new_name = self.tabs[i].layers.edit_buf.trim().to_string();
|
|||
|
|
let old_name = self.tabs[i].layers.layers.get(idx)
|
|||
|
|
.map(|l| l.name.clone())
|
|||
|
|
.unwrap_or_default();
|
|||
|
|
if !new_name.is_empty() && new_name != old_name
|
|||
|
|
&& !self.tabs[i].scene.document.layers.contains(&new_name)
|
|||
|
|
{
|
|||
|
|
self.push_undo_snapshot(i, "LAYER RENAME");
|
|||
|
|
// acadrust Table doesn't have rename; add new + remove old
|
|||
|
|
if let Some(old_layer) = self.tabs[i].scene.document.layers.get(&old_name) {
|
|||
|
|
use acadrust::tables::layer::Layer as DocLayer;
|
|||
|
|
let mut nl = DocLayer::new(&new_name);
|
|||
|
|
nl.color = old_layer.color.clone();
|
|||
|
|
nl.flags = old_layer.flags.clone();
|
|||
|
|
let _ = self.tabs[i].scene.document.layers.add(nl);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].scene.document.layers.remove(&old_name);
|
|||
|
|
// Update entities on that layer
|
|||
|
|
for e in self.tabs[i].scene.document.entities_mut() {
|
|||
|
|
if e.as_entity().layer() == old_name {
|
|||
|
|
e.as_entity_mut().set_layer(new_name.clone());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
}
|
|||
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|||
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|||
|
|
self.tabs[i].layers.edit_buf.clear();
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerColorPickerToggle(idx) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let panel = &mut self.tabs[i].layers;
|
|||
|
|
if panel.color_picker_row == Some(idx) {
|
|||
|
|
panel.color_picker_row = None;
|
|||
|
|
panel.color_full_palette = false;
|
|||
|
|
} else {
|
|||
|
|
panel.color_picker_row = Some(idx);
|
|||
|
|
panel.color_full_palette = false;
|
|||
|
|
panel.selected = Some(idx);
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerColorMorePalette => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].layers.color_full_palette =
|
|||
|
|
!self.tabs[i].layers.color_full_palette;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerColorSet(aci) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|||
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|||
|
|
let name = layer.name.clone();
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|||
|
|
dl.color = AcadColor::Index(aci);
|
|||
|
|
}
|
|||
|
|
use crate::ui::layers::iced_color_from_acad;
|
|||
|
|
let new_color = iced_color_from_acad(&AcadColor::Index(aci));
|
|||
|
|
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
|||
|
|
pl.color = new_color;
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
}
|
|||
|
|
self.tabs[i].layers.color_picker_row = None;
|
|||
|
|
self.tabs[i].layers.color_full_palette = false;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerLinetypeSet(lt) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|||
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|||
|
|
let name = layer.name.clone();
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|||
|
|
dl.line_type = lt.clone();
|
|||
|
|
}
|
|||
|
|
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
|||
|
|
pl.linetype = lt;
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerLineweightSet(lw) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|||
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|||
|
|
let name = layer.name.clone();
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|||
|
|
dl.line_weight = lw;
|
|||
|
|
}
|
|||
|
|
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
|||
|
|
pl.lineweight = lw;
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayerTransparencyEdit(idx, s) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
|||
|
|
if let Ok(v) = s.parse::<i32>() {
|
|||
|
|
pl.transparency = v.clamp(0, 90);
|
|||
|
|
} else if s.is_empty() {
|
|||
|
|
pl.transparency = 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::CursorMoved(p) => {
|
|||
|
|
let (vw, _vh) = self.tabs[self.active_tab].scene.selection.borrow().vp_size;
|
|||
|
|
self.cursor_pos = Point::new(
|
|||
|
|
vw - VIEWCUBE_PAD - VIEWCUBE_HIT_SIZE + p.x,
|
|||
|
|
VIEWCUBE_PAD + p.y,
|
|||
|
|
);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportMove(p) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.last_move_pos = Some(p);
|
|||
|
|
|
|||
|
|
if sel.left_down {
|
|||
|
|
let press = sel.left_press_pos.unwrap_or(p);
|
|||
|
|
let dx = p.x - press.x;
|
|||
|
|
let dy = p.y - press.y;
|
|||
|
|
let dist2 = dx * dx + dy * dy;
|
|||
|
|
let elapsed_ms = sel
|
|||
|
|
.left_press_time
|
|||
|
|
.map(|t| Instant::now().duration_since(t).as_millis())
|
|||
|
|
.unwrap_or(u128::MAX);
|
|||
|
|
if !sel.left_dragging && elapsed_ms >= POLY_START_DELAY_MS && dist2 > 9.0 {
|
|||
|
|
sel.left_dragging = true;
|
|||
|
|
sel.poly_active = true;
|
|||
|
|
sel.poly_crossing = p.x < press.x;
|
|||
|
|
sel.poly_points.clear();
|
|||
|
|
sel.poly_points.push(press);
|
|||
|
|
sel.poly_points.push(p);
|
|||
|
|
} else if sel.left_dragging && sel.poly_active {
|
|||
|
|
if sel.poly_points.last().map_or(true, |lp| {
|
|||
|
|
let ddx = p.x - lp.x;
|
|||
|
|
let ddy = p.y - lp.y;
|
|||
|
|
ddx * ddx + ddy * ddy > 16.0
|
|||
|
|
}) {
|
|||
|
|
sel.poly_points.push(p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else if sel.box_anchor.is_some() {
|
|||
|
|
sel.box_current = Some(p);
|
|||
|
|
if let Some(a) = sel.box_anchor {
|
|||
|
|
sel.box_crossing = p.x < a.x;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if sel.right_down {
|
|||
|
|
if let Some(press) = sel.right_press_pos {
|
|||
|
|
let dx = p.x - press.x;
|
|||
|
|
let dy = p.y - press.y;
|
|||
|
|
if !sel.right_dragging && (dx * dx + dy * dy) > 9.0 {
|
|||
|
|
sel.right_dragging = true;
|
|||
|
|
sel.context_menu = None;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if sel.right_dragging {
|
|||
|
|
if let Some(last) = sel.right_last_pos {
|
|||
|
|
let (dx, dy) = (p.x - last.x, p.y - last.y);
|
|||
|
|
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy);
|
|||
|
|
}
|
|||
|
|
sel.right_last_pos = Some(p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if sel.middle_down {
|
|||
|
|
if let Some(last) = sel.middle_last_pos {
|
|||
|
|
let (dx, dy) = (p.x - last.x, p.y - last.y);
|
|||
|
|
self.tabs[i].scene.camera.borrow_mut().pan(dx, dy);
|
|||
|
|
}
|
|||
|
|
sel.middle_last_pos = Some(p);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let vp_size = sel.vp_size;
|
|||
|
|
drop(sel);
|
|||
|
|
|
|||
|
|
// ── Grip drag ─────────────────────────────────────────────
|
|||
|
|
if let Some(grip) = self.tabs[i].active_grip.clone() {
|
|||
|
|
let (vw, vh) = vp_size;
|
|||
|
|
let bounds = iced::Rectangle {
|
|||
|
|
x: 0.0,
|
|||
|
|
y: 0.0,
|
|||
|
|
width: vw,
|
|||
|
|
height: vh,
|
|||
|
|
};
|
|||
|
|
let cam = self.tabs[i].scene.camera.borrow();
|
|||
|
|
let raw = cam.pick_on_target_plane(p, bounds);
|
|||
|
|
let vp_mat = cam.view_proj(bounds);
|
|||
|
|
drop(cam);
|
|||
|
|
|
|||
|
|
// Exclude the entity being dragged so its own segments
|
|||
|
|
// don't interfere with snap (avoids Nearest/Endpoint sticking).
|
|||
|
|
let edited_name = grip.handle.value().to_string();
|
|||
|
|
let all_wires = self.tabs[i].scene.entity_wires();
|
|||
|
|
let snap_wires: Vec<_> = all_wires
|
|||
|
|
.iter()
|
|||
|
|
.filter(|w| w.name != edited_name)
|
|||
|
|
.cloned()
|
|||
|
|
.collect();
|
|||
|
|
let snap_hit = self.snapper.snap(raw, p, &snap_wires, vp_mat, bounds);
|
|||
|
|
let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw);
|
|||
|
|
self.tabs[i].snap_result = snap_hit;
|
|||
|
|
|
|||
|
|
// Apply ortho/polar constraints relative to the grip's origin position.
|
|||
|
|
// Only when snap did not find a geometry hit (snap takes priority).
|
|||
|
|
if snap_hit.is_none() {
|
|||
|
|
let base = grip.origin_world;
|
|||
|
|
if self.ortho_mode {
|
|||
|
|
snapped = ortho_constrain(snapped, base);
|
|||
|
|
} else if self.polar_mode {
|
|||
|
|
snapped = polar_constrain(snapped, base, 45.0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let apply = if grip.is_translate {
|
|||
|
|
GripApply::Translate(snapped - grip.last_world)
|
|||
|
|
} else {
|
|||
|
|
GripApply::Absolute(snapped)
|
|||
|
|
};
|
|||
|
|
self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.apply_grip(grip.handle, grip.grip_id, apply);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped;
|
|||
|
|
self.refresh_selected_grips();
|
|||
|
|
self.refresh_properties();
|
|||
|
|
return Task::none();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if self.tabs[i].active_cmd.is_some() {
|
|||
|
|
let (vw, vh) = vp_size;
|
|||
|
|
let bounds = iced::Rectangle {
|
|||
|
|
x: 0.0,
|
|||
|
|
y: 0.0,
|
|||
|
|
width: vw,
|
|||
|
|
height: vh,
|
|||
|
|
};
|
|||
|
|
let cam = self.tabs[i].scene.camera.borrow();
|
|||
|
|
let cursor_world = cam.pick_on_target_plane(p, bounds);
|
|||
|
|
let view_proj = cam.view_proj(bounds);
|
|||
|
|
drop(cam);
|
|||
|
|
|
|||
|
|
let all_wires = self.tabs[i].scene.entity_wires();
|
|||
|
|
let needs_tan = self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|c| c.needs_tangent_pick())
|
|||
|
|
.unwrap_or(false);
|
|||
|
|
self.tabs[i].snap_result = if needs_tan {
|
|||
|
|
self.snapper.snap_tangent_only(
|
|||
|
|
cursor_world,
|
|||
|
|
p,
|
|||
|
|
&all_wires,
|
|||
|
|
view_proj,
|
|||
|
|
bounds,
|
|||
|
|
)
|
|||
|
|
} else {
|
|||
|
|
self.snapper
|
|||
|
|
.snap(cursor_world, p, &all_wires, view_proj, bounds)
|
|||
|
|
};
|
|||
|
|
let effective = {
|
|||
|
|
let mut pt = self.tabs[i]
|
|||
|
|
.snap_result
|
|||
|
|
.map(|s| s.world)
|
|||
|
|
.unwrap_or(cursor_world);
|
|||
|
|
if self.tabs[i].active_cmd.is_some() {
|
|||
|
|
pt.z = 0.0;
|
|||
|
|
}
|
|||
|
|
if let Some(base) = self.last_point {
|
|||
|
|
if self.ortho_mode {
|
|||
|
|
pt = ortho_constrain(pt, base);
|
|||
|
|
} else if self.polar_mode {
|
|||
|
|
pt = polar_constrain(pt, base, 45.0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
pt
|
|||
|
|
};
|
|||
|
|
self.tabs[i].last_cursor_world = effective;
|
|||
|
|
|
|||
|
|
let needs_entity = self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|c| c.needs_entity_pick())
|
|||
|
|
.unwrap_or(false);
|
|||
|
|
let previews = if needs_entity {
|
|||
|
|
let hover_handle =
|
|||
|
|
scene::hit_test::click_hit(p, &all_wires, view_proj, bounds)
|
|||
|
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
|||
|
|
.unwrap_or(acadrust::Handle::NULL);
|
|||
|
|
self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_mut()
|
|||
|
|
.map(|c| c.on_hover_entity(hover_handle, effective))
|
|||
|
|
.unwrap_or_default()
|
|||
|
|
} else {
|
|||
|
|
self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_mut()
|
|||
|
|
.map(|c| c.on_preview_wires(effective))
|
|||
|
|
.unwrap_or_default()
|
|||
|
|
};
|
|||
|
|
self.tabs[i].scene.set_preview_wires(previews);
|
|||
|
|
} else {
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportExit => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.left_down = false;
|
|||
|
|
sel.left_press_pos = None;
|
|||
|
|
sel.left_press_time = None;
|
|||
|
|
sel.left_dragging = false;
|
|||
|
|
sel.right_down = false;
|
|||
|
|
sel.right_press_pos = None;
|
|||
|
|
sel.right_last_pos = None;
|
|||
|
|
sel.right_dragging = false;
|
|||
|
|
sel.middle_down = false;
|
|||
|
|
sel.middle_last_pos = None;
|
|||
|
|
sel.box_anchor = None;
|
|||
|
|
sel.box_current = None;
|
|||
|
|
sel.box_crossing = false;
|
|||
|
|
sel.poly_active = false;
|
|||
|
|
sel.poly_points.clear();
|
|||
|
|
sel.poly_crossing = false;
|
|||
|
|
sel.context_menu = None;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportLeftPress => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let (p, vp_size) = {
|
|||
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|||
|
|
let p = match sel.last_move_pos {
|
|||
|
|
Some(p) => p,
|
|||
|
|
None => return Task::none(),
|
|||
|
|
};
|
|||
|
|
(p, sel.vp_size)
|
|||
|
|
};
|
|||
|
|
let (vw, vh) = vp_size;
|
|||
|
|
let bounds = iced::Rectangle {
|
|||
|
|
x: 0.0,
|
|||
|
|
y: 0.0,
|
|||
|
|
width: vw,
|
|||
|
|
height: vh,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Viewcube check.
|
|||
|
|
if vw > 1.0 && vh > 1.0 {
|
|||
|
|
let cam = self.tabs[i].scene.camera.borrow();
|
|||
|
|
if scene::hit_test(p.x, p.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX)
|
|||
|
|
.is_some()
|
|||
|
|
{
|
|||
|
|
return Task::none();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Grip hit-test: only when no command is active and one entity selected.
|
|||
|
|
if self.tabs[i].active_cmd.is_none() && !self.tabs[i].selected_grips.is_empty() {
|
|||
|
|
if let Some(handle) = self.tabs[i].selected_handle {
|
|||
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|||
|
|
let grip_hit =
|
|||
|
|
find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds);
|
|||
|
|
if let Some((grip_id, is_translate, world)) = grip_hit {
|
|||
|
|
self.tabs[i].active_grip = Some(GripEdit {
|
|||
|
|
handle,
|
|||
|
|
grip_id,
|
|||
|
|
is_translate,
|
|||
|
|
origin_world: world,
|
|||
|
|
last_world: world,
|
|||
|
|
});
|
|||
|
|
return Task::none(); // swallow press — selection box must not start
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.context_menu = None;
|
|||
|
|
sel.left_down = true;
|
|||
|
|
sel.left_press_pos = Some(p);
|
|||
|
|
sel.left_press_time = Some(Instant::now());
|
|||
|
|
sel.left_dragging = false;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportLeftRelease => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
// Extract the state we need before any mutable borrows.
|
|||
|
|
let (p, is_click, is_down) = {
|
|||
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|||
|
|
let p = match sel.last_move_pos {
|
|||
|
|
Some(p) => p,
|
|||
|
|
None => return Task::none(),
|
|||
|
|
};
|
|||
|
|
(p, !sel.left_dragging, sel.left_down)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ── Grip drag end ─────────────────────────────────────────
|
|||
|
|
if self.tabs[i].active_grip.is_some() {
|
|||
|
|
self.tabs[i].active_grip = None;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
return Task::none();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Whether the active command is in selection-gathering mode.
|
|||
|
|
let is_gathering = self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|c| c.is_selection_gathering())
|
|||
|
|
.unwrap_or(false);
|
|||
|
|
|
|||
|
|
// ── Active command: plain click = point pick ──────────────
|
|||
|
|
// Skip this path when gathering — let clicks flow into the
|
|||
|
|
// normal selection system below.
|
|||
|
|
if is_down && is_click && self.tabs[i].active_cmd.is_some() && !is_gathering {
|
|||
|
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
|||
|
|
let bounds = iced::Rectangle {
|
|||
|
|
x: 0.0,
|
|||
|
|
y: 0.0,
|
|||
|
|
width: vw,
|
|||
|
|
height: vh,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Take snap result once; preserve tangent_obj for pick routing.
|
|||
|
|
let snap_taken = self.tabs[i].snap_result.take();
|
|||
|
|
let tangent_obj_at_click = snap_taken.and_then(|s| s.tangent_obj);
|
|||
|
|
|
|||
|
|
let world_pt = {
|
|||
|
|
let raw = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.camera
|
|||
|
|
.borrow()
|
|||
|
|
.pick_on_target_plane(p, bounds);
|
|||
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|||
|
|
let all_wires = self.tabs[i].scene.entity_wires();
|
|||
|
|
let needs_tan = self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|c| c.needs_tangent_pick())
|
|||
|
|
.unwrap_or(false);
|
|||
|
|
let snap_hit = if needs_tan {
|
|||
|
|
self.snapper
|
|||
|
|
.snap_tangent_only(raw, p, &all_wires, vp_mat, bounds)
|
|||
|
|
} else {
|
|||
|
|
self.snapper.snap(raw, p, &all_wires, vp_mat, bounds)
|
|||
|
|
};
|
|||
|
|
let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw);
|
|||
|
|
pt.z = 0.0;
|
|||
|
|
if let Some(base) = self.last_point {
|
|||
|
|
if self.ortho_mode {
|
|||
|
|
pt = ortho_constrain(pt, base);
|
|||
|
|
} else if self.polar_mode {
|
|||
|
|
pt = polar_constrain(pt, base, 45.0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
pt
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let result = if self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|c| c.needs_entity_pick())
|
|||
|
|
.unwrap_or(false)
|
|||
|
|
{
|
|||
|
|
let vp_mat2 = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|||
|
|
let all_wires2 = self.tabs[i].scene.entity_wires();
|
|||
|
|
let hit = scene::hit_test::click_hit(p, &all_wires2, vp_mat2, bounds)
|
|||
|
|
.and_then(|s| Scene::handle_from_wire_name(s));
|
|||
|
|
if let Some(handle) = hit {
|
|||
|
|
self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_mut()
|
|||
|
|
.map(|c| c.on_entity_pick(handle, world_pt))
|
|||
|
|
} else {
|
|||
|
|
self.command_line.push_info("Nothing found at that point.");
|
|||
|
|
None
|
|||
|
|
}
|
|||
|
|
} else if self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|c| c.needs_tangent_pick())
|
|||
|
|
.unwrap_or(false)
|
|||
|
|
{
|
|||
|
|
let tangent_obj = tangent_obj_at_click;
|
|||
|
|
if let Some(obj) = tangent_obj {
|
|||
|
|
self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_mut()
|
|||
|
|
.map(|c| c.on_tangent_point(obj, world_pt))
|
|||
|
|
} else {
|
|||
|
|
self.command_line.push_info("Select a tangent object.");
|
|||
|
|
None
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
self.last_point = Some(world_pt);
|
|||
|
|
self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_mut()
|
|||
|
|
.map(|c| c.on_point(world_pt))
|
|||
|
|
};
|
|||
|
|
if let Some(r) = result {
|
|||
|
|
let task = self.apply_cmd_result(r);
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.left_down = false;
|
|||
|
|
sel.left_press_pos = None;
|
|||
|
|
sel.left_press_time = None;
|
|||
|
|
sel.left_dragging = false;
|
|||
|
|
return task;
|
|||
|
|
}
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.left_down = false;
|
|||
|
|
sel.left_press_pos = None;
|
|||
|
|
sel.left_press_time = None;
|
|||
|
|
sel.left_dragging = false;
|
|||
|
|
return Task::none();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let (is_down2, is_dragging, box_anchor, box_crossing, vp_size, elapsed_ms) = {
|
|||
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|||
|
|
let elapsed = sel
|
|||
|
|
.left_press_time
|
|||
|
|
.map(|t| Instant::now().duration_since(t).as_millis())
|
|||
|
|
.unwrap_or(u128::MAX);
|
|||
|
|
(
|
|||
|
|
sel.left_down,
|
|||
|
|
sel.left_dragging,
|
|||
|
|
sel.box_anchor,
|
|||
|
|
sel.box_crossing,
|
|||
|
|
sel.vp_size,
|
|||
|
|
elapsed,
|
|||
|
|
)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// True when a selection action fully completed this release event,
|
|||
|
|
// meaning the gathering command should fire immediately.
|
|||
|
|
let mut selection_just_completed = false;
|
|||
|
|
|
|||
|
|
if is_down2 {
|
|||
|
|
let bounds = iced::Rectangle {
|
|||
|
|
x: 0.0,
|
|||
|
|
y: 0.0,
|
|||
|
|
width: vp_size.0,
|
|||
|
|
height: vp_size.1,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if is_dragging {
|
|||
|
|
// ── Drag ended ────────────────────────────────────
|
|||
|
|
if elapsed_ms < POLY_START_DELAY_MS {
|
|||
|
|
if let Some(a) = box_anchor {
|
|||
|
|
let crossing = box_crossing;
|
|||
|
|
let all_wires = self.tabs[i].scene.entity_wires();
|
|||
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|||
|
|
let mut handles: Vec<Handle> = scene::hit_test::box_hit(
|
|||
|
|
a, p, crossing, &all_wires, vp_mat, bounds,
|
|||
|
|
)
|
|||
|
|
.into_iter()
|
|||
|
|
.filter_map(|s| Scene::handle_from_wire_name(s))
|
|||
|
|
.collect();
|
|||
|
|
handles.extend(scene::hit_test::box_hit_hatch(
|
|||
|
|
a,
|
|||
|
|
p,
|
|||
|
|
crossing,
|
|||
|
|
&self.tabs[i].scene.hatches,
|
|||
|
|
vp_mat,
|
|||
|
|
bounds,
|
|||
|
|
));
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
for h in &handles {
|
|||
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].scene.expand_selection_for_groups(&handles);
|
|||
|
|
self.refresh_properties();
|
|||
|
|
selection_just_completed = true;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// Long drag → lasso polygon selection.
|
|||
|
|
let (poly_pts, crossing) = {
|
|||
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|||
|
|
(sel.poly_points.clone(), sel.poly_crossing)
|
|||
|
|
};
|
|||
|
|
self.tabs[i].scene.selection.borrow_mut().poly_last_crossing = crossing;
|
|||
|
|
|
|||
|
|
let all_wires = self.tabs[i].scene.entity_wires();
|
|||
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|||
|
|
let mut handles: Vec<Handle> = scene::hit_test::poly_hit(
|
|||
|
|
&poly_pts, crossing, &all_wires, vp_mat, bounds,
|
|||
|
|
)
|
|||
|
|
.into_iter()
|
|||
|
|
.filter_map(|s| Scene::handle_from_wire_name(s))
|
|||
|
|
.collect();
|
|||
|
|
handles.extend(scene::hit_test::poly_hit_hatch(
|
|||
|
|
&poly_pts,
|
|||
|
|
crossing,
|
|||
|
|
&self.tabs[i].scene.hatches,
|
|||
|
|
vp_mat,
|
|||
|
|
bounds,
|
|||
|
|
));
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
for h in &handles {
|
|||
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].scene.expand_selection_for_groups(&handles);
|
|||
|
|
self.refresh_properties();
|
|||
|
|
selection_just_completed = true;
|
|||
|
|
}
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.poly_active = false;
|
|||
|
|
sel.poly_points.clear();
|
|||
|
|
sel.poly_crossing = false;
|
|||
|
|
sel.box_anchor = None;
|
|||
|
|
sel.box_current = None;
|
|||
|
|
} else {
|
|||
|
|
// ── Non-drag click ────────────────────────────────
|
|||
|
|
if box_anchor.is_none() {
|
|||
|
|
let all_wires = self.tabs[i].scene.entity_wires();
|
|||
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|||
|
|
let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
|||
|
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
|||
|
|
.or_else(|| {
|
|||
|
|
scene::hit_test::click_hit_hatch(
|
|||
|
|
p,
|
|||
|
|
&self.tabs[i].scene.hatches,
|
|||
|
|
vp_mat,
|
|||
|
|
bounds,
|
|||
|
|
)
|
|||
|
|
});
|
|||
|
|
if let Some(handle) = hit {
|
|||
|
|
self.tabs[i].scene.select_entity(handle, true);
|
|||
|
|
self.tabs[i].scene.expand_selection_for_groups(&[handle]);
|
|||
|
|
self.refresh_properties();
|
|||
|
|
// Single-click hit completes the gather; miss starts a box.
|
|||
|
|
selection_just_completed = true;
|
|||
|
|
} else {
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
self.refresh_properties();
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.box_anchor = Some(p);
|
|||
|
|
sel.box_current = Some(p);
|
|||
|
|
sel.box_crossing = false;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// Second click → complete the selection box.
|
|||
|
|
let a = box_anchor.unwrap();
|
|||
|
|
let crossing = box_crossing;
|
|||
|
|
let all_wires = self.tabs[i].scene.entity_wires();
|
|||
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|||
|
|
let mut handles: Vec<Handle> = scene::hit_test::box_hit(
|
|||
|
|
a, p, crossing, &all_wires, vp_mat, bounds,
|
|||
|
|
)
|
|||
|
|
.into_iter()
|
|||
|
|
.filter_map(|s| Scene::handle_from_wire_name(s))
|
|||
|
|
.collect();
|
|||
|
|
handles.extend(scene::hit_test::box_hit_hatch(
|
|||
|
|
a,
|
|||
|
|
p,
|
|||
|
|
crossing,
|
|||
|
|
&self.tabs[i].scene.hatches,
|
|||
|
|
vp_mat,
|
|||
|
|
bounds,
|
|||
|
|
));
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
for h in &handles {
|
|||
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].scene.expand_selection_for_groups(&handles);
|
|||
|
|
self.refresh_properties();
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.box_last = Some((a, p));
|
|||
|
|
sel.box_last_crossing = crossing;
|
|||
|
|
sel.box_anchor = None;
|
|||
|
|
sel.box_current = None;
|
|||
|
|
sel.box_crossing = false;
|
|||
|
|
selection_just_completed = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.left_down = false;
|
|||
|
|
sel.left_press_pos = None;
|
|||
|
|
sel.left_press_time = None;
|
|||
|
|
sel.left_dragging = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Fire gathering command after selection completes ───────
|
|||
|
|
if is_gathering && selection_just_completed {
|
|||
|
|
let handles: Vec<Handle> = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.selected_entities()
|
|||
|
|
.into_iter()
|
|||
|
|
.map(|(h, _)| h)
|
|||
|
|
.collect();
|
|||
|
|
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
|||
|
|
let result = cmd.on_selection_complete(handles);
|
|||
|
|
return self.apply_cmd_result(result);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportRightPress => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
let Some(p) = sel.last_move_pos else {
|
|||
|
|
return Task::none();
|
|||
|
|
};
|
|||
|
|
sel.context_menu = None;
|
|||
|
|
sel.right_down = true;
|
|||
|
|
sel.right_press_pos = Some(p);
|
|||
|
|
sel.right_last_pos = Some(p);
|
|||
|
|
sel.right_dragging = false;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportRightRelease => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
let Some(_p) = sel.last_move_pos else {
|
|||
|
|
return Task::none();
|
|||
|
|
};
|
|||
|
|
if sel.right_down {
|
|||
|
|
if !sel.right_dragging {
|
|||
|
|
sel.context_menu = sel.last_move_pos;
|
|||
|
|
}
|
|||
|
|
sel.right_down = false;
|
|||
|
|
sel.right_press_pos = None;
|
|||
|
|
sel.right_last_pos = None;
|
|||
|
|
sel.right_dragging = false;
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportMiddlePress => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let now = iced::time::Instant::now();
|
|||
|
|
let is_double = {
|
|||
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|||
|
|
sel.middle_last_press_time
|
|||
|
|
.map(|t| now.duration_since(t).as_millis() < 300)
|
|||
|
|
.unwrap_or(false)
|
|||
|
|
};
|
|||
|
|
{
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
let Some(p) = sel.last_move_pos else {
|
|||
|
|
return Task::none();
|
|||
|
|
};
|
|||
|
|
sel.middle_down = true;
|
|||
|
|
sel.middle_last_pos = Some(p);
|
|||
|
|
sel.middle_last_press_time = Some(now);
|
|||
|
|
}
|
|||
|
|
if is_double {
|
|||
|
|
self.tabs[i].scene.fit_all();
|
|||
|
|
self.command_line.push_output("Zoom Extents");
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportMiddleRelease => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|||
|
|
sel.middle_down = false;
|
|||
|
|
sel.middle_last_pos = None;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportScroll(delta) => {
|
|||
|
|
let s = match delta {
|
|||
|
|
mouse::ScrollDelta::Lines { y, .. } => y,
|
|||
|
|
mouse::ScrollDelta::Pixels { y, .. } => y * 0.01,
|
|||
|
|
};
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let cursor = self.tabs[i].scene.selection.borrow().last_move_pos;
|
|||
|
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
|||
|
|
let bounds = iced::Rectangle {
|
|||
|
|
x: 0.0,
|
|||
|
|
y: 0.0,
|
|||
|
|
width: vw,
|
|||
|
|
height: vh,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let mut cam = self.tabs[i].scene.camera.borrow_mut();
|
|||
|
|
if let Some(cursor) = cursor {
|
|||
|
|
cam.zoom_about_point(cursor, bounds, s);
|
|||
|
|
} else {
|
|||
|
|
cam.zoom(s);
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewportClick => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let cam = self.tabs[i].scene.camera.borrow();
|
|||
|
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
|||
|
|
if let Some(region) = scene::hit_test(
|
|||
|
|
self.cursor_pos.x,
|
|||
|
|
self.cursor_pos.y,
|
|||
|
|
vw,
|
|||
|
|
vh,
|
|||
|
|
cam.view_rotation_mat(),
|
|||
|
|
VIEWCUBE_PX,
|
|||
|
|
) {
|
|||
|
|
return Task::done(Message::ViewCubeSnap(region));
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::WindowResized(w, h) => {
|
|||
|
|
self.vp_size = ((w - 440.0).max(200.0), h);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ViewCubeSnap(region) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let mut region = region;
|
|||
|
|
{
|
|||
|
|
let mut cam = self.tabs[i].scene.camera.borrow_mut();
|
|||
|
|
let (target_yaw, target_pitch) = region.snap_angles();
|
|||
|
|
if angle_close(cam.yaw, target_yaw, 0.01)
|
|||
|
|
&& angle_close(cam.pitch, target_pitch, 0.01)
|
|||
|
|
{
|
|||
|
|
region = region.opposite();
|
|||
|
|
}
|
|||
|
|
let (yaw, pitch) = region.snap_angles();
|
|||
|
|
cam.snap_to_angles(yaw, pitch);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].scene.camera_generation += 1;
|
|||
|
|
self.command_line
|
|||
|
|
.push_output(&format!("View: {}", region.label()));
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ToggleSnapEnabled => {
|
|||
|
|
self.snapper.toggle_global();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::ToggleGridSnap => {
|
|||
|
|
self.snapper.toggle(snap::SnapType::Grid);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::ToggleGrid => {
|
|||
|
|
self.show_grid ^= true;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::ToggleOrtho => {
|
|||
|
|
self.ortho_mode ^= true;
|
|||
|
|
if self.ortho_mode {
|
|||
|
|
self.polar_mode = false;
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::TogglePolar => {
|
|||
|
|
self.polar_mode ^= true;
|
|||
|
|
if self.polar_mode {
|
|||
|
|
self.ortho_mode = false;
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::ToggleSnap(t) => {
|
|||
|
|
self.snapper.toggle(t);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::ToggleSnapPopup => {
|
|||
|
|
self.snap_popup_open ^= true;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::CloseSnapPopup => {
|
|||
|
|
self.snap_popup_open = false;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::SnapSelectAll => {
|
|||
|
|
self.snapper.enable_all();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::SnapClearAll => {
|
|||
|
|
self.snapper.disable_all();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::ToggleRibbonDropdown(id) => {
|
|||
|
|
self.ribbon.toggle_dropdown(&id);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::CloseRibbonDropdown => {
|
|||
|
|
self.ribbon.close_dropdown();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::DropdownSelectItem { dropdown_id, cmd } => {
|
|||
|
|
self.ribbon.select_dropdown_item(dropdown_id, cmd);
|
|||
|
|
self.ribbon.activate_tool(cmd);
|
|||
|
|
self.dispatch_command(cmd)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::DeleteSelected => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
self.push_undo_snapshot(i, "ERASE");
|
|||
|
|
self.tabs[i].scene.erase_entities(&handles);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Properties panel messages ─────────────────────────────────
|
|||
|
|
Message::PropSelectionGroupChanged(group) => {
|
|||
|
|
self.tabs[self.active_tab].properties.selected_group = Some(group);
|
|||
|
|
self.refresh_properties();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::RibbonLayerChanged(layer) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].active_layer = layer.clone();
|
|||
|
|
self.tabs[i].layers.current_layer = layer.clone();
|
|||
|
|
self.ribbon.active_layer = layer;
|
|||
|
|
self.ribbon.close_dropdown();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::RibbonColorChanged(color) => {
|
|||
|
|
self.ribbon.active_color = color;
|
|||
|
|
self.ribbon.prop_color_palette_open = false;
|
|||
|
|
self.ribbon.close_dropdown();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::RibbonColorPaletteToggle => {
|
|||
|
|
self.ribbon.prop_color_palette_open ^= true;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::RibbonLinetypeChanged(lt) => {
|
|||
|
|
self.ribbon.active_linetype = lt;
|
|||
|
|
self.ribbon.close_dropdown();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
Message::RibbonLineweightChanged(lw) => {
|
|||
|
|
self.ribbon.active_lineweight = lw;
|
|||
|
|
self.ribbon.close_dropdown();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropLayerChanged(layer) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles = self.property_target_handles(i);
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|||
|
|
for handle in handles {
|
|||
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|||
|
|
dispatch::apply_common_prop(entity, "layer", &layer);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropColorChanged(color) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles = self.property_target_handles(i);
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|||
|
|
for handle in handles {
|
|||
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|||
|
|
dispatch::apply_color(entity, color);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].properties.color_picker_open = false;
|
|||
|
|
self.tabs[i].properties.color_palette_open = false;
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropLwChanged(lw) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles = self.property_target_handles(i);
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|||
|
|
for handle in handles {
|
|||
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|||
|
|
dispatch::apply_line_weight(entity, lw);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropLinetypeChanged(lt) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles = self.property_target_handles(i);
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|||
|
|
for handle in handles {
|
|||
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|||
|
|
dispatch::apply_common_prop(entity, "linetype", <);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropHatchPatternChanged(name) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles = self.property_target_handles(i);
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
use crate::scene::hatch_patterns;
|
|||
|
|
if let Some(entry) = hatch_patterns::find(&name) {
|
|||
|
|
self.push_undo_snapshot(i, "HATCHEDIT");
|
|||
|
|
for handle in handles {
|
|||
|
|
if let Some(acadrust::EntityType::Hatch(dxf)) =
|
|||
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|||
|
|
{
|
|||
|
|
dxf.pattern = hatch_patterns::build_dxf_pattern(entry);
|
|||
|
|
dxf.is_solid = matches!(
|
|||
|
|
entry.gpu,
|
|||
|
|
crate::scene::hatch_model::HatchPattern::Solid
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) {
|
|||
|
|
model.pattern = entry.gpu.clone();
|
|||
|
|
model.name = name.clone();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropBoolToggle(field) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles = self.property_target_handles(i);
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|||
|
|
for handle in handles {
|
|||
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|||
|
|
match field {
|
|||
|
|
"invisible" => dispatch::toggle_invisible(entity),
|
|||
|
|
_ => dispatch::apply_geom_prop(entity, field, "toggle"),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropGeomChoiceChanged { field, value } => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles = self.property_target_handles(i);
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|||
|
|
for handle in handles {
|
|||
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|||
|
|
dispatch::apply_geom_prop(entity, field, &value);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropGeomInput { field, value } => {
|
|||
|
|
self.tabs[self.active_tab]
|
|||
|
|
.properties
|
|||
|
|
.edit_buf
|
|||
|
|
.insert(field.to_string(), value);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropGeomCommit(field) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let handles = self.property_target_handles(i);
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
if let Some(val) = self.tabs[i].properties.edit_buf.remove(field) {
|
|||
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|||
|
|
for handle in handles {
|
|||
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
|
|||
|
|
{
|
|||
|
|
match field {
|
|||
|
|
"linetype_scale" | "transparency" => {
|
|||
|
|
dispatch::apply_common_prop(entity, field, &val);
|
|||
|
|
}
|
|||
|
|
_ => {
|
|||
|
|
dispatch::apply_geom_prop(entity, field, &val);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropColorPickerToggle => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
self.tabs[i].properties.color_picker_open =
|
|||
|
|
!self.tabs[i].properties.color_picker_open;
|
|||
|
|
if self.tabs[i].properties.color_picker_open {
|
|||
|
|
self.tabs[i].properties.color_palette_open = false;
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::PropColorPaletteToggle => {
|
|||
|
|
self.tabs[self.active_tab].properties.color_palette_open =
|
|||
|
|
!self.tabs[self.active_tab].properties.color_palette_open;
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayoutSwitch(name) => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let going_to_paper = name != "Model";
|
|||
|
|
self.tabs[i].scene.current_layout = name;
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
self.tabs[i].scene.fit_all();
|
|||
|
|
// Switch to Layout tab when entering paper space; back to Home when leaving.
|
|||
|
|
if going_to_paper {
|
|||
|
|
if let Some(idx) = self.ribbon.layout_module_index() {
|
|||
|
|
self.ribbon.select(idx);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if self.ribbon.active_is_layout() {
|
|||
|
|
self.ribbon.select(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::LayoutCreate => {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let count = self.tabs[i].scene.layout_names().len(); // "Model" + existing paper
|
|||
|
|
let new_name = format!("Layout{}", count);
|
|||
|
|
self.push_undo_snapshot(i, "LAYOUT");
|
|||
|
|
match self.tabs[i].scene.document.add_layout(&new_name) {
|
|||
|
|
Ok(_) => {
|
|||
|
|
self.tabs[i].scene.current_layout = new_name.clone();
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
self.tabs[i].scene.fit_all();
|
|||
|
|
if let Some(idx) = self.ribbon.layout_module_index() {
|
|||
|
|
self.ribbon.select(idx);
|
|||
|
|
}
|
|||
|
|
self.command_line.push_output(&format!(
|
|||
|
|
"Layout \"{new_name}\" oluşturuldu — MVIEW ile viewport ekleyin"
|
|||
|
|
));
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
}
|
|||
|
|
Err(e) => self
|
|||
|
|
.command_line
|
|||
|
|
.push_error(&format!("Layout oluşturulamadı: {e}")),
|
|||
|
|
}
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::Undo => {
|
|||
|
|
self.undo_active_tab();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::Redo => {
|
|||
|
|
self.redo_active_tab();
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::UndoMany(steps) => {
|
|||
|
|
self.ribbon.close_dropdown();
|
|||
|
|
self.undo_steps(steps);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::RedoMany(steps) => {
|
|||
|
|
self.ribbon.close_dropdown();
|
|||
|
|
self.redo_steps(steps);
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Message::Noop => Task::none(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn dispatch_command(&mut self, cmd: &str) -> Task<Message> {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
// Cancel any running command before starting a new one.
|
|||
|
|
if self.tabs[i].active_cmd.is_some() {
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") {
|
|||
|
|
let path = PathBuf::from(path_str);
|
|||
|
|
return Task::perform(io::open_path(path), Message::FileOpened);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
match cmd {
|
|||
|
|
"NEW" => return Task::done(Message::ClearScene),
|
|||
|
|
"OPEN" => return Task::done(Message::OpenFile),
|
|||
|
|
"SAVE"|"QSAVE" => return Task::done(Message::SaveFile),
|
|||
|
|
"SAVEAS" => return Task::done(Message::SaveAs),
|
|||
|
|
"UNDO"|"U" => return Task::done(Message::Undo),
|
|||
|
|
"REDO" => return Task::done(Message::Redo),
|
|||
|
|
"CLEAR"|"CLR" => return Task::done(Message::ClearScene),
|
|||
|
|
"WIREFRAME"|"VW" => return Task::done(Message::SetWireframe(true)),
|
|||
|
|
"SOLID"|"VS" => return Task::done(Message::SetWireframe(false)),
|
|||
|
|
"ORTHO" => return Task::done(Message::SetProjection(true)),
|
|||
|
|
"PERSP" => return Task::done(Message::SetProjection(false)),
|
|||
|
|
"LAYERS"|"LA" => return Task::done(Message::ToggleLayers),
|
|||
|
|
|
|||
|
|
// ── Layer object commands ──────────────────────────────────────
|
|||
|
|
// Commands that operate on the layer of selected (or picked) objects.
|
|||
|
|
|
|||
|
|
"LAYOFF" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("LAYOFF");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|||
|
|
.selected_entities().into_iter()
|
|||
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|||
|
|
self.push_undo_snapshot(i, "LAYOFF");
|
|||
|
|
for name in &layers {
|
|||
|
|
if name == "0" { continue; }
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|||
|
|
dl.turn_off();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
self.command_line.push_info("Layer(s) turned off.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LAYFRZ" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("LAYFRZ");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|||
|
|
.selected_entities().into_iter()
|
|||
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|||
|
|
self.push_undo_snapshot(i, "LAYFRZ");
|
|||
|
|
for name in &layers {
|
|||
|
|
if name == "0" { continue; }
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|||
|
|
dl.freeze();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
self.command_line.push_info("Layer(s) frozen.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LAYLCK" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("LAYLCK");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|||
|
|
.selected_entities().into_iter()
|
|||
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|||
|
|
self.push_undo_snapshot(i, "LAYLCK");
|
|||
|
|
for name in &layers {
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|||
|
|
dl.lock();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
self.command_line.push_info("Layer(s) locked.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LAYMCUR" => {
|
|||
|
|
let entities = self.tabs[i].scene.selected_entities();
|
|||
|
|
if entities.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("LAYMCUR");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let layer = entities[0].1.common().layer.clone();
|
|||
|
|
self.tabs[i].active_layer = layer.clone();
|
|||
|
|
self.ribbon.active_layer = layer.clone();
|
|||
|
|
self.tabs[i].layers.current_layer = layer.clone();
|
|||
|
|
self.command_line.push_info(&format!("Current layer set to \"{layer}\"."));
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LAYON" => {
|
|||
|
|
self.push_undo_snapshot(i, "LAYON");
|
|||
|
|
for name in self.tabs[i].scene.document.layers.iter()
|
|||
|
|
.map(|l| l.name.clone()).collect::<Vec<_>>()
|
|||
|
|
{
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|||
|
|
dl.turn_on();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
self.command_line.push_info("All layers turned on.");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LAYTHW" => {
|
|||
|
|
self.push_undo_snapshot(i, "LAYTHW");
|
|||
|
|
for name in self.tabs[i].scene.document.layers.iter()
|
|||
|
|
.map(|l| l.name.clone()).collect::<Vec<_>>()
|
|||
|
|
{
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|||
|
|
dl.thaw();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
self.command_line.push_info("All layers thawed.");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LAYULK" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("LAYULK");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|||
|
|
.selected_entities().into_iter()
|
|||
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|||
|
|
self.push_undo_snapshot(i, "LAYULK");
|
|||
|
|
for name in &layers {
|
|||
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|||
|
|
dl.unlock();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
self.command_line.push_info("Layer(s) unlocked.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LAYMATCH"|"LAYMCH" => {
|
|||
|
|
use crate::command::laymatch::LayMatchCommand;
|
|||
|
|
let dest: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
let cmd = LayMatchCommand::new(dest);
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"MATCHPROP"|"MA" => {
|
|||
|
|
use crate::command::matchprop::MatchPropCommand;
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
let cmd = MatchPropCommand::new();
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"GROUP"|"G" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.selected_entities()
|
|||
|
|
.into_iter()
|
|||
|
|
.map(|(h, _)| h)
|
|||
|
|
.collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("GROUP");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let auto_name = Self::next_group_auto_name(&self.tabs[i].scene);
|
|||
|
|
use crate::command::group::GroupCommand;
|
|||
|
|
let cmd = GroupCommand::new(handles, auto_name);
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"UNGROUP"|"UG" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.selected_entities()
|
|||
|
|
.into_iter()
|
|||
|
|
.map(|(h, _)| h)
|
|||
|
|
.collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::ungroup::UngroupCommand;
|
|||
|
|
let cmd = UngroupCommand::new();
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
self.push_undo_snapshot(i, "UNGROUP");
|
|||
|
|
let count = self.tabs[i].scene.delete_groups_containing(&handles);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
if count > 0 {
|
|||
|
|
self.command_line.push_info(&format!("{} group(s) dissolved.", count));
|
|||
|
|
} else {
|
|||
|
|
self.command_line.push_info("No groups found for selected objects.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"COPYCLIP"|"CC" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i]
|
|||
|
|
.scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("COPYCLIP");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let entities: Vec<_> = handles.iter()
|
|||
|
|
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned())
|
|||
|
|
.collect();
|
|||
|
|
self.clipboard_centroid = Self::entities_centroid(
|
|||
|
|
&self.tabs[i].scene.wire_models_for(&handles),
|
|||
|
|
);
|
|||
|
|
self.clipboard = entities;
|
|||
|
|
self.command_line.push_info(
|
|||
|
|
&format!("{} object(s) copied to clipboard.", self.clipboard.len()),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"CUTCLIP"|"CX" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i]
|
|||
|
|
.scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("CUTCLIP");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let entities: Vec<_> = handles.iter()
|
|||
|
|
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned())
|
|||
|
|
.collect();
|
|||
|
|
self.clipboard_centroid = Self::entities_centroid(
|
|||
|
|
&self.tabs[i].scene.wire_models_for(&handles),
|
|||
|
|
);
|
|||
|
|
let count = entities.len();
|
|||
|
|
self.clipboard = entities;
|
|||
|
|
self.push_undo_snapshot(i, "CUTCLIP");
|
|||
|
|
self.tabs[i].scene.erase_entities(&handles);
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
self.command_line.push_info(
|
|||
|
|
&format!("{} object(s) cut to clipboard.", count),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"PASTECLIP"|"PC" => {
|
|||
|
|
if self.clipboard.is_empty() {
|
|||
|
|
self.command_line.push_error("Clipboard is empty.");
|
|||
|
|
} else {
|
|||
|
|
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
|
|||
|
|
use crate::command::paste::PasteCommand;
|
|||
|
|
let cmd = PasteCommand::new(wires, self.clipboard_centroid);
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"BLOCK" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.selected_entities()
|
|||
|
|
.into_iter()
|
|||
|
|
.map(|(h, _)| h)
|
|||
|
|
.collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("BLOCK");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::insert::create_block::CreateBlockCommand;
|
|||
|
|
let cmd = CreateBlockCommand::new(handles);
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
"INSERT" => {
|
|||
|
|
let blocks = self.tabs[i].scene.custom_block_names();
|
|||
|
|
if blocks.is_empty() {
|
|||
|
|
self.command_line
|
|||
|
|
.push_error("No user-defined blocks found in this drawing.");
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::insert::insert_block::InsertBlockCommand;
|
|||
|
|
let cmd = InsertBlockCommand::new(blocks);
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LINE"|"L" => {
|
|||
|
|
use crate::modules::home::draw::line::LineCommand;
|
|||
|
|
let new_cmd = LineCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"CIRCLE"|"C" => {
|
|||
|
|
use crate::modules::home::draw::circle::CircleCommand;
|
|||
|
|
let new_cmd = CircleCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"CIRCLE_CD" => {
|
|||
|
|
use crate::modules::home::draw::circle::CircleCDCommand;
|
|||
|
|
let new_cmd = CircleCDCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"CIRCLE_2P" => {
|
|||
|
|
use crate::modules::home::draw::circle::Circle2PCommand;
|
|||
|
|
let new_cmd = Circle2PCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"CIRCLE_3P" => {
|
|||
|
|
use crate::modules::home::draw::circle::Circle3PCommand;
|
|||
|
|
let new_cmd = Circle3PCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"CIRCLE_TTR" => {
|
|||
|
|
use crate::modules::home::draw::circle::CircleTTRCommand;
|
|||
|
|
let new_cmd = CircleTTRCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent));
|
|||
|
|
self.snapper.enabled.insert(crate::snap::SnapType::Tangent);
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"CIRCLE_TTT" => {
|
|||
|
|
use crate::modules::home::draw::circle::CircleTTTCommand;
|
|||
|
|
let new_cmd = CircleTTTCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent));
|
|||
|
|
self.snapper.enabled.insert(crate::snap::SnapType::Tangent);
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ARC"|"A" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcCommand;
|
|||
|
|
let new_cmd = ArcCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_3P" => {
|
|||
|
|
use crate::modules::home::draw::arc::Arc3PCommand;
|
|||
|
|
let new_cmd = Arc3PCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_SCE" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcSCECommand;
|
|||
|
|
let new_cmd = ArcSCECommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_SCA" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcSCACommand;
|
|||
|
|
let new_cmd = ArcSCACommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_SCL" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcSCLCommand;
|
|||
|
|
let new_cmd = ArcSCLCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_SEA" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcSEACommand;
|
|||
|
|
let new_cmd = ArcSEACommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_SER" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcSERCommand;
|
|||
|
|
let new_cmd = ArcSERCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_SED" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcSEDCommand;
|
|||
|
|
let new_cmd = ArcSEDCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_CSA" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcCSACommand;
|
|||
|
|
let new_cmd = ArcCSACommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"ARC_CSL" => {
|
|||
|
|
use crate::modules::home::draw::arc::ArcCSLCommand;
|
|||
|
|
let new_cmd = ArcCSLCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"RECT" => {
|
|||
|
|
use crate::modules::home::draw::shapes::RectCommand;
|
|||
|
|
let new_cmd = RectCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"RECT_ROT" => {
|
|||
|
|
use crate::modules::home::draw::shapes::RectRotCommand;
|
|||
|
|
let new_cmd = RectRotCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"RECT_CEN" => {
|
|||
|
|
use crate::modules::home::draw::shapes::RectCenCommand;
|
|||
|
|
let new_cmd = RectCenCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"POLY" => {
|
|||
|
|
use crate::modules::home::draw::shapes::PolyCommand;
|
|||
|
|
let new_cmd = PolyCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"POLY_C" => {
|
|||
|
|
use crate::modules::home::draw::shapes::PolyCCommand;
|
|||
|
|
let new_cmd = PolyCCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"POLY_E" => {
|
|||
|
|
use crate::modules::home::draw::shapes::PolyECommand;
|
|||
|
|
let new_cmd = PolyECommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"PLINE"|"PL" => {
|
|||
|
|
use crate::modules::home::draw::polyline::PlineCommand;
|
|||
|
|
let new_cmd = PlineCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"MOVE"|"M" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("MOVE");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::translate::MoveCommand;
|
|||
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|||
|
|
let new_cmd = MoveCommand::new(handles, wires);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"COPY"|"CO" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("COPY");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::copy::CopyCommand;
|
|||
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|||
|
|
let new_cmd = CopyCommand::new(handles, wires);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ROTATE"|"RO" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("ROTATE");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::rotate::RotateCommand;
|
|||
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|||
|
|
let new_cmd = RotateCommand::new(handles, wires);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
"POINT"|"PO" => {
|
|||
|
|
use crate::modules::home::draw::point::PointCommand;
|
|||
|
|
let new_cmd = PointCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"HATCH"|"H" => {
|
|||
|
|
use crate::modules::home::draw::hatch::HatchCommand;
|
|||
|
|
let outlines = self.tabs[i].scene.closed_outlines();
|
|||
|
|
let new_cmd = HatchCommand::new(outlines);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"GRADIENT" => {
|
|||
|
|
use crate::modules::home::draw::hatch::GradientCommand;
|
|||
|
|
let outlines = self.tabs[i].scene.closed_outlines();
|
|||
|
|
let new_cmd = GradientCommand::new(outlines);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"BOUNDARY" => {
|
|||
|
|
use crate::modules::home::draw::hatch::BoundaryCommand;
|
|||
|
|
let outlines = self.tabs[i].scene.closed_outlines();
|
|||
|
|
let new_cmd = BoundaryCommand::new(outlines);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ELLIPSE"|"EL" => {
|
|||
|
|
use crate::modules::home::draw::ellipse::EllipseCommand;
|
|||
|
|
let new_cmd = EllipseCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ELLIPSE_AXIS" => {
|
|||
|
|
use crate::modules::home::draw::ellipse::EllipseAxisCommand;
|
|||
|
|
let new_cmd = EllipseAxisCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ELLIPSE_ARC" => {
|
|||
|
|
use crate::modules::home::draw::ellipse::EllipseArcCommand;
|
|||
|
|
let new_cmd = EllipseArcCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"SPLINE"|"SPL" => {
|
|||
|
|
use crate::modules::home::draw::spline::SplineCommand;
|
|||
|
|
let new_cmd = SplineCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"SCALE"|"SC" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("SCALE");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::scale::ScaleCommand;
|
|||
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|||
|
|
let new_cmd = ScaleCommand::new(handles, wires);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"MIRROR"|"MI" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("MIRROR");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::mirror::MirrorCommand;
|
|||
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|||
|
|
let new_cmd = MirrorCommand::new(handles, wires);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ERASE"|"E" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("ERASE");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
let n = handles.len();
|
|||
|
|
self.push_undo_snapshot(i, "ERASE");
|
|||
|
|
self.tabs[i].scene.erase_entities(&handles);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
self.command_line.push_output(&format!("{n} object(s) erased."));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"TEXT"|"T"|"DT" => {
|
|||
|
|
use crate::modules::annotate::text::TextCommand;
|
|||
|
|
let new_cmd = TextCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"MTEXT"|"MT" => {
|
|||
|
|
use crate::modules::annotate::mtext::MTextCommand;
|
|||
|
|
let new_cmd = MTextCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"DIMLINEAR" => {
|
|||
|
|
use crate::modules::annotate::linear_dim::LinearDimensionCommand;
|
|||
|
|
let new_cmd = LinearDimensionCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"DIMRADIUS" => {
|
|||
|
|
use crate::modules::annotate::radius_dim::RadiusDimensionCommand;
|
|||
|
|
let new_cmd = RadiusDimensionCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"DIMANGULAR" => {
|
|||
|
|
use crate::modules::annotate::angular_dim::AngularDimensionCommand;
|
|||
|
|
let new_cmd = AngularDimensionCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"LEADER"|"LE" => {
|
|||
|
|
use crate::modules::annotate::leader_cmd::LeaderCommand;
|
|||
|
|
let new_cmd = LeaderCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"MLEADER"|"MLD" => {
|
|||
|
|
use crate::modules::annotate::mleader_cmd::MLeaderCommand;
|
|||
|
|
let new_cmd = MLeaderCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ZOOM EXTENTS"|"ZOOMEXTENTS"|"ZE" => {
|
|||
|
|
self.tabs[i].scene.fit_all();
|
|||
|
|
self.command_line.push_output("Zoom Extents");
|
|||
|
|
}
|
|||
|
|
"STRETCH"|"SS" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("STRETCH");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::stretch::StretchCommand;
|
|||
|
|
let new_cmd = StretchCommand::new(handles);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"FILLET"|"F" => {
|
|||
|
|
use crate::modules::home::modify::fillet::FilletCommand;
|
|||
|
|
let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|||
|
|
.filter_map(|w| {
|
|||
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|||
|
|
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
|||
|
|
}).collect();
|
|||
|
|
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
|||
|
|
let new_cmd = FilletCommand::new(
|
|||
|
|
crate::modules::home::defaults::get_fillet_radius(), all_entities);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
"ARRAY"|"AR"|"ARRAYRECT" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("ARRAYRECT");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::array::ArrayRectCommand;
|
|||
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|||
|
|
let new_cmd = ArrayRectCommand::new(handles, wires);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ARRAYPOLAR" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("ARRAYPOLAR");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::array::ArrayPolarCommand;
|
|||
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|||
|
|
let new_cmd = ArrayPolarCommand::new(handles, wires);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"ARRAYPATH" => {
|
|||
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().map(|(h, _)| h).collect();
|
|||
|
|
if handles.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("ARRAYPATH");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::home::modify::array::ArrayPathCommand;
|
|||
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|||
|
|
let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|||
|
|
.filter_map(|w| {
|
|||
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|||
|
|
self.tabs[i].scene.document.get_entity(h).cloned()
|
|||
|
|
}).collect();
|
|||
|
|
let new_cmd = ArrayPathCommand::new(handles, wires, all_entities);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"CHAMFER"|"CHA" => {
|
|||
|
|
use crate::modules::home::modify::fillet::ChamferCommand;
|
|||
|
|
let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|||
|
|
.filter_map(|w| {
|
|||
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|||
|
|
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
|||
|
|
}).collect();
|
|||
|
|
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
|||
|
|
let new_cmd = ChamferCommand::new(
|
|||
|
|
crate::modules::home::defaults::get_chamfer_dist1(), all_entities);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"EXPLODE"|"X" => {
|
|||
|
|
use crate::modules::home::modify::explode::explode_entity;
|
|||
|
|
// Collect into owned vec to release borrow before mutating
|
|||
|
|
let entities: Vec<_> = self.tabs[i].scene.selected_entities()
|
|||
|
|
.into_iter().collect();
|
|||
|
|
if entities.is_empty() {
|
|||
|
|
use crate::command::select::SelectObjectsCommand;
|
|||
|
|
let cmd = SelectObjectsCommand::new("EXPLODE");
|
|||
|
|
self.command_line.push_info(&cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|||
|
|
} else {
|
|||
|
|
// Pre-compute all replacements before mutating
|
|||
|
|
let replacements: Vec<(acadrust::Handle, Vec<acadrust::EntityType>)> = entities
|
|||
|
|
.iter()
|
|||
|
|
.filter_map(|(h, e)| {
|
|||
|
|
let pieces = explode_entity(e, &self.tabs[i].scene.document);
|
|||
|
|
if pieces.is_empty() { None } else { Some((*h, pieces)) }
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
let exploded = replacements.len();
|
|||
|
|
if exploded > 0 {
|
|||
|
|
self.push_undo_snapshot(i, "EXPLODE");
|
|||
|
|
}
|
|||
|
|
for (handle, pieces) in replacements {
|
|||
|
|
self.tabs[i].scene.erase_entities(&[handle]);
|
|||
|
|
for piece in pieces {
|
|||
|
|
self.tabs[i].scene.add_entity(piece);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if exploded > 0 {
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
self.command_line.push_output(&format!("{exploded} object(s) exploded."));
|
|||
|
|
} else {
|
|||
|
|
self.command_line.push_info("EXPLODE: no explodable objects selected.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"OFFSET"|"O" => {
|
|||
|
|
use crate::modules::home::modify::offset::OffsetCommand;
|
|||
|
|
let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|||
|
|
.filter_map(|w| {
|
|||
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|||
|
|
self.tabs[i].scene.document.get_entity(h).cloned()
|
|||
|
|
}).collect();
|
|||
|
|
let new_cmd = OffsetCommand::new(
|
|||
|
|
crate::modules::home::defaults::get_offset_dist(), all_entities);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
"TRIM"|"TR" => {
|
|||
|
|
use crate::modules::home::modify::trim::TrimCommand;
|
|||
|
|
let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|||
|
|
.filter_map(|w| {
|
|||
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|||
|
|
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
|||
|
|
}).collect();
|
|||
|
|
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
|||
|
|
let new_cmd = TrimCommand::new(all_entities);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"EXTEND"|"EX" => {
|
|||
|
|
use crate::modules::home::modify::trim::ExtendCommand;
|
|||
|
|
let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|||
|
|
.filter_map(|w| {
|
|||
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|||
|
|
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
|||
|
|
}).collect();
|
|||
|
|
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
|||
|
|
let new_cmd = ExtendCommand::new(all_entities);
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
"3DORBIT"|"3O"=> self.command_line.push_info("3D Orbit: drag with right mouse button."),
|
|||
|
|
"HELP"|"?" => self.command_line.push_output("Draw: LINE CIRCLE ARC PLINE POINT ELLIPSE SPLINE | Modify: MOVE COPY ROTATE SCALE MIRROR ERASE | Text: TEXT MTEXT | File: OPEN SAVE SAVEAS"),
|
|||
|
|
|
|||
|
|
"DONATE" => {
|
|||
|
|
let _ = open::that("https://patreon.com/HakanSeven12");
|
|||
|
|
self.command_line.push_info("Opening Patreon page...");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Layout / viewport ─────────────────────────────────────────
|
|||
|
|
"MVIEW"|"MV" => {
|
|||
|
|
if self.tabs[i].scene.current_layout == "Model" {
|
|||
|
|
self.command_line.push_error("MVIEW: önce bir paper space layout'una geçin.");
|
|||
|
|
} else {
|
|||
|
|
use crate::modules::layout::mview::MviewCommand;
|
|||
|
|
let new_cmd = MviewCommand::new();
|
|||
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|||
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_ => self.command_line.push_error(&format!("Unknown command: {cmd}")),
|
|||
|
|
}
|
|||
|
|
// Focus the command line whenever a command just became active.
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
if self.tabs[i].active_cmd.is_some() {
|
|||
|
|
self.tabs[i].last_cmd = Some(cmd.to_string());
|
|||
|
|
self.focus_cmd_input()
|
|||
|
|
} else {
|
|||
|
|
Task::none()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Rebuild the PropertiesPanel from the current entity selection.
|
|||
|
|
/// Preserves UI state (open pickers, edit buffer) across refreshes.
|
|||
|
|
fn refresh_properties(&mut self) {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
// Preserve transient UI state across rebuilds.
|
|||
|
|
let color_picker_open = self.tabs[i].properties.color_picker_open;
|
|||
|
|
let color_palette_open = self.tabs[i].properties.color_palette_open;
|
|||
|
|
let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf);
|
|||
|
|
let selected_group = self.tabs[i].properties.selected_group.clone();
|
|||
|
|
|
|||
|
|
let layer_names: Vec<String> = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.document
|
|||
|
|
.layers
|
|||
|
|
.iter()
|
|||
|
|
.map(|l| l.name.clone())
|
|||
|
|
.collect();
|
|||
|
|
let linetype_items: Vec<ui::properties::LinetypeItem> = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.document
|
|||
|
|
.line_types
|
|||
|
|
.iter()
|
|||
|
|
.map(|lt| {
|
|||
|
|
let name = if lt.name.is_empty() {
|
|||
|
|
"ByLayer".to_string()
|
|||
|
|
} else {
|
|||
|
|
lt.name.clone()
|
|||
|
|
};
|
|||
|
|
let art = linetypes::extract_pattern(<.description);
|
|||
|
|
ui::properties::LinetypeItem { name, art }
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
let text_style_names: Vec<String> = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.document
|
|||
|
|
.text_styles
|
|||
|
|
.iter()
|
|||
|
|
.map(|style| style.name.trim().to_string())
|
|||
|
|
.filter(|name| !name.is_empty())
|
|||
|
|
.collect();
|
|||
|
|
|
|||
|
|
let new_panel = {
|
|||
|
|
let selected = self.tabs[i].scene.selected_entities();
|
|||
|
|
let mut panel = match selected.len() {
|
|||
|
|
0 => ui::PropertiesPanel::empty(),
|
|||
|
|
1 => {
|
|||
|
|
let (handle, entity) = selected[0];
|
|||
|
|
let group_names = self.tabs[i].scene.group_names_for_entity(handle);
|
|||
|
|
let mut sections =
|
|||
|
|
dispatch::properties_sectioned(handle, entity, &text_style_names);
|
|||
|
|
if !group_names.is_empty() {
|
|||
|
|
let label = group_names.join(", ");
|
|||
|
|
if let Some(general) = sections.first_mut() {
|
|||
|
|
general.props.push(crate::scene::object::Property {
|
|||
|
|
label: "Group".to_string(),
|
|||
|
|
field: "group",
|
|||
|
|
value: crate::scene::object::PropValue::ReadOnly(label),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
let title = entity_type_label(entity);
|
|||
|
|
ui::PropertiesPanel {
|
|||
|
|
choice_combos: sections
|
|||
|
|
.iter()
|
|||
|
|
.flat_map(|section| section.props.iter())
|
|||
|
|
.filter_map(|prop| match &prop.value {
|
|||
|
|
crate::scene::object::PropValue::Choice { options, .. } => Some((
|
|||
|
|
prop.field.to_string(),
|
|||
|
|
iced::widget::combo_box::State::new(options.clone()),
|
|||
|
|
)),
|
|||
|
|
_ => None,
|
|||
|
|
})
|
|||
|
|
.collect(),
|
|||
|
|
sections,
|
|||
|
|
title,
|
|||
|
|
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
|||
|
|
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
|||
|
|
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
|||
|
|
crate::scene::hatch_patterns::names(),
|
|||
|
|
),
|
|||
|
|
lineweight_combo: iced::widget::combo_box::State::new(
|
|||
|
|
ui::properties::lw_options(),
|
|||
|
|
),
|
|||
|
|
linetype_items,
|
|||
|
|
..Default::default()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
_ => {
|
|||
|
|
let groups = build_selection_groups(&selected);
|
|||
|
|
let active_group = selected_group
|
|||
|
|
.and_then(|group| groups.iter().find(|g| g.label == group.label).cloned())
|
|||
|
|
.or_else(|| groups.first().cloned());
|
|||
|
|
|
|||
|
|
let filtered: Vec<(Handle, &EntityType)> = active_group
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|group| {
|
|||
|
|
selected
|
|||
|
|
.iter()
|
|||
|
|
.filter(|(handle, _)| group.handles.contains(handle))
|
|||
|
|
.copied()
|
|||
|
|
.collect()
|
|||
|
|
})
|
|||
|
|
.unwrap_or_default();
|
|||
|
|
|
|||
|
|
let sections = aggregate_sections(&filtered, &text_style_names);
|
|||
|
|
ui::PropertiesPanel {
|
|||
|
|
choice_combos: sections
|
|||
|
|
.iter()
|
|||
|
|
.flat_map(|section| section.props.iter())
|
|||
|
|
.filter_map(|prop| match &prop.value {
|
|||
|
|
crate::scene::object::PropValue::Choice { options, .. } => Some((
|
|||
|
|
prop.field.to_string(),
|
|||
|
|
iced::widget::combo_box::State::new(options.clone()),
|
|||
|
|
)),
|
|||
|
|
_ => None,
|
|||
|
|
})
|
|||
|
|
.collect(),
|
|||
|
|
sections,
|
|||
|
|
title: format!("{} objects selected", selected.len()),
|
|||
|
|
selection_group_combo: iced::widget::combo_box::State::new(groups.clone()),
|
|||
|
|
selection_groups: groups,
|
|||
|
|
selected_group: active_group,
|
|||
|
|
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
|||
|
|
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
|||
|
|
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
|||
|
|
crate::scene::hatch_patterns::names(),
|
|||
|
|
),
|
|||
|
|
lineweight_combo: iced::widget::combo_box::State::new(
|
|||
|
|
ui::properties::lw_options(),
|
|||
|
|
),
|
|||
|
|
linetype_items,
|
|||
|
|
..Default::default()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
// Restore UI state.
|
|||
|
|
panel.color_picker_open = color_picker_open;
|
|||
|
|
panel.color_palette_open = color_palette_open;
|
|||
|
|
panel.edit_buf = edit_buf;
|
|||
|
|
panel
|
|||
|
|
// `selected` is dropped here, releasing the borrow on `self.tabs[i].scene`
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
self.tabs[i].properties = new_panel;
|
|||
|
|
self.refresh_selected_grips();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Rebuild the cached selected_grips from the current entity selection.
|
|||
|
|
fn refresh_selected_grips(&mut self) {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let (new_handle, new_grips) = {
|
|||
|
|
let selected = self.tabs[i].scene.selected_entities();
|
|||
|
|
if selected.len() == 1 {
|
|||
|
|
let (handle, entity) = selected[0];
|
|||
|
|
let grips = dispatch::grips(entity);
|
|||
|
|
(Some(handle), grips)
|
|||
|
|
} else {
|
|||
|
|
(None, vec![])
|
|||
|
|
}
|
|||
|
|
// `selected` is dropped here
|
|||
|
|
};
|
|||
|
|
self.tabs[i].selected_handle = new_handle;
|
|||
|
|
self.tabs[i].selected_grips = new_grips;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn property_target_handles(&self, i: usize) -> Vec<Handle> {
|
|||
|
|
let handles = self.tabs[i].properties.selected_handles();
|
|||
|
|
if !handles.is_empty() {
|
|||
|
|
handles
|
|||
|
|
} else {
|
|||
|
|
self.tabs[i].selected_handle.into_iter().collect()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String {
|
|||
|
|
self.tabs[i]
|
|||
|
|
.active_cmd
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|cmd| cmd.name().to_string())
|
|||
|
|
.unwrap_or_else(|| fallback.to_string())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn capture_history_snapshot(&self, i: usize, label: impl Into<String>) -> HistorySnapshot {
|
|||
|
|
HistorySnapshot {
|
|||
|
|
document: self.tabs[i].scene.document.clone(),
|
|||
|
|
current_layout: self.tabs[i].scene.current_layout.clone(),
|
|||
|
|
selected: self.tabs[i].scene.selected.iter().copied().collect(),
|
|||
|
|
dirty: self.tabs[i].dirty,
|
|||
|
|
label: label.into(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn sync_ribbon_layers(&mut self) {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let active = self.tabs[i].active_layer.clone();
|
|||
|
|
// Build LayerInfo from the panel (already synced from doc).
|
|||
|
|
let infos: Vec<crate::ui::ribbon::LayerInfo> = self.tabs[i]
|
|||
|
|
.layers
|
|||
|
|
.layers
|
|||
|
|
.iter()
|
|||
|
|
.map(|l| crate::ui::ribbon::LayerInfo {
|
|||
|
|
name: l.name.clone(),
|
|||
|
|
color: l.color,
|
|||
|
|
visible: l.visible,
|
|||
|
|
frozen: l.frozen,
|
|||
|
|
locked: l.locked,
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
let names: Vec<String> = infos.iter().map(|l| l.name.clone()).collect();
|
|||
|
|
// If the active layer no longer exists, fall back to "0".
|
|||
|
|
let active = if names.contains(&active) { active } else { "0".to_string() };
|
|||
|
|
self.tabs[i].active_layer = active.clone();
|
|||
|
|
self.tabs[i].layers.current_layer = active.clone();
|
|||
|
|
self.ribbon.set_layers(infos, &active);
|
|||
|
|
// Sync linetypes into the layer panel (using LinetypeItem with ASCII art)
|
|||
|
|
let lt_items: Vec<ui::properties::LinetypeItem> = self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.document
|
|||
|
|
.line_types
|
|||
|
|
.iter()
|
|||
|
|
.map(|lt| {
|
|||
|
|
let name = if lt.name.eq_ignore_ascii_case("bylayer") {
|
|||
|
|
"ByLayer".to_string()
|
|||
|
|
} else {
|
|||
|
|
lt.name.clone()
|
|||
|
|
};
|
|||
|
|
let art = linetypes::extract_pattern(<.description);
|
|||
|
|
ui::properties::LinetypeItem { name, art }
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
self.tabs[i].layers.sync_linetypes(lt_items.clone());
|
|||
|
|
self.ribbon.set_available_linetypes(lt_items);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn push_undo_snapshot(&mut self, i: usize, label: impl Into<String>) {
|
|||
|
|
let snapshot = self.capture_history_snapshot(i, label);
|
|||
|
|
self.tabs[i].history.undo_stack.push(snapshot);
|
|||
|
|
self.tabs[i].history.redo_stack.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn restore_history_snapshot(&mut self, i: usize, snapshot: HistorySnapshot) {
|
|||
|
|
self.tabs[i].scene.document = snapshot.document;
|
|||
|
|
self.tabs[i].scene.current_layout = snapshot.current_layout;
|
|||
|
|
self.tabs[i].scene.selected = snapshot
|
|||
|
|
.selected
|
|||
|
|
.into_iter()
|
|||
|
|
.filter(|h| self.tabs[i].scene.document.get_entity(*h).is_some())
|
|||
|
|
.collect::<HashSet<_>>();
|
|||
|
|
self.tabs[i].scene.populate_hatches_from_document();
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].scene.meshes.clear();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.tabs[i].active_grip = None;
|
|||
|
|
self.tabs[i].dirty = snapshot.dirty;
|
|||
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|||
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn undo_active_tab(&mut self) {
|
|||
|
|
self.undo_steps(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn redo_active_tab(&mut self) {
|
|||
|
|
self.redo_steps(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn undo_steps(&mut self, steps: usize) {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let available = self.tabs[i].history.undo_stack.len();
|
|||
|
|
let steps = steps.min(available);
|
|||
|
|
if steps == 0 {
|
|||
|
|
self.command_line.push_info("Nothing to undo.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut last_label = String::new();
|
|||
|
|
for _ in 0..steps {
|
|||
|
|
let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else {
|
|||
|
|
break;
|
|||
|
|
};
|
|||
|
|
let label = snapshot.label.clone();
|
|||
|
|
let current = self.capture_history_snapshot(i, label.clone());
|
|||
|
|
self.tabs[i].history.redo_stack.push(current);
|
|||
|
|
self.restore_history_snapshot(i, snapshot);
|
|||
|
|
last_label = label;
|
|||
|
|
}
|
|||
|
|
self.command_line.push_output(&format!("Undo: {last_label}"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn redo_steps(&mut self, steps: usize) {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let available = self.tabs[i].history.redo_stack.len();
|
|||
|
|
let steps = steps.min(available);
|
|||
|
|
if steps == 0 {
|
|||
|
|
self.command_line.push_info("Nothing to redo.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut last_label = String::new();
|
|||
|
|
for _ in 0..steps {
|
|||
|
|
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
|
|||
|
|
break;
|
|||
|
|
};
|
|||
|
|
let label = snapshot.label.clone();
|
|||
|
|
let current = self.capture_history_snapshot(i, label.clone());
|
|||
|
|
self.tabs[i].history.undo_stack.push(current);
|
|||
|
|
self.restore_history_snapshot(i, snapshot);
|
|||
|
|
last_label = label;
|
|||
|
|
}
|
|||
|
|
self.command_line.push_output(&format!("Redo: {last_label}"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Add an entity to the correct space:
|
|||
|
|
/// - Viewport entities while in paper space → add_entity_to_layout
|
|||
|
|
/// - Everything else → model space (add_entity)
|
|||
|
|
fn commit_entity(&mut self, mut entity: acadrust::EntityType) {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let layer = &self.tabs[i].active_layer;
|
|||
|
|
if layer != "0" || entity.as_entity().layer().is_empty() {
|
|||
|
|
entity.as_entity_mut().set_layer(layer.clone());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Apply active color/linetype/lineweight from Properties toolbar.
|
|||
|
|
crate::scene::dispatch::apply_color(&mut entity, self.ribbon.active_color);
|
|||
|
|
crate::scene::dispatch::apply_common_prop(
|
|||
|
|
&mut entity,
|
|||
|
|
"linetype",
|
|||
|
|
&self.ribbon.active_linetype.clone(),
|
|||
|
|
);
|
|||
|
|
crate::scene::dispatch::apply_line_weight(&mut entity, self.ribbon.active_lineweight);
|
|||
|
|
|
|||
|
|
if matches!(&entity, acadrust::EntityType::Viewport(_))
|
|||
|
|
&& self.tabs[i].scene.current_layout != "Model"
|
|||
|
|
{
|
|||
|
|
let layout = self.tabs[i].scene.current_layout.clone();
|
|||
|
|
match self.tabs[i]
|
|||
|
|
.scene
|
|||
|
|
.document
|
|||
|
|
.add_entity_to_layout(entity, &layout)
|
|||
|
|
{
|
|||
|
|
Ok(_) => {}
|
|||
|
|
Err(e) => self
|
|||
|
|
.command_line
|
|||
|
|
.push_error(&format!("Viewport eklenemedi: {e}")),
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
self.tabs[i].scene.add_entity(entity);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn apply_cmd_result(&mut self, result: CmdResult) -> Task<Message> {
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
match result {
|
|||
|
|
CmdResult::NeedPoint => {
|
|||
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|||
|
|
if let Some(p) = prompt {
|
|||
|
|
self.command_line.push_info(&p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::Preview(wire) => {
|
|||
|
|
self.tabs[i].scene.set_preview_wires(vec![wire]);
|
|||
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|||
|
|
if let Some(p) = prompt {
|
|||
|
|
self.command_line.push_info(&p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::InterimWire(wire) => {
|
|||
|
|
self.tabs[i].scene.set_interim_wire(wire);
|
|||
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|||
|
|
if let Some(p) = prompt {
|
|||
|
|
self.command_line.push_info(&p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::CommitEntity(entity) => {
|
|||
|
|
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
|||
|
|
self.push_undo_snapshot(i, label);
|
|||
|
|
self.commit_entity(entity);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|||
|
|
if let Some(p) = prompt {
|
|||
|
|
self.command_line.push_info(&p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::TransformSelected(handles, transform) => {
|
|||
|
|
let label = self.history_label_from_active_cmd(i, "MOVE");
|
|||
|
|
self.push_undo_snapshot(i, label);
|
|||
|
|
self.tabs[i].scene.transform_entities(&handles, &transform);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
|||
|
|
if !was_on {
|
|||
|
|
self.snapper.enabled.remove(&crate::snap::SnapType::Tangent);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
CmdResult::CopySelected(handles, transform) => {
|
|||
|
|
let label = self.history_label_from_active_cmd(i, "COPY");
|
|||
|
|
self.push_undo_snapshot(i, label);
|
|||
|
|
let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
for h in new_handles {
|
|||
|
|
self.tabs[i].scene.select_entity(h, false);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|||
|
|
if let Some(p) = prompt {
|
|||
|
|
self.command_line.push_info(&p);
|
|||
|
|
}
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
CmdResult::CommitAndExit(entity) => {
|
|||
|
|
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
|||
|
|
self.push_undo_snapshot(i, label);
|
|||
|
|
self.commit_entity(entity);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
|||
|
|
if !was_on {
|
|||
|
|
self.snapper.enabled.remove(&crate::snap::SnapType::Tangent);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::CreateBlock {
|
|||
|
|
handles,
|
|||
|
|
name,
|
|||
|
|
base,
|
|||
|
|
} => {
|
|||
|
|
self.push_undo_snapshot(i, "BLOCK");
|
|||
|
|
match self.tabs[i].scene.create_block_from_entities(&handles, &name, base) {
|
|||
|
|
Ok(insert_handle) => {
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
if !insert_handle.is_null() {
|
|||
|
|
self.tabs[i].scene.select_entity(insert_handle, false);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.command_line
|
|||
|
|
.push_output(&format!("Block \"{name}\" created."));
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
Err(err) => {
|
|||
|
|
let _ = self.tabs[i].history.undo_stack.pop();
|
|||
|
|
self.command_line.push_error(&err);
|
|||
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|||
|
|
if let Some(p) = prompt {
|
|||
|
|
self.command_line.push_info(&p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::CommitHatch(hatch) => {
|
|||
|
|
let label = self.history_label_from_active_cmd(i, "HATCH");
|
|||
|
|
self.push_undo_snapshot(i, label);
|
|||
|
|
let new_handle = self.tabs[i].scene.add_hatch(hatch);
|
|||
|
|
// Select the newly created hatch so Properties reflects it immediately.
|
|||
|
|
if !new_handle.is_null() {
|
|||
|
|
self.tabs[i].scene.select_entity(new_handle, true);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
|||
|
|
if !was_on {
|
|||
|
|
self.snapper.enabled.remove(&crate::snap::SnapType::Tangent);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
CmdResult::BatchCopy(handles, transforms) => {
|
|||
|
|
let label = self.history_label_from_active_cmd(i, "ARRAY");
|
|||
|
|
self.push_undo_snapshot(i, label);
|
|||
|
|
let count = transforms.len();
|
|||
|
|
for t in &transforms {
|
|||
|
|
self.tabs[i].scene.copy_entities(&handles, t);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
|||
|
|
if !was_on {
|
|||
|
|
self.snapper.enabled.remove(&crate::snap::SnapType::Tangent);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.command_line
|
|||
|
|
.push_output(&format!("ARRAY: {count} copies created."));
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
CmdResult::ReplaceMany(replacements, additions) => {
|
|||
|
|
let label = self.history_label_from_active_cmd(i, "FILLET");
|
|||
|
|
self.push_undo_snapshot(i, label);
|
|||
|
|
for (handle, entities) in replacements {
|
|||
|
|
self.tabs[i].scene.erase_entities(&[handle]);
|
|||
|
|
for entity in entities {
|
|||
|
|
self.tabs[i].scene.add_entity(entity);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
for entity in additions {
|
|||
|
|
self.tabs[i].scene.add_entity(entity);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
}
|
|||
|
|
CmdResult::ReplaceEntity(handle, new_entities) => {
|
|||
|
|
let label = self.history_label_from_active_cmd(i, "TRIM");
|
|||
|
|
self.push_undo_snapshot(i, label);
|
|||
|
|
self.tabs[i].scene.erase_entities(&[handle]);
|
|||
|
|
let new_handles: Vec<acadrust::Handle> = new_entities
|
|||
|
|
.into_iter()
|
|||
|
|
.map(|e| self.tabs[i].scene.add_entity(e))
|
|||
|
|
.collect();
|
|||
|
|
if let Some(cmd) = &mut self.tabs[i].active_cmd {
|
|||
|
|
cmd.on_entity_replaced(handle, &new_handles);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|||
|
|
if let Some(p) = prompt {
|
|||
|
|
self.command_line.push_info(&p);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::Cancel => {
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
|||
|
|
if !was_on {
|
|||
|
|
self.snapper.enabled.remove(&crate::snap::SnapType::Tangent);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.command_line.push_info("Command cancelled.");
|
|||
|
|
}
|
|||
|
|
CmdResult::Relaunch(cmd, handles) => {
|
|||
|
|
// Install the gathered selection then re-dispatch the original command.
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
for h in &handles {
|
|||
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
|||
|
|
if !was_on {
|
|||
|
|
self.snapper.enabled.remove(&crate::snap::SnapType::Tangent);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
let _ = self.dispatch_command(&cmd);
|
|||
|
|
}
|
|||
|
|
CmdResult::MatchEntityLayer { dest, src } => {
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
// Look up source entity's layer name.
|
|||
|
|
let src_layer = self.tabs[i].scene.document
|
|||
|
|
.get_entity(src)
|
|||
|
|
.map(|e| e.common().layer.clone());
|
|||
|
|
if let Some(layer) = src_layer {
|
|||
|
|
self.push_undo_snapshot(i, "LAYMATCH");
|
|||
|
|
for h in &dest {
|
|||
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
|||
|
|
e.as_entity_mut().set_layer(layer.clone());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.command_line.push_info(&format!("Layer matched to \"{layer}\"."));
|
|||
|
|
self.sync_ribbon_layers();
|
|||
|
|
} else {
|
|||
|
|
self.command_line.push_error("Source object not found.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::MatchProperties { dest, src } => {
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
|
|||
|
|
// Read all visual properties from source entity.
|
|||
|
|
let props = self.tabs[i].scene.document.get_entity(src).map(|e| {
|
|||
|
|
let c = e.common();
|
|||
|
|
(
|
|||
|
|
c.layer.clone(),
|
|||
|
|
c.color,
|
|||
|
|
c.linetype.clone(),
|
|||
|
|
c.linetype_scale,
|
|||
|
|
c.line_weight,
|
|||
|
|
)
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if let Some((layer, color, linetype, lt_scale, lw)) = props {
|
|||
|
|
self.push_undo_snapshot(i, "MATCHPROP");
|
|||
|
|
for h in &dest {
|
|||
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
|||
|
|
e.as_entity_mut().set_layer(layer.clone());
|
|||
|
|
crate::scene::dispatch::apply_color(e, color);
|
|||
|
|
crate::scene::dispatch::apply_line_weight(e, lw);
|
|||
|
|
e.common_mut().linetype = linetype.clone();
|
|||
|
|
e.common_mut().linetype_scale = lt_scale;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
self.command_line.push_info(
|
|||
|
|
&format!("Properties matched to {} object(s).", dest.len())
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
self.command_line.push_error("Source object not found.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::PasteClipboard { base_pt } => {
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
if self.clipboard.is_empty() {
|
|||
|
|
self.command_line.push_error("Clipboard is empty.");
|
|||
|
|
} else {
|
|||
|
|
let delta = base_pt - self.clipboard_centroid;
|
|||
|
|
let translate = command::EntityTransform::Translate(delta);
|
|||
|
|
self.push_undo_snapshot(i, "PASTECLIP");
|
|||
|
|
let count = self.clipboard.len();
|
|||
|
|
let new_handles: Vec<Handle> = self.clipboard.clone()
|
|||
|
|
.into_iter()
|
|||
|
|
.map(|mut entity| {
|
|||
|
|
crate::scene::dispatch::apply_transform(&mut entity, &translate);
|
|||
|
|
entity.common_mut().handle = acadrust::Handle::NULL;
|
|||
|
|
self.tabs[i].scene.add_entity(entity)
|
|||
|
|
})
|
|||
|
|
.filter(|h| !h.is_null())
|
|||
|
|
.collect();
|
|||
|
|
self.tabs[i].scene.deselect_all();
|
|||
|
|
for h in new_handles {
|
|||
|
|
self.tabs[i].scene.select_entity(h, false);
|
|||
|
|
}
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.refresh_properties();
|
|||
|
|
self.command_line.push_info(&format!("{count} object(s) pasted."));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CmdResult::CreateGroup { handles, name } => {
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.push_undo_snapshot(i, "GROUP");
|
|||
|
|
self.tabs[i].scene.create_group(name.clone(), handles);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
self.command_line.push_info(&format!("Group \"{}\" created.", name));
|
|||
|
|
}
|
|||
|
|
CmdResult::DeleteGroups { handles } => {
|
|||
|
|
self.tabs[i].active_cmd = None;
|
|||
|
|
self.tabs[i].snap_result = None;
|
|||
|
|
self.tabs[i].scene.clear_preview_wire();
|
|||
|
|
self.push_undo_snapshot(i, "UNGROUP");
|
|||
|
|
let count = self.tabs[i].scene.delete_groups_containing(&handles);
|
|||
|
|
self.tabs[i].dirty = true;
|
|||
|
|
if count > 0 {
|
|||
|
|
self.command_line.push_info(&format!("{} group(s) dissolved.", count));
|
|||
|
|
} else {
|
|||
|
|
self.command_line.push_info("No groups found for selected objects.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Focus the command-line input while a command is active; blur it when the command ends.
|
|||
|
|
if self.tabs[i].active_cmd.is_some() {
|
|||
|
|
self.focus_cmd_input()
|
|||
|
|
} else {
|
|||
|
|
self.blur_cmd_input()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Compute the centroid of a set of wire models (average of all points).
|
|||
|
|
fn entities_centroid(wires: &[scene::WireModel]) -> glam::Vec3 {
|
|||
|
|
let mut sum = glam::Vec3::ZERO;
|
|||
|
|
let mut count = 0usize;
|
|||
|
|
for w in wires {
|
|||
|
|
for p in &w.points {
|
|||
|
|
sum += glam::Vec3::from(*p);
|
|||
|
|
count += 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if count > 0 { sum / count as f32 } else { glam::Vec3::ZERO }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Generate the next available auto group name ("*A1", "*A2", …).
|
|||
|
|
fn next_group_auto_name(scene: &scene::Scene) -> String {
|
|||
|
|
let existing: HashSet<String> = scene.groups().map(|g| g.name.clone()).collect();
|
|||
|
|
for n in 1..=9999 {
|
|||
|
|
let name = format!("*A{n}");
|
|||
|
|
if !existing.contains(&name) {
|
|||
|
|
return name;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
"*A".to_string()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn focus_cmd_input(&self) -> Task<Message> {
|
|||
|
|
iced::widget::operation::focus(iced::widget::Id::new(ui::command_line::CMD_INPUT_ID))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn blur_cmd_input(&self) -> Task<Message> {
|
|||
|
|
let op = iced::advanced::widget::operation::focusable::unfocus::<Message>();
|
|||
|
|
iced::advanced::widget::operate(op)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn view(&self, window_id: window::Id) -> Element<'_, Message> {
|
|||
|
|
// ── Layer Properties Manager window ───────────────────────────────
|
|||
|
|
if Some(window_id) == self.layer_window {
|
|||
|
|
let tab = &self.tabs[self.active_tab];
|
|||
|
|
return tab.layers.view_window();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let i = self.active_tab;
|
|||
|
|
let tab = &self.tabs[i];
|
|||
|
|
let is_paper = tab.scene.current_layout != "Model";
|
|||
|
|
let viewport_3d = shader(&tab.scene).width(Fill).height(Fill);
|
|||
|
|
|
|||
|
|
let selection_overlay = {
|
|||
|
|
let sel = tab.scene.selection.borrow().clone();
|
|||
|
|
let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type));
|
|||
|
|
|
|||
|
|
// Compute grip markers when a single entity is selected and no command active.
|
|||
|
|
let grips: Vec<ui::overlay::GripMarker> =
|
|||
|
|
if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() {
|
|||
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|||
|
|
let bounds = iced::Rectangle {
|
|||
|
|
x: 0.0,
|
|||
|
|
y: 0.0,
|
|||
|
|
width: vw,
|
|||
|
|
height: vh,
|
|||
|
|
};
|
|||
|
|
let vp_mat = tab.scene.camera.borrow().view_proj(bounds);
|
|||
|
|
let sel_h = tab.selected_handle;
|
|||
|
|
grips_to_screen(&tab.selected_grips, vp_mat, bounds)
|
|||
|
|
.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)| {
|
|||
|
|
let is_hot = tab
|
|||
|
|
.active_grip
|
|||
|
|
.as_ref()
|
|||
|
|
.map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id);
|
|||
|
|
ui::overlay::GripMarker {
|
|||
|
|
pos: screen,
|
|||
|
|
shape,
|
|||
|
|
is_hot,
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.collect()
|
|||
|
|
} else {
|
|||
|
|
vec![]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let grid = if self.show_grid {
|
|||
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|||
|
|
let bounds = iced::Rectangle {
|
|||
|
|
x: 0.0,
|
|||
|
|
y: 0.0,
|
|||
|
|
width: vw,
|
|||
|
|
height: vh,
|
|||
|
|
};
|
|||
|
|
let cam = tab.scene.camera.borrow();
|
|||
|
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
|||
|
|
Some(overlay::GridParams {
|
|||
|
|
view_proj: cam.view_proj(bounds),
|
|||
|
|
bounds,
|
|||
|
|
plane,
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
None
|
|||
|
|
};
|
|||
|
|
overlay::selection_overlay(sel, snap_info, grips, grid)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let nav = container(overlay::nav_toolbar())
|
|||
|
|
.align_right(Fill)
|
|||
|
|
.align_top(Fill)
|
|||
|
|
.padding(iced::Padding {
|
|||
|
|
top: 148.0,
|
|||
|
|
right: 8.0,
|
|||
|
|
bottom: 0.0,
|
|||
|
|
left: 0.0,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let info = container(overlay::info_bar(
|
|||
|
|
if is_paper {
|
|||
|
|
&tab.scene.current_layout
|
|||
|
|
} else {
|
|||
|
|
"Custom View"
|
|||
|
|
},
|
|||
|
|
&tab.visual_style,
|
|||
|
|
))
|
|||
|
|
.padding([4, 6]);
|
|||
|
|
|
|||
|
|
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 cube_click = mouse_area(container(
|
|||
|
|
iced::widget::Space::new()
|
|||
|
|
.width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE))
|
|||
|
|
.height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)),
|
|||
|
|
))
|
|||
|
|
.on_move(Message::CursorMoved)
|
|||
|
|
.on_press(Message::ViewportClick);
|
|||
|
|
|
|||
|
|
let cube_click = container(cube_click)
|
|||
|
|
.align_right(Fill)
|
|||
|
|
.align_top(Fill)
|
|||
|
|
.padding(iced::Padding {
|
|||
|
|
top: VIEWCUBE_PAD,
|
|||
|
|
right: VIEWCUBE_PAD,
|
|||
|
|
bottom: 0.0,
|
|||
|
|
left: 0.0,
|
|||
|
|
})
|
|||
|
|
.width(Fill)
|
|||
|
|
.height(Fill);
|
|||
|
|
|
|||
|
|
let bg_color = if is_paper {
|
|||
|
|
Color {
|
|||
|
|
r: 0.22,
|
|||
|
|
g: 0.24,
|
|||
|
|
b: 0.28,
|
|||
|
|
a: 1.0,
|
|||
|
|
} // slightly blue-grey for paper space
|
|||
|
|
} else {
|
|||
|
|
Color {
|
|||
|
|
r: 0.11,
|
|||
|
|
g: 0.11,
|
|||
|
|
b: 0.11,
|
|||
|
|
a: 1.0,
|
|||
|
|
} // dark for model space
|
|||
|
|
};
|
|||
|
|
let viewport_stack = stack![
|
|||
|
|
container(viewport_3d)
|
|||
|
|
.style(move |_: &Theme| container::Style {
|
|||
|
|
background: Some(Background::Color(bg_color)),
|
|||
|
|
..Default::default()
|
|||
|
|
})
|
|||
|
|
.width(Fill)
|
|||
|
|
.height(Fill),
|
|||
|
|
container(info).width(Fill).height(Fill),
|
|||
|
|
selection_overlay,
|
|||
|
|
viewport_mouse,
|
|||
|
|
nav,
|
|||
|
|
cube_click,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
let viewport_stack = viewport_stack.width(Fill).height(Fill);
|
|||
|
|
let center_stack = iced::widget::stack![
|
|||
|
|
row![tab.properties.view(), viewport_stack]
|
|||
|
|
.width(Fill)
|
|||
|
|
.height(Fill),
|
|||
|
|
]
|
|||
|
|
.width(Fill)
|
|||
|
|
.height(Fill);
|
|||
|
|
let center = center_stack;
|
|||
|
|
|
|||
|
|
// Document tab bar — ribbon ile center arasında tam genişlikte
|
|||
|
|
let tab_bar = doc_tab_bar(&self.tabs, self.active_tab);
|
|||
|
|
|
|||
|
|
let main_ui = container(
|
|||
|
|
column![
|
|||
|
|
self.ribbon.view(
|
|||
|
|
is_paper,
|
|||
|
|
self.tabs[self.active_tab].history.undo_stack.len(),
|
|||
|
|
self.tabs[self.active_tab].history.redo_stack.len(),
|
|||
|
|
),
|
|||
|
|
tab_bar,
|
|||
|
|
center,
|
|||
|
|
self.command_line.view(),
|
|||
|
|
self.status_bar.view(
|
|||
|
|
&self.snapper,
|
|||
|
|
self.snap_popup_open,
|
|||
|
|
self.ortho_mode,
|
|||
|
|
self.polar_mode,
|
|||
|
|
self.show_grid,
|
|||
|
|
tab.scene.layout_names(),
|
|||
|
|
tab.scene.current_layout.clone()
|
|||
|
|
)
|
|||
|
|
]
|
|||
|
|
.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);
|
|||
|
|
|
|||
|
|
let snap_layer: Element<'_, Message> = if self.snap_popup_open {
|
|||
|
|
ui::snap_popup::snap_popup_overlay(&self.snapper, 4.0)
|
|||
|
|
} 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),
|
|||
|
|
)
|
|||
|
|
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into());
|
|||
|
|
|
|||
|
|
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer].into()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn subscription(&self) -> Subscription<Message> {
|
|||
|
|
use iced::{event, keyboard};
|
|||
|
|
iced::Subscription::batch([
|
|||
|
|
window::frames().map(Message::Tick),
|
|||
|
|
event::listen_with(|ev, status, win_id| {
|
|||
|
|
use iced::event::Status;
|
|||
|
|
match ev {
|
|||
|
|
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::KeyPressed {
|
|||
|
|
key, modifiers, ..
|
|||
|
|
}) => {
|
|||
|
|
let ctrl = modifiers.control();
|
|||
|
|
let shift = modifiers.shift();
|
|||
|
|
match key {
|
|||
|
|
keyboard::Key::Named(keyboard::key::Named::Enter)
|
|||
|
|
| keyboard::Key::Named(keyboard::key::Named::Space)
|
|||
|
|
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::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)
|
|||
|
|
}
|
|||
|
|
// ── File shortcuts ────────────────────────────
|
|||
|
|
keyboard::Key::Character(c) if ctrl => match c.as_str() {
|
|||
|
|
"n" => Some(Message::ClearScene),
|
|||
|
|
"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),
|
|||
|
|
"c" => Some(Message::Command("COPYCLIP".to_string())),
|
|||
|
|
"x" => Some(Message::Command("CUTCLIP".to_string())),
|
|||
|
|
"v" => Some(Message::Command("PASTECLIP".to_string())),
|
|||
|
|
_ => None,
|
|||
|
|
},
|
|||
|
|
_ => None,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
_ => None,
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Default for H7CAD {
|
|||
|
|
fn default() -> Self {
|
|||
|
|
Self::new()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn history_dropdown_labels(stack: &[HistorySnapshot]) -> Vec<String> {
|
|||
|
|
stack.iter().rev().map(|snapshot| snapshot.label.clone()).collect()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Grid plane detection ───────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// Choose the grid plane whose normal is most aligned with the camera view direction.
|
|||
|
|
/// Switches at the 45° octant boundary — no hysteresis needed since the grid is
|
|||
|
|
/// a visual aid and not selection-sensitive.
|
|||
|
|
fn grid_plane_from_camera(pitch: f32, yaw: f32) -> ui::overlay::GridPlane {
|
|||
|
|
use ui::overlay::GridPlane;
|
|||
|
|
// Camera forward direction components (Z-up, see camera.rs):
|
|||
|
|
// forward.x = -cos(pitch) * sin(yaw)
|
|||
|
|
// forward.y = -cos(pitch) * cos(yaw)
|
|||
|
|
// forward.z = -sin(pitch)
|
|||
|
|
let fz = pitch.sin().abs(); // Z-axis alignment → XY plane
|
|||
|
|
let fy = (pitch.cos() * yaw.cos()).abs(); // Y-axis alignment → XZ plane
|
|||
|
|
let fx = (pitch.cos() * yaw.sin()).abs(); // X-axis alignment → YZ plane
|
|||
|
|
if fz >= fy && fz >= fx {
|
|||
|
|
GridPlane::Xy
|
|||
|
|
} else if fy >= fx {
|
|||
|
|
GridPlane::Xz
|
|||
|
|
} else {
|
|||
|
|
GridPlane::Yz
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Drawing constraint helpers ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// Constrain `pt` to the nearest 90° direction from `base` (XY plane, Z-up).
|
|||
|
|
fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3) -> glam::Vec3 {
|
|||
|
|
let dx = (pt.x - base.x).abs();
|
|||
|
|
let dy = (pt.y - base.y).abs();
|
|||
|
|
if dx >= dy {
|
|||
|
|
glam::Vec3::new(pt.x, base.y, pt.z) // horizontal
|
|||
|
|
} else {
|
|||
|
|
glam::Vec3::new(base.x, pt.y, pt.z) // vertical
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Constrain `pt` to the nearest polar angle multiple from `base` (XY plane, Z-up).
|
|||
|
|
fn polar_constrain(pt: glam::Vec3, base: glam::Vec3, step_deg: f32) -> glam::Vec3 {
|
|||
|
|
let dx = pt.x - base.x;
|
|||
|
|
let dy = pt.y - base.y;
|
|||
|
|
let dist = (dx * dx + dy * dy).sqrt();
|
|||
|
|
if dist < 1e-6 {
|
|||
|
|
return pt;
|
|||
|
|
}
|
|||
|
|
let step = step_deg.to_radians();
|
|||
|
|
let angle = dy.atan2(dx);
|
|||
|
|
let snapped = (angle / step).round() * step;
|
|||
|
|
glam::Vec3::new(
|
|||
|
|
base.x + dist * snapped.cos(),
|
|||
|
|
base.y + dist * snapped.sin(),
|
|||
|
|
pt.z,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let mut bar = Row::new().spacing(0).align_y(iced::Center);
|
|||
|
|
|
|||
|
|
for (idx, tab) in tabs.iter().enumerate() {
|
|||
|
|
let is_active = idx == active_tab;
|
|||
|
|
let name = tab.tab_display_name();
|
|||
|
|
let label = if tab.dirty {
|
|||
|
|
format!("● {}", name)
|
|||
|
|
} else {
|
|||
|
|
name
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let title_btn = button(text(label).size(12))
|
|||
|
|
.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,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Show close button only when there are multiple tabs, or always show it
|
|||
|
|
let close_btn = button(text("×").size(11).color(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()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
bar = bar.push(
|
|||
|
|
container(row![title_btn, close_btn].spacing(0).align_y(iced::Center)).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(),
|
|||
|
|
},
|
|||
|
|
..Default::default()
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// "+" new tab button
|
|||
|
|
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()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
bar = bar.push(new_btn);
|
|||
|
|
bar = bar.push(iced::widget::Space::new().width(Fill));
|
|||
|
|
|
|||
|
|
container(bar)
|
|||
|
|
.style(|_: &Theme| container::Style {
|
|||
|
|
background: Some(Background::Color(BAR_BG)),
|
|||
|
|
border: Border {
|
|||
|
|
color: BORDER_COLOR,
|
|||
|
|
width: 1.0,
|
|||
|
|
radius: 0.0.into(),
|
|||
|
|
},
|
|||
|
|
..Default::default()
|
|||
|
|
})
|
|||
|
|
.height(30)
|
|||
|
|
.width(Fill)
|
|||
|
|
.padding([0, 2])
|
|||
|
|
.into()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Window icon ────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// Builds a 32×32 RGBA icon: red background with H7 drawn in white pixels.
|
|||
|
|
fn build_window_icon() -> Vec<u8> {
|
|||
|
|
const W: usize = 32;
|
|||
|
|
const SZ: usize = W * W * 4;
|
|||
|
|
|
|||
|
|
let bg = [176u8, 48, 32, 255];
|
|||
|
|
let fg = [255u8, 255, 255, 255];
|
|||
|
|
|
|||
|
|
let mut px = vec![0u8; SZ];
|
|||
|
|
for i in 0..W * W {
|
|||
|
|
px[i * 4..i * 4 + 4].copy_from_slice(&bg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Draw a 3px-wide line from (ax,ay) to (bx,by)
|
|||
|
|
fn stroke(px: &mut Vec<u8>, ax: i32, ay: i32, bx: i32, by: i32, fg: [u8; 4]) {
|
|||
|
|
let steps = ((bx - ax).abs().max((by - ay).abs()) * 3).max(1);
|
|||
|
|
for s in 0..=steps {
|
|||
|
|
let t = s as f32 / steps as f32;
|
|||
|
|
let cx = ax as f32 + (bx - ax) as f32 * t;
|
|||
|
|
let cy = ay as f32 + (by - ay) as f32 * t;
|
|||
|
|
for dy in -1i32..=1 {
|
|||
|
|
for dx in -1i32..=1 {
|
|||
|
|
let ix = cx.round() as i32 + dx;
|
|||
|
|
let iy = cy.round() as i32 + dy;
|
|||
|
|
if ix >= 0 && ix < W as i32 && iy >= 0 && iy < W as i32 {
|
|||
|
|
let idx = (iy as usize * W + ix as usize) * 4;
|
|||
|
|
px[idx..idx + 4].copy_from_slice(&fg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// H
|
|||
|
|
stroke(&mut px, 4, 5, 4, 26, fg);
|
|||
|
|
stroke(&mut px, 13, 5, 13, 26, fg);
|
|||
|
|
stroke(&mut px, 4, 15, 13, 15, fg);
|
|||
|
|
// 7
|
|||
|
|
stroke(&mut px, 17, 5, 27, 5, fg);
|
|||
|
|
stroke(&mut px, 27, 5, 20, 26, fg);
|
|||
|
|
stroke(&mut px, 20, 16, 26, 16, fg);
|
|||
|
|
|
|||
|
|
px
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Standalone helpers ────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
fn entity_type_label(entity: &acadrust::EntityType) -> String {
|
|||
|
|
use acadrust::EntityType::*;
|
|||
|
|
match entity {
|
|||
|
|
Line(_) => "Line",
|
|||
|
|
Circle(_) => "Circle",
|
|||
|
|
Arc(_) => "Arc",
|
|||
|
|
Ellipse(_) => "Ellipse",
|
|||
|
|
Spline(_) => "Spline",
|
|||
|
|
LwPolyline(_) => "Polyline",
|
|||
|
|
Text(_) => "Text",
|
|||
|
|
MText(_) => "MText",
|
|||
|
|
Dimension(_) => "Dimension",
|
|||
|
|
Insert(_) => "Block Reference",
|
|||
|
|
Point(_) => "Point",
|
|||
|
|
Hatch(_) => "Hatch",
|
|||
|
|
_ => "Entity",
|
|||
|
|
}
|
|||
|
|
.to_string()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn entity_type_key(entity: &acadrust::EntityType) -> String {
|
|||
|
|
match entity {
|
|||
|
|
acadrust::EntityType::LwPolyline(_) => "pline",
|
|||
|
|
acadrust::EntityType::Circle(_) => "circle",
|
|||
|
|
acadrust::EntityType::Line(_) => "line",
|
|||
|
|
acadrust::EntityType::Arc(_) => "arc",
|
|||
|
|
acadrust::EntityType::Ellipse(_) => "ellipse",
|
|||
|
|
acadrust::EntityType::Spline(_) => "spline",
|
|||
|
|
acadrust::EntityType::Text(_) => "text",
|
|||
|
|
acadrust::EntityType::MText(_) => "mtext",
|
|||
|
|
acadrust::EntityType::Dimension(_) => "dimension",
|
|||
|
|
acadrust::EntityType::Insert(_) => "insert",
|
|||
|
|
acadrust::EntityType::Point(_) => "point",
|
|||
|
|
acadrust::EntityType::Hatch(_) => "hatch",
|
|||
|
|
_ => "entity",
|
|||
|
|
}
|
|||
|
|
.to_string()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn build_selection_groups(
|
|||
|
|
selected: &[(Handle, &EntityType)],
|
|||
|
|
) -> Vec<ui::properties::SelectionGroup> {
|
|||
|
|
let mut groups = vec![ui::properties::SelectionGroup {
|
|||
|
|
label: format!("All({})", selected.len()),
|
|||
|
|
handles: selected.iter().map(|(handle, _)| *handle).collect(),
|
|||
|
|
}];
|
|||
|
|
|
|||
|
|
let mut by_type: std::collections::BTreeMap<String, Vec<Handle>> = std::collections::BTreeMap::new();
|
|||
|
|
for (handle, entity) in selected {
|
|||
|
|
by_type.entry(entity_type_key(entity)).or_default().push(*handle);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (kind, handles) in by_type {
|
|||
|
|
groups.push(ui::properties::SelectionGroup {
|
|||
|
|
label: format!("{}({})", title_case_word(&kind), handles.len()),
|
|||
|
|
handles,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
groups
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn title_case_word(value: &str) -> String {
|
|||
|
|
let mut chars = value.chars();
|
|||
|
|
match chars.next() {
|
|||
|
|
Some(first) => {
|
|||
|
|
let mut out = first.to_uppercase().collect::<String>();
|
|||
|
|
out.push_str(chars.as_str());
|
|||
|
|
out
|
|||
|
|
}
|
|||
|
|
None => String::new(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn aggregate_sections(
|
|||
|
|
selected: &[(Handle, &EntityType)],
|
|||
|
|
text_style_names: &[String],
|
|||
|
|
) -> Vec<crate::scene::object::PropSection> {
|
|||
|
|
if selected.is_empty() {
|
|||
|
|
return vec![];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut all_sections: Vec<Vec<crate::scene::object::PropSection>> = selected
|
|||
|
|
.iter()
|
|||
|
|
.map(|(handle, entity)| dispatch::properties_sectioned(*handle, entity, text_style_names))
|
|||
|
|
.collect();
|
|||
|
|
|
|||
|
|
let mut result = all_sections.remove(0);
|
|||
|
|
for sections in all_sections {
|
|||
|
|
result = merge_sections(&result, §ions);
|
|||
|
|
}
|
|||
|
|
result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn merge_sections(
|
|||
|
|
left: &[crate::scene::object::PropSection],
|
|||
|
|
right: &[crate::scene::object::PropSection],
|
|||
|
|
) -> Vec<crate::scene::object::PropSection> {
|
|||
|
|
left.iter()
|
|||
|
|
.filter_map(|section| {
|
|||
|
|
let rhs = right.iter().find(|candidate| candidate.title == section.title)?;
|
|||
|
|
let props: Vec<crate::scene::object::Property> = section
|
|||
|
|
.props
|
|||
|
|
.iter()
|
|||
|
|
.filter_map(|prop| {
|
|||
|
|
let other = rhs.props.iter().find(|candidate| candidate.field == prop.field)?;
|
|||
|
|
Some(crate::scene::object::Property {
|
|||
|
|
label: prop.label.clone(),
|
|||
|
|
field: prop.field,
|
|||
|
|
value: merge_prop_value(&prop.value, &other.value),
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
if props.is_empty() {
|
|||
|
|
None
|
|||
|
|
} else {
|
|||
|
|
Some(crate::scene::object::PropSection {
|
|||
|
|
title: section.title.clone(),
|
|||
|
|
props,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.collect()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn merge_prop_value(
|
|||
|
|
left: &crate::scene::object::PropValue,
|
|||
|
|
right: &crate::scene::object::PropValue,
|
|||
|
|
) -> crate::scene::object::PropValue {
|
|||
|
|
use crate::scene::object::PropValue;
|
|||
|
|
|
|||
|
|
if left == right {
|
|||
|
|
return left.clone();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
match (left, right) {
|
|||
|
|
(PropValue::LayerChoice(_), PropValue::LayerChoice(_)) => {
|
|||
|
|
PropValue::LayerChoice(VARIES_LABEL.into())
|
|||
|
|
}
|
|||
|
|
(PropValue::ColorChoice(_), PropValue::ColorChoice(_))
|
|||
|
|
| (PropValue::ColorVaries, _)
|
|||
|
|
| (_, PropValue::ColorVaries) => PropValue::ColorVaries,
|
|||
|
|
(PropValue::LwChoice(_), PropValue::LwChoice(_))
|
|||
|
|
| (PropValue::LwVaries, _)
|
|||
|
|
| (_, PropValue::LwVaries) => PropValue::LwVaries,
|
|||
|
|
(PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => {
|
|||
|
|
PropValue::LinetypeChoice(VARIES_LABEL.into())
|
|||
|
|
}
|
|||
|
|
(
|
|||
|
|
PropValue::Choice { options, .. },
|
|||
|
|
PropValue::Choice {
|
|||
|
|
options: other_options,
|
|||
|
|
..
|
|||
|
|
},
|
|||
|
|
) if options == other_options => PropValue::Choice {
|
|||
|
|
selected: VARIES_LABEL.into(),
|
|||
|
|
options: options.clone(),
|
|||
|
|
},
|
|||
|
|
(PropValue::EditText(_), PropValue::EditText(_)) => {
|
|||
|
|
PropValue::EditText(VARIES_LABEL.into())
|
|||
|
|
}
|
|||
|
|
(PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => {
|
|||
|
|
PropValue::ReadOnly(VARIES_LABEL.into())
|
|||
|
|
}
|
|||
|
|
(PropValue::HatchPatternChoice(_), PropValue::HatchPatternChoice(_)) => {
|
|||
|
|
PropValue::HatchPatternChoice(VARIES_LABEL.into())
|
|||
|
|
}
|
|||
|
|
(
|
|||
|
|
PropValue::BoolToggle { field, .. },
|
|||
|
|
PropValue::BoolToggle {
|
|||
|
|
field: other_field, ..
|
|||
|
|
},
|
|||
|
|
) if field == other_field => PropValue::ReadOnly(VARIES_LABEL.into()),
|
|||
|
|
_ => left.clone(),
|
|||
|
|
}
|
|||
|
|
}
|