forked from 1b-agent/opencadstudio-upstream
532 lines
21 KiB
Rust
532 lines
21 KiB
Rust
use super::*;
|
|
|
|
impl OpenCADStudio {
|
|
pub(super) fn handle_mview_create(
|
|
&mut self,
|
|
viewport: codec::entities::Viewport,
|
|
preserve_view: bool,
|
|
) {
|
|
let i = self.active_tab;
|
|
let saved_view = preserve_view.then(|| {
|
|
(
|
|
viewport.view_target.clone(),
|
|
viewport.view_direction.clone(),
|
|
viewport.view_center.clone(),
|
|
viewport.view_height,
|
|
viewport.custom_scale,
|
|
viewport.lens_length,
|
|
viewport.twist_angle,
|
|
viewport.status.perspective,
|
|
)
|
|
});
|
|
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
|
let pending = self.begin_undo(i, label, 1, true);
|
|
let handle = self.commit_entity_handle(codec::EntityType::Viewport(viewport));
|
|
if let (Some(handle), Some(saved)) = (handle, saved_view) {
|
|
if let Some(codec::EntityType::Viewport(viewport)) =
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
{
|
|
viewport.view_target = saved.0;
|
|
viewport.view_direction = saved.1;
|
|
viewport.view_center = saved.2;
|
|
viewport.view_height = saved.3;
|
|
viewport.custom_scale = saved.4;
|
|
viewport.lens_length = saved.5;
|
|
viewport.twist_angle = saved.6;
|
|
viewport.status.perspective = saved.7;
|
|
}
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
}
|
|
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.restore_pre_cmd_tangent();
|
|
if let Some(pending) = pending {
|
|
self.commit_undo_delta(i, pending);
|
|
}
|
|
}
|
|
|
|
/// VPCLIP: clip `viewport` to a new polygon (`boundary`) or a picked closed
|
|
/// paper-space object (`boundary_handle`), or — with neither — delete its
|
|
/// clip. The viewport shrinks to the boundary's extents and its view
|
|
/// moves with it, so the model stays where it was on paper; the old
|
|
/// boundary is erased. One undo step.
|
|
pub(super) fn handle_vpclip(
|
|
&mut self,
|
|
viewport: Handle,
|
|
boundary: Option<codec::EntityType>,
|
|
boundary_handle: Handle,
|
|
) {
|
|
let i = self.active_tab;
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
let Some(codec::EntityType::Viewport(vp)) =
|
|
self.tabs[i].scene.document.get_entity(viewport).cloned()
|
|
else {
|
|
return;
|
|
};
|
|
if boundary.is_none() && !boundary_handle.is_null() {
|
|
let scene = &self.tabs[i].scene;
|
|
let closed = scene.entity_belongs_to_current_layout(boundary_handle)
|
|
&& scene
|
|
.document
|
|
.get_entity(boundary_handle)
|
|
.is_some_and(|entity| match entity {
|
|
codec::EntityType::Circle(_) => true,
|
|
codec::EntityType::Ellipse(ellipse) => ellipse.is_full(),
|
|
codec::EntityType::LwPolyline(polyline) => polyline.is_closed,
|
|
codec::EntityType::Polyline(polyline) => polyline.is_closed(),
|
|
codec::EntityType::Polyline2D(polyline) => polyline.is_closed(),
|
|
_ => false,
|
|
});
|
|
if !closed {
|
|
self.command_line
|
|
.push_error(crate::t!("Object is not a closed curve.").as_ref());
|
|
return;
|
|
}
|
|
}
|
|
self.push_undo_snapshot(i, "VPCLIP");
|
|
let old = vp.clip_boundary_handle;
|
|
let scene = &mut self.tabs[i].scene;
|
|
let created = boundary.is_some();
|
|
let clip = match boundary {
|
|
Some(entity) => scene.add_entity(entity),
|
|
None => boundary_handle,
|
|
};
|
|
let mut vp = vp;
|
|
if clip.is_null() {
|
|
vp.clip_boundary_handle = Handle::NULL;
|
|
vp.status.non_rectangular_clipping = false;
|
|
} else {
|
|
let (lo, hi) = clip_boundary_extents(scene, clip)
|
|
.map(|(lo, hi)| (glam::DVec2::from(lo), glam::DVec2::from(hi)))
|
|
.unwrap_or((glam::DVec2::ZERO, glam::DVec2::ZERO));
|
|
if hi.x - lo.x < 1e-6 || hi.y - lo.y < 1e-6 {
|
|
// The polygon drawn for this clip must not stay behind.
|
|
if created {
|
|
scene.erase_entities(&[clip]);
|
|
}
|
|
self.command_line.push_error(
|
|
crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref(),
|
|
);
|
|
return;
|
|
}
|
|
// Model units per paper unit; the view centre follows the paper
|
|
// centre so the model does not move on the sheet.
|
|
let scale = if vp.height.abs() > 1e-12 {
|
|
vp.view_height / vp.height
|
|
} else {
|
|
1.0
|
|
};
|
|
let center = (lo + hi) * 0.5;
|
|
let (dx, dy) = (center.x - vp.center.x, center.y - vp.center.y);
|
|
let (s, c) = (-vp.twist_angle).sin_cos();
|
|
vp.view_center.x += (dx * c - dy * s) * scale;
|
|
vp.view_center.y += (dx * s + dy * c) * scale;
|
|
vp.center.x = center.x;
|
|
vp.center.y = center.y;
|
|
vp.width = hi.x - lo.x;
|
|
vp.height = hi.y - lo.y;
|
|
vp.view_height = vp.height * scale;
|
|
vp.clip_boundary_handle = clip;
|
|
vp.status.non_rectangular_clipping = true;
|
|
if let Some(entity) = scene.document.get_entity_mut(clip) {
|
|
let common = entity.common_mut();
|
|
if !common.reactors.contains(&viewport) {
|
|
common.reactors.push(viewport);
|
|
}
|
|
}
|
|
}
|
|
if let Some(entity) = scene.document.get_entity_mut(viewport) {
|
|
*entity = codec::EntityType::Viewport(vp);
|
|
}
|
|
if !old.is_null() && old != clip {
|
|
scene.erase_entities(&[old]);
|
|
}
|
|
let mut changes = vec![(viewport, crate::scene::ChangeKind::Modified)];
|
|
if !clip.is_null() {
|
|
changes.push((clip, crate::scene::ChangeKind::Modified));
|
|
}
|
|
scene.bump_entities(&changes);
|
|
scene.camera_generation += 1;
|
|
self.tabs[i].dirty = true;
|
|
self.restore_pre_cmd_tangent();
|
|
}
|
|
|
|
pub(super) fn handle_mview_create_clipped(
|
|
&mut self,
|
|
boundary: Option<codec::EntityType>,
|
|
boundary_handle: Handle,
|
|
) -> Option<Task<Message>> {
|
|
let i = self.active_tab;
|
|
if boundary.is_none() {
|
|
let scene = &self.tabs[i].scene;
|
|
let valid = scene.entity_belongs_to_current_layout(boundary_handle)
|
|
&& scene
|
|
.document
|
|
.get_entity(boundary_handle)
|
|
.is_some_and(|entity| match entity {
|
|
codec::EntityType::Circle(_) => true,
|
|
codec::EntityType::Ellipse(ellipse) => ellipse.is_full(),
|
|
codec::EntityType::LwPolyline(polyline) => polyline.is_closed,
|
|
codec::EntityType::Polyline(polyline) => polyline.is_closed(),
|
|
codec::EntityType::Polyline2D(polyline) => polyline.is_closed(),
|
|
codec::EntityType::Polyline3D(polyline) => polyline.flags.closed,
|
|
_ => false,
|
|
});
|
|
if !valid {
|
|
self.command_line.push_error(
|
|
crate::t!(
|
|
"MVIEW Object: select a closed paper-space circle, ellipse, or polyline."
|
|
)
|
|
.as_ref(),
|
|
);
|
|
if let Some(prompt) = self.tabs[i]
|
|
.active_cmd
|
|
.as_ref()
|
|
.map(|command| command.prompt())
|
|
{
|
|
self.command_line.push_info(&prompt);
|
|
}
|
|
return Some(Task::none());
|
|
}
|
|
}
|
|
|
|
let created_boundary = boundary.is_some();
|
|
let touched = 2;
|
|
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
|
let pending = self.begin_undo(i, label, touched, true);
|
|
let clip_handle = match boundary {
|
|
Some(boundary) => {
|
|
// A non-rectangular viewport owns its boundary through
|
|
// `clip_boundary_handle`: a visible paper-space object that
|
|
// is picked and erased together with the viewport.
|
|
match self.commit_entity_handle(boundary) {
|
|
Some(handle) => handle,
|
|
None => {
|
|
self.tabs[i].active_cmd = None;
|
|
if let Some(pending) = pending {
|
|
self.commit_undo_delta(i, pending);
|
|
}
|
|
return Some(Task::none());
|
|
}
|
|
}
|
|
}
|
|
None => boundary_handle,
|
|
};
|
|
let bounds = clip_boundary_extents(&self.tabs[i].scene, clip_handle);
|
|
let Some(([min_x, min_y], [max_x, max_y])) = bounds else {
|
|
self.command_line
|
|
.push_error(crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref());
|
|
self.tabs[i].active_cmd = None;
|
|
if let Some(pending) = pending {
|
|
self.commit_undo_delta(i, pending);
|
|
}
|
|
return Some(Task::none());
|
|
};
|
|
if max_x - min_x < 1e-6 || max_y - min_y < 1e-6 {
|
|
self.command_line
|
|
.push_error(crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref());
|
|
self.tabs[i].active_cmd = None;
|
|
if let Some(pending) = pending {
|
|
self.commit_undo_delta(i, pending);
|
|
}
|
|
return Some(Task::none());
|
|
}
|
|
|
|
let mut viewport = codec::entities::Viewport::new();
|
|
viewport.center =
|
|
codec::types::Vector3::new((min_x + max_x) / 2.0, (min_y + max_y) / 2.0, 0.0);
|
|
viewport.width = max_x - min_x;
|
|
viewport.height = max_y - min_y;
|
|
viewport.id = 2;
|
|
viewport.clip_boundary_handle = clip_handle;
|
|
viewport.status.non_rectangular_clipping = !clip_handle.is_null();
|
|
let viewport_handle = self.commit_entity_handle(codec::EntityType::Viewport(viewport));
|
|
if let Some(viewport_handle) = viewport_handle {
|
|
if !created_boundary {
|
|
let before = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.get_entity(clip_handle)
|
|
.cloned()
|
|
.map(std::sync::Arc::new);
|
|
self.tabs[i].scene.record_undo_before(clip_handle, before);
|
|
}
|
|
if let Some(boundary) = self.tabs[i].scene.document.get_entity_mut(clip_handle) {
|
|
let common = boundary.common_mut();
|
|
if !common.reactors.contains(&viewport_handle) {
|
|
common.reactors.push(viewport_handle);
|
|
}
|
|
}
|
|
self.tabs[i]
|
|
.scene
|
|
.bump_entities(&[(clip_handle, crate::scene::ChangeKind::Modified)]);
|
|
}
|
|
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.restore_pre_cmd_tangent();
|
|
if let Some(pending) = pending {
|
|
self.commit_undo_delta(i, pending);
|
|
}
|
|
None
|
|
}
|
|
|
|
pub(super) fn handle_wipeout_from_polyline(
|
|
&mut self,
|
|
handle: Handle,
|
|
erase_source: bool,
|
|
) -> Option<Task<Message>> {
|
|
let i = self.active_tab;
|
|
let wipeout = {
|
|
let scene = &self.tabs[i].scene;
|
|
scene
|
|
.entity_belongs_to_active_space(handle)
|
|
.then(|| scene.document.get_entity(handle))
|
|
.flatten()
|
|
.and_then(crate::modules::draw::draw::wipeout::wipeout_from_polyline)
|
|
};
|
|
if let Some(wipeout) = wipeout {
|
|
if erase_source {
|
|
return Some(self.apply_cmd_result(CmdResult::ReplaceMany(
|
|
vec![(handle, Vec::new())],
|
|
vec![wipeout],
|
|
)));
|
|
}
|
|
return Some(self.apply_cmd_result(CmdResult::CommitAndExit(wipeout)));
|
|
}
|
|
self.command_line.push_error(
|
|
crate::t!("WIPEOUT Polyline: select a straight, closed, planar 2D polyline with at least 3 non-intersecting vertices.").as_ref(),
|
|
);
|
|
let command = crate::modules::draw::draw::wipeout::WipeoutCommand::new_polyline();
|
|
self.command_line
|
|
.push_info(&crate::command::CadCommand::prompt(&command));
|
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
None
|
|
}
|
|
|
|
pub(super) fn handle_mview_switch_layout(&mut self, layout: String) -> Task<Message> {
|
|
let i = self.active_tab;
|
|
let task = self.on_layout_switch_preserving_command(layout);
|
|
if let Some(prompt) = self.tabs[i]
|
|
.active_cmd
|
|
.as_ref()
|
|
.map(|command| command.prompt())
|
|
{
|
|
self.command_line.push_info(&prompt);
|
|
}
|
|
task
|
|
}
|
|
|
|
pub(super) fn handle_mview_cancel_to_layout(&mut self, layout: String) -> Task<Message> {
|
|
let i = self.active_tab;
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
self.restore_pre_cmd_tangent();
|
|
self.on_layout_switch(layout)
|
|
}
|
|
|
|
pub(super) fn handle_vp_layer_update(
|
|
&mut self,
|
|
vp_handle: Handle,
|
|
freeze: Vec<String>,
|
|
thaw: Vec<String>,
|
|
) {
|
|
let i = self.active_tab;
|
|
// Resolve layer names → handles, then update frozen_layers on the viewport(s).
|
|
// vp_handle == Handle::NULL means "apply to all viewports in current layout".
|
|
let freeze_handles: Vec<Handle> = freeze
|
|
.iter()
|
|
.filter_map(|name| {
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.layers
|
|
.iter()
|
|
.find(|l| l.name.eq_ignore_ascii_case(name))
|
|
.map(|l| l.handle)
|
|
})
|
|
.collect();
|
|
let thaw_handles: Vec<Handle> = thaw
|
|
.iter()
|
|
.filter_map(|name| {
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.layers
|
|
.iter()
|
|
.find(|l| l.name.eq_ignore_ascii_case(name))
|
|
.map(|l| l.handle)
|
|
})
|
|
.collect();
|
|
|
|
let mut frozen_count = 0usize;
|
|
let mut thawed_count = 0usize;
|
|
|
|
// Collect target viewport handles
|
|
let target_handles: Vec<Handle> = if vp_handle == codec::Handle::NULL {
|
|
// All viewports in current layout block
|
|
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter(|e| {
|
|
e.common().owner_handle == block_handle
|
|
&& matches!(e, codec::EntityType::Viewport(_))
|
|
})
|
|
.map(|e| e.common().handle)
|
|
.collect()
|
|
} else {
|
|
vec![vp_handle]
|
|
};
|
|
|
|
for &target_handle in &target_handles {
|
|
if let Some(codec::EntityType::Viewport(vp)) =
|
|
self.tabs[i].scene.document.get_entity_mut(target_handle)
|
|
{
|
|
for h in &freeze_handles {
|
|
if !vp.frozen_layers.contains(h) {
|
|
vp.frozen_layers.push(*h);
|
|
frozen_count += 1;
|
|
}
|
|
}
|
|
for h in &thaw_handles {
|
|
let before = vp.frozen_layers.len();
|
|
vp.frozen_layers.retain(|fh| fh != h);
|
|
if vp.frozen_layers.len() < before {
|
|
thawed_count += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if frozen_count > 0 || thawed_count > 0 {
|
|
self.push_undo_snapshot(i, "VPLAYER");
|
|
self.tabs[i].dirty = true;
|
|
if frozen_count > 0 {
|
|
self.command_line.push_info(
|
|
crate::tf!("VPLAYER: {frozen_count} layer(s) frozen in viewport.").as_ref(),
|
|
);
|
|
}
|
|
if thawed_count > 0 {
|
|
self.command_line.push_info(
|
|
crate::tf!("VPLAYER: {thawed_count} layer(s) thawed in viewport.").as_ref(),
|
|
);
|
|
}
|
|
// Sync layer panel so VP freeze columns update immediately.
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
let vp_info = self.tabs[i].scene.viewport_list();
|
|
self.tabs[i]
|
|
.layers
|
|
.sync_with_viewports(&doc_layers, vp_info);
|
|
}
|
|
|
|
// Show updated prompt (command stays active for more operations).
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|
if let Some(p) = prompt {
|
|
self.command_line.push_info(&p);
|
|
}
|
|
}
|
|
|
|
pub(in crate::app) fn handle_zoom_to_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
|
|
let i = self.active_tab;
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.tabs[i].scene.remember_current_view();
|
|
self.tabs[i]
|
|
.scene
|
|
.zoom_to_window(p1.as_vec3(), p2.as_vec3());
|
|
self.command_line
|
|
.push_output(crate::t!("Zoom Window").as_ref());
|
|
}
|
|
|
|
pub(super) fn handle_set_plot_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
|
|
let i = self.active_tab;
|
|
let layout_name = self.tabs[i].scene.current_layout.clone();
|
|
if layout_name == "Model" {
|
|
// Model space: remember the window (world X/Y) for the plot dialog.
|
|
let x0 = p1.x.min(p2.x);
|
|
let y0 = p1.y.min(p2.y);
|
|
let x1 = p1.x.max(p2.x);
|
|
let y1 = p1.y.max(p2.y);
|
|
self.plot_window = Some((x0, y0, x1, y1));
|
|
self.plot_dialog.window = self.plot_window;
|
|
self.command_line.push_output(
|
|
crate::tf!("Plot window: {x0:.2},{y0:.2} to {x1:.2},{y1:.2}").as_ref(),
|
|
);
|
|
// Pick window closed the plot dialog so the viewport could
|
|
// receive the two clicks — bring the dialog back with the
|
|
// window now active.
|
|
self.plot_dialog.area = "Window".to_string();
|
|
// Remember the pick immediately, like the printer choice.
|
|
self.save_config();
|
|
self.active_modal = Some(super::super::ModalKind::Plot);
|
|
} else {
|
|
// PLOTWINDOW always describes the plotted layout. In MSPACE
|
|
// the command points are model coordinates, so map them back
|
|
// through the active floating viewport first.
|
|
let p1 = self.tabs[i].scene.model_to_paper(p1);
|
|
let p2 = self.tabs[i].scene.model_to_paper(p2);
|
|
let x1 = p1.x.min(p2.x);
|
|
let y1 = p1.y.min(p2.y);
|
|
let x2 = p1.x.max(p2.x);
|
|
let y2 = p1.y.max(p2.y);
|
|
self.plot_window = Some((x1, y1, x2, y2));
|
|
self.plot_dialog.window = self.plot_window;
|
|
self.command_line.push_output(
|
|
crate::tf!("Plot window: {x1:.2},{y1:.2} to {x2:.2},{y2:.2}").as_ref(),
|
|
);
|
|
self.plot_dialog.area = "Window".to_string();
|
|
self.save_config();
|
|
self.active_modal = Some(super::super::ModalKind::Plot);
|
|
}
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.restore_pre_cmd_tangent();
|
|
}
|
|
|
|
pub(super) fn handle_quick_print(&mut self, handles: Vec<Handle>) -> Task<Message> {
|
|
let i = self.active_tab;
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.restore_pre_cmd_tangent();
|
|
self.on_quick_print_handles(handles)
|
|
}
|
|
}
|
|
|
|
/// Paper-space extents of a viewport clip boundary: exact for lines, arcs
|
|
/// and circles (bulges included), else from its outline.
|
|
fn clip_boundary_extents(
|
|
scene: &crate::scene::Scene,
|
|
clip: Handle,
|
|
) -> Option<([f64; 2], [f64; 2])> {
|
|
let exact = scene
|
|
.document
|
|
.get_entity(clip)
|
|
.and_then(crate::entities::curve::entity_curve_xy)
|
|
.and_then(|curve| kernel::geom2d::analytic_curve_bounds(&[curve]));
|
|
if exact.is_some() {
|
|
return exact;
|
|
}
|
|
let polygon = scene.clip_boundary_polygon(clip, 0.0);
|
|
polygon
|
|
.iter()
|
|
.filter(|p| p[0].is_finite() && p[1].is_finite())
|
|
.fold(None, |bounds: Option<([f64; 2], [f64; 2])>, p| {
|
|
let (x, y) = (p[0] as f64, p[1] as f64);
|
|
Some(match bounds {
|
|
Some((lo, hi)) => ([lo[0].min(x), lo[1].min(y)], [hi[0].max(x), hi[1].max(y)]),
|
|
None => ([x, y], [x, y]),
|
|
})
|
|
})
|
|
}
|