forked from 1b-agent/opencadstudio-upstream
CLIP on raster images and viewports, IMAGECLIP, VPCLIP, clip depth rules
- CLIP continues as IMAGECLIP on a raster image and as VPCLIP on a layout viewport, with the reference prompts and stored data. - Image clip boundaries sit on pixel centres, OFF only clears the display flag, and an inverted boundary cuts the image. - VPCLIP polygon (Arc/Length/Close/Undo), clipping object and Delete; the viewport shrinks to the boundary's exact extents, view kept. - XCLIP Clipdepth: Remove clears only the plane asked, a front plane behind the back one is rejected with the reference message. - Attached and imported images carry the reference display flags. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
857f2f2e58
commit
bd17e7e0d4
12 files changed
+569
-112
No files matched your search
@@ -471,8 +471,11 @@ impl OpenCADStudio {
|
||||
CmdResult::MviewCreateClipped {
|
||||
boundary,
|
||||
boundary_handle,
|
||||
target,
|
||||
} => {
|
||||
if let Some(task) = self.handle_mview_create_clipped(boundary, boundary_handle) {
|
||||
if !target.is_null() {
|
||||
self.handle_vpclip(target, boundary, boundary_handle);
|
||||
} else if let Some(task) = self.handle_mview_create_clipped(boundary, boundary_handle) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,101 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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("Object is not a closed curve.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.push_undo_snapshot(i, "VPCLIP");
|
||||
let old = vp.clip_boundary_handle;
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
let clip = match boundary {
|
||||
Some(mut entity) => {
|
||||
entity.common_mut().invisible = true;
|
||||
scene.add_entity(entity)
|
||||
}
|
||||
None => boundary_handle,
|
||||
};
|
||||
let mut vp = vp;
|
||||
if clip.is_null() {
|
||||
vp.clip_boundary_handle = Handle::NULL;
|
||||
} 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 {
|
||||
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;
|
||||
if let Some(entity) = scene.document.get_entity_mut(clip) {
|
||||
let common = entity.common_mut();
|
||||
common.invisible = true;
|
||||
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>,
|
||||
@@ -107,27 +202,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
None => boundary_handle,
|
||||
};
|
||||
let polygon = self.tabs[i].scene.clip_boundary_polygon(clip_handle, 0.0);
|
||||
let bounds: Option<(f64, f64, f64, f64)> = polygon.iter().fold(None, |bounds, point| {
|
||||
if !point[0].is_finite() || !point[1].is_finite() {
|
||||
return bounds;
|
||||
}
|
||||
Some(match bounds {
|
||||
Some((min_x, min_y, max_x, max_y)) => (
|
||||
min_x.min(point[0] as f64),
|
||||
min_y.min(point[1] as f64),
|
||||
max_x.max(point[0] as f64),
|
||||
max_y.max(point[1] as f64),
|
||||
),
|
||||
None => (
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
),
|
||||
})
|
||||
});
|
||||
let Some((min_x, min_y, max_x, max_y)) = bounds else {
|
||||
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;
|
||||
@@ -412,3 +488,27 @@ impl OpenCADStudio {
|
||||
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]),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -715,6 +715,18 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
"IMAGECLIP" => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::modules::insert::pdf_clip::PdfClipCommand::image();
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
"VPCLIP" => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::modules::layout::mview::MviewCommand::vpclip_select();
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
"CLIP" => {
|
||||
use crate::command::CadCommand;
|
||||
let command = crate::modules::insert::xclip::ClipCommand::new();
|
||||
|
||||
@@ -37,6 +37,9 @@ impl OpenCADStudio {
|
||||
inserts: Vec<codec::Handle>,
|
||||
action: XclipAction,
|
||||
) {
|
||||
if let XclipAction::Depth { rejected: true, .. } = action {
|
||||
self.command_line.push_error(crate::modules::insert::xclip::DEPTH_REJECTED);
|
||||
}
|
||||
self.push_undo_snapshot(i, "XCLIP");
|
||||
let extents: Vec<_> = inserts.iter().map(|h| self.insert_extents(i, *h)).collect();
|
||||
let current_layer = self.tabs[i].scene.document.header.current_layer_name.clone();
|
||||
@@ -51,7 +54,7 @@ impl OpenCADStudio {
|
||||
XclipAction::Delete => {
|
||||
clip::remove_insert_clip(doc, *insert);
|
||||
}
|
||||
XclipAction::Depth { front, back } => {
|
||||
XclipAction::Depth { front, back, .. } => {
|
||||
clip::set_insert_clip_depth(doc, *insert, *front, *back);
|
||||
}
|
||||
XclipAction::New { boundary, inverted } => {
|
||||
|
||||
@@ -1938,6 +1938,9 @@ pub enum CmdResult {
|
||||
MviewCreateClipped {
|
||||
boundary: Option<EntityType>,
|
||||
boundary_handle: Handle,
|
||||
/// VPCLIP: the viewport to clip instead of creating one (NULL for
|
||||
/// MVIEW). With neither boundary nor handle its clip is deleted.
|
||||
target: Handle,
|
||||
},
|
||||
/// Create a wipeout from an existing closed polyline in the active space.
|
||||
/// `erase_source` controls whether the source boundary is consumed.
|
||||
|
||||
@@ -63,6 +63,53 @@ fn image_wire(corners: [[f64; 3]; 4], with_x: bool) -> Vec<[f64; 3]> {
|
||||
pts
|
||||
}
|
||||
|
||||
/// The clip boundary of a raster image in drawing pixel units (x along the
|
||||
/// u-vector, y along the v-vector, 0 at the insertion corner), or `None`
|
||||
/// when the image shows unclipped. The stored boundary is in raster space:
|
||||
/// pixel centres at whole numbers, row 0 at the top — the frame edge is at
|
||||
/// -0.5, so a stored `(x, y)` sits at `(x + 0.5, h - y - 0.5)`. Clipping
|
||||
/// shows only while both the clip state and the display flag are on (the
|
||||
/// OFF option clears the flag and keeps the state).
|
||||
pub(crate) fn image_clip_polygon(img: &RasterImage) -> Option<Vec<[f64; 2]>> {
|
||||
use codec::entities::{ClipType, ImageDisplayFlags};
|
||||
if !img.clipping_enabled || !img.flags.contains(ImageDisplayFlags::USE_CLIPPING_BOUNDARY) {
|
||||
return None;
|
||||
}
|
||||
let h = img.size.y;
|
||||
let at = |x: f64, y: f64| [x + 0.5, h - y - 0.5];
|
||||
let cb = &img.clip_boundary;
|
||||
match cb.clip_type {
|
||||
ClipType::Rectangular if cb.vertices.len() >= 2 => {
|
||||
let (a, b) = (cb.vertices[0], cb.vertices[1]);
|
||||
Some(vec![at(a.x, a.y), at(b.x, a.y), at(b.x, b.y), at(a.x, b.y)])
|
||||
}
|
||||
ClipType::Polygonal if cb.vertices.len() >= 3 => {
|
||||
let mut poly: Vec<[f64; 2]> = cb.vertices.iter().map(|v| at(v.x, v.y)).collect();
|
||||
// The stored polygon repeats its first vertex at the end.
|
||||
if poly.len() > 3 && poly.first() == poly.last() {
|
||||
poly.pop();
|
||||
}
|
||||
Some(poly)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// World point → raster clip space (the inverse of `image_clip_polygon`'s
|
||||
/// mapping), for the clip commands.
|
||||
pub(crate) fn image_world_to_clip(img: &RasterImage, p: [f64; 3]) -> codec::types::Vector2 {
|
||||
let (u, v) = (&img.u_vector, &img.v_vector);
|
||||
let d = [p[0] - img.insertion_point.x, p[1] - img.insertion_point.y];
|
||||
// Solve d = a·u + b·v in the image plane (x/y components).
|
||||
let det = u.x * v.y - u.y * v.x;
|
||||
let (a, b) = if det.abs() > 1e-300 {
|
||||
((d[0] * v.y - d[1] * v.x) / det, (u.x * d[1] - u.y * d[0]) / det)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
codec::types::Vector2::new(a - 0.5, img.size.y - b - 0.5)
|
||||
}
|
||||
|
||||
fn reflect_vec3(vx: &mut f64, vy: &mut f64, ax: f64, ay: f64, len2: f64) {
|
||||
let dot = *vx * ax + *vy * ay;
|
||||
*vx = 2.0 * dot * ax / len2 - *vx;
|
||||
@@ -93,43 +140,28 @@ impl RenderConvertible for RasterImage {
|
||||
]
|
||||
};
|
||||
|
||||
// Clip-boundary Y is in image raster space (row 0 = top, Y down); the
|
||||
// image's v-vector points up, so flip each vertex's Y (`ih - y`) to
|
||||
// place the boundary where AutoCAD draws it. Must match the raster's
|
||||
// own clip triangulation in `ImageModel` so outline and pixels align.
|
||||
let ih = self.size.y;
|
||||
// Diagonals are the BROKEN-reference placeholder; a resolvable image
|
||||
// draws its pixels inside the frame, so the X would scribble over it.
|
||||
let path_probe = self.file_path.trim();
|
||||
let resolvable = path_probe.is_empty()
|
||||
|| crate::scene::model::image_model::resolve_image(path_probe).is_some();
|
||||
let pts = if self.clipping_enabled {
|
||||
let cb = &self.clip_boundary;
|
||||
match cb.clip_type {
|
||||
codec::entities::ClipType::Polygonal if cb.vertices.len() >= 3 => {
|
||||
let mut poly: Vec<[f64; 3]> =
|
||||
cb.vertices.iter().map(|v| px_to_world(v.x, ih - v.y)).collect();
|
||||
if let Some(&first) = poly.first() {
|
||||
poly.push(first);
|
||||
}
|
||||
poly
|
||||
// Outline: the clip boundary, or — when it hides its inside — the
|
||||
// frame with the boundary as a hole. Must match the raster's own clip
|
||||
// triangulation in `ImageModel` so outline and pixels align.
|
||||
let pts = match image_clip_polygon(self) {
|
||||
Some(poly) => {
|
||||
let mut ring: Vec<[f64; 3]> = poly.iter().map(|p| px_to_world(p[0], p[1])).collect();
|
||||
ring.push(ring[0]);
|
||||
if self.clip_boundary.clip_mode == codec::entities::ClipMode::Inside {
|
||||
let mut frame = image_wire(corners, false);
|
||||
frame.push([f64::NAN; 3]);
|
||||
frame.extend(ring);
|
||||
frame
|
||||
} else {
|
||||
ring
|
||||
}
|
||||
codec::entities::ClipType::Rectangular if cb.vertices.len() >= 2 => {
|
||||
let v0 = &cb.vertices[0];
|
||||
let v1 = &cb.vertices[1];
|
||||
let (xa, xb) = (v0.x.min(v1.x), v0.x.max(v1.x));
|
||||
let (y0, y1) = (ih - v0.y, ih - v1.y);
|
||||
let (ya, yb) = (y0.min(y1), y0.max(y1));
|
||||
let c0 = px_to_world(xa, ya);
|
||||
let c1 = px_to_world(xb, ya);
|
||||
let c2 = px_to_world(xb, yb);
|
||||
let c3 = px_to_world(xa, yb);
|
||||
vec![c0, c1, c2, c3, c0]
|
||||
}
|
||||
_ => image_wire(corners, !resolvable),
|
||||
}
|
||||
} else {
|
||||
image_wire(corners, !resolvable)
|
||||
None => image_wire(corners, !resolvable),
|
||||
};
|
||||
|
||||
// A raster OCS can display renders its pixels (built separately) inside
|
||||
|
||||
@@ -96,6 +96,7 @@ impl ImageCommand {
|
||||
world_height,
|
||||
);
|
||||
img.flags = codec::entities::ImageDisplayFlags::SHOW_IMAGE
|
||||
| codec::entities::ImageDisplayFlags::SHOW_NOT_ALIGNED
|
||||
| codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
|
||||
self.plane.place_entity(EntityType::RasterImage(img))
|
||||
}
|
||||
|
||||
+129
-17
@@ -1,17 +1,23 @@
|
||||
// PDFCLIP — clip a PDF underlay to a boundary (CLIP continues here for a
|
||||
// PDF underlay).
|
||||
// PDFCLIP / IMAGECLIP — clip a PDF underlay or a raster image to a boundary
|
||||
// (CLIP continues here for either).
|
||||
//
|
||||
// Select PDF to clip:
|
||||
// Select PDF to clip: (Select image to clip:)
|
||||
// Enter PDF clipping option [ON/OFF/Delete/New boundary] <New boundary>:
|
||||
// Delete old boundary? [Yes/No] <Yes>: (when one exists)
|
||||
// (Enter image clipping option [ON/OFF/Delete/New boundary] <New>:)
|
||||
// Delete old boundary? [Yes/No] <Yes>: (when one exists; [No/Yes] for
|
||||
// an image)
|
||||
// Outside mode - Objects outside boundary will be hidden.
|
||||
// Specify clipping boundary or select invert option:
|
||||
// [Select polyline/Polygonal/Rectangular/Invert clip] <Rectangular>:
|
||||
//
|
||||
// The boundary is stored in underlay units; the clip-inside bit follows the
|
||||
// Invert choice.
|
||||
// The boundary is stored in underlay units (image: raster pixels, a
|
||||
// rectangle as its two corners); the clip-inside bit follows the Invert
|
||||
// choice.
|
||||
|
||||
use codec::entities::{Underlay, UnderlayDisplayFlags, UnderlayType};
|
||||
use codec::entities::{
|
||||
ClipBoundary, ClipMode, ImageDisplayFlags, RasterImage, Underlay, UnderlayDisplayFlags,
|
||||
UnderlayType,
|
||||
};
|
||||
use codec::types::{Handle, Vector2};
|
||||
use codec::EntityType;
|
||||
use glam::DVec3;
|
||||
@@ -35,6 +41,9 @@ pub struct PdfClipCommand {
|
||||
step: Step,
|
||||
handle: Handle,
|
||||
underlay: Option<Underlay>,
|
||||
/// IMAGECLIP: the image being clipped (`underlay` stays empty).
|
||||
image: Option<RasterImage>,
|
||||
image_mode: bool,
|
||||
picked: Option<EntityType>,
|
||||
inverted: bool,
|
||||
points: Vec<DVec3>,
|
||||
@@ -50,12 +59,90 @@ impl PdfClipCommand {
|
||||
step: Step::Select,
|
||||
handle: Handle::NULL,
|
||||
underlay: None,
|
||||
image: None,
|
||||
image_mode: false,
|
||||
picked: None,
|
||||
inverted: false,
|
||||
points: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// IMAGECLIP: asks for an image.
|
||||
pub fn image() -> Self {
|
||||
Self { image_mode: true, ..Self::new() }
|
||||
}
|
||||
|
||||
/// An image CLIP picked: straight to the clipping option.
|
||||
pub fn for_image(handle: Handle, image: RasterImage) -> Self {
|
||||
let mut command = Self::image();
|
||||
command.handle = handle;
|
||||
command.image = Some(image);
|
||||
command.step = Step::Option;
|
||||
command
|
||||
}
|
||||
|
||||
fn finish_image(&self, image: RasterImage) -> CmdResult {
|
||||
CmdResult::UpdateEntityAndFinish {
|
||||
handle: self.handle,
|
||||
entity: EntityType::RasterImage(image),
|
||||
}
|
||||
}
|
||||
|
||||
/// Image options: ON / OFF switch only the display flag (the boundary
|
||||
/// stays), Delete drops the boundary back to the whole image, New asks
|
||||
/// to replace a boundary that exists.
|
||||
fn image_option(&mut self, token: &str) -> CmdResult {
|
||||
let Some(mut image) = self.image.clone() else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
match token {
|
||||
"ON" => image.flags |= ImageDisplayFlags::USE_CLIPPING_BOUNDARY,
|
||||
"OFF" => image.flags -= ImageDisplayFlags::USE_CLIPPING_BOUNDARY,
|
||||
"D" | "DELETE" => {
|
||||
let mode = image.clip_boundary.clip_mode;
|
||||
image.clip_boundary = ClipBoundary::full_image(image.size.x, image.size.y);
|
||||
image.clip_boundary.clip_mode = mode;
|
||||
image.clipping_enabled = false;
|
||||
}
|
||||
"" | "N" | "NEW" | "NEW BOUNDARY" if image.clipping_enabled => {
|
||||
self.step = Step::DeleteOld;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
"" | "N" | "NEW" | "NEW BOUNDARY" => return self.enter_mode(),
|
||||
_ => return CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||
}
|
||||
self.finish_image(image)
|
||||
}
|
||||
|
||||
/// A rectangle keeps its two corners (lowest and highest raster
|
||||
/// coordinates); a polygon is closed by repeating its first vertex.
|
||||
fn apply_image_boundary(&self, world: &[DVec3], rectangle: bool) -> CmdResult {
|
||||
let Some(mut image) = self.image.clone() else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let raster: Vec<Vector2> = world
|
||||
.iter()
|
||||
.map(|p| crate::entities::raster_image::image_world_to_clip(&image, p.to_array()))
|
||||
.collect();
|
||||
let mut boundary = if rectangle {
|
||||
let (mut lo, mut hi) = (raster[0], raster[0]);
|
||||
for p in &raster {
|
||||
lo = Vector2::new(lo.x.min(p.x), lo.y.min(p.y));
|
||||
hi = Vector2::new(hi.x.max(p.x), hi.y.max(p.y));
|
||||
}
|
||||
ClipBoundary::rectangular(lo, hi)
|
||||
} else {
|
||||
let mut ring = raster;
|
||||
ring.push(ring[0]);
|
||||
ClipBoundary::polygonal(ring)
|
||||
};
|
||||
boundary.clip_mode = if self.inverted { ClipMode::Inside } else { ClipMode::Outside };
|
||||
image.clip_boundary = boundary;
|
||||
image.clipping_enabled = true;
|
||||
image.flags |= ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
|
||||
self.finish_image(image)
|
||||
}
|
||||
|
||||
/// A new boundary for an underlay already chosen (the underlay tab's
|
||||
/// Create Clipping Boundary): the New boundary branch at once. Returns the
|
||||
/// command and the result of its first step.
|
||||
@@ -99,7 +186,10 @@ impl PdfClipCommand {
|
||||
Vector2::new((dx * c + dy * s) / sx, (-dx * s + dy * c) / sy)
|
||||
}
|
||||
|
||||
fn apply_boundary(&self, world: &[DVec3]) -> CmdResult {
|
||||
fn apply_boundary(&self, world: &[DVec3], rectangle: bool) -> CmdResult {
|
||||
if self.image.is_some() {
|
||||
return self.apply_image_boundary(world, rectangle);
|
||||
}
|
||||
let Some(mut underlay) = self.underlay.clone() else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
@@ -110,6 +200,9 @@ impl PdfClipCommand {
|
||||
}
|
||||
|
||||
fn option(&mut self, text: &str) -> CmdResult {
|
||||
if self.image.is_some() {
|
||||
return self.image_option(&text.trim().to_ascii_uppercase());
|
||||
}
|
||||
let Some(mut underlay) = self.underlay.clone() else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
@@ -174,15 +267,20 @@ impl PdfClipCommand {
|
||||
|
||||
impl CadCommand for PdfClipCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"PDFCLIP"
|
||||
if self.image_mode { "IMAGECLIP" } else { "PDFCLIP" }
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Step::Select if self.image_mode => "Select image to clip:".to_string(),
|
||||
Step::Select => "Select PDF to clip:".to_string(),
|
||||
Step::Option if self.image_mode => {
|
||||
"Enter image clipping option [ON/OFF/Delete/New boundary] <New>:".to_string()
|
||||
}
|
||||
Step::Option => {
|
||||
"Enter PDF clipping option [ON/OFF/Delete/New boundary] <New boundary>:".to_string()
|
||||
}
|
||||
Step::DeleteOld if self.image_mode => "Delete old boundary? [No/Yes] <Yes>:".to_string(),
|
||||
Step::DeleteOld => "Delete old boundary? [Yes/No] <Yes>:".to_string(),
|
||||
Step::Mode => {
|
||||
"[Select polyline/Polygonal/Rectangular/Invert clip] <Rectangular>:".to_string()
|
||||
@@ -206,6 +304,9 @@ impl CadCommand for PdfClipCommand {
|
||||
CmdOption::new("Delete", "D"),
|
||||
CmdOption::new("New boundary", "N"),
|
||||
],
|
||||
Step::DeleteOld if self.image_mode => {
|
||||
vec![CmdOption::new("No", "N"), CmdOption::new("Yes", "Y")]
|
||||
}
|
||||
Step::DeleteOld => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")],
|
||||
Step::Mode => vec![
|
||||
CmdOption::new("Select polyline", "S"),
|
||||
@@ -247,6 +348,16 @@ impl CadCommand for PdfClipCommand {
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
let picked = self.picked.take();
|
||||
match self.step {
|
||||
Step::Select if self.image_mode => match picked {
|
||||
Some(EntityType::RasterImage(image)) => {
|
||||
self.handle = handle;
|
||||
self.image = Some(image);
|
||||
self.step = Step::Option;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
// Anything else: the prompt again, as the reference does.
|
||||
_ => CmdResult::NeedPoint,
|
||||
},
|
||||
Step::Select => match picked {
|
||||
Some(EntityType::Underlay(u)) if u.underlay_type == UnderlayType::Pdf => {
|
||||
self.handle = handle;
|
||||
@@ -264,7 +375,7 @@ impl CadCommand for PdfClipCommand {
|
||||
.iter()
|
||||
.map(|v| DVec3::new(v.location.x, v.location.y, z))
|
||||
.collect();
|
||||
self.apply_boundary(&world)
|
||||
self.apply_boundary(&world, false)
|
||||
}
|
||||
_ => CmdResult::ReportError("Invalid object selected.".to_string()),
|
||||
},
|
||||
@@ -281,7 +392,7 @@ impl CadCommand for PdfClipCommand {
|
||||
}
|
||||
Step::RectSecond => {
|
||||
let first = self.points[0];
|
||||
self.apply_boundary(&Self::rectangle(first, pt))
|
||||
self.apply_boundary(&Self::rectangle(first, pt), true)
|
||||
}
|
||||
Step::PolyPoints => {
|
||||
self.points.push(pt);
|
||||
@@ -296,9 +407,10 @@ impl CadCommand for PdfClipCommand {
|
||||
Step::Option => self.option(text),
|
||||
Step::DeleteOld => match text.trim().to_ascii_uppercase().as_str() {
|
||||
"" | "Y" | "YES" => self.enter_mode(),
|
||||
"N" | "NO" => match self.underlay.clone() {
|
||||
Some(underlay) => self.finish(underlay),
|
||||
None => CmdResult::Cancel,
|
||||
"N" | "NO" => match (self.image.clone(), self.underlay.clone()) {
|
||||
(Some(image), _) => self.finish_image(image),
|
||||
(None, Some(underlay)) => self.finish(underlay),
|
||||
_ => CmdResult::Cancel,
|
||||
},
|
||||
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||
},
|
||||
@@ -310,7 +422,7 @@ impl CadCommand for PdfClipCommand {
|
||||
}
|
||||
"C" | "CLOSE" if self.points.len() >= 3 => {
|
||||
let points = self.points.clone();
|
||||
self.apply_boundary(&points)
|
||||
self.apply_boundary(&points, false)
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
@@ -325,7 +437,7 @@ impl CadCommand for PdfClipCommand {
|
||||
Step::Mode => self.mode(""),
|
||||
Step::PolyPoints if self.points.len() >= 3 => {
|
||||
let points = self.points.clone();
|
||||
self.apply_boundary(&points)
|
||||
self.apply_boundary(&points, false)
|
||||
}
|
||||
Step::PolyPoints => CmdResult::NeedPoint,
|
||||
_ => CmdResult::Cancel,
|
||||
@@ -349,5 +461,5 @@ impl CadCommand for PdfClipCommand {
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["PDFCLIP"]
|
||||
names: &["PDFCLIP", "IMAGECLIP"]
|
||||
});
|
||||
@@ -13,7 +13,8 @@
|
||||
// Specify front clip point or [Distance/Remove]:
|
||||
//
|
||||
// CLIP asks for one object: a block reference continues as XCLIP, a PDF
|
||||
// underlay as PDFCLIP, anything else is "*Invalid selection*".
|
||||
// underlay as PDFCLIP, a raster image as IMAGECLIP, a layout viewport as
|
||||
// VPCLIP, anything else is "*Invalid selection*".
|
||||
|
||||
use codec::entities::UnderlayType;
|
||||
use codec::types::Handle;
|
||||
@@ -22,6 +23,7 @@ use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
use crate::modules::insert::pdf_clip::PdfClipCommand;
|
||||
use crate::modules::layout::mview::MviewCommand;
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
@@ -43,8 +45,9 @@ pub enum XclipAction {
|
||||
Polyline,
|
||||
/// Boundary in WCS (two points = rectangle corners).
|
||||
New { boundary: Vec<[f64; 2]>, inverted: bool },
|
||||
/// Front and back clipping planes; `None` removes one.
|
||||
Depth { front: Option<f64>, back: Option<f64> },
|
||||
/// Front and back clipping planes: `Some(None)` removes one, `None`
|
||||
/// leaves it. `rejected` prints that the front plane was not applied.
|
||||
Depth { front: Option<Option<f64>>, back: Option<Option<f64>>, rejected: bool },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -76,9 +79,12 @@ pub struct XclipCommand {
|
||||
inverted: bool,
|
||||
points: Vec<DVec3>,
|
||||
picked: Option<EntityType>,
|
||||
/// The front plane answered: a distance, or `None` for Remove.
|
||||
front: Option<f64>,
|
||||
}
|
||||
|
||||
pub const DEPTH_REJECTED: &str = "Error: Front clipping plane behind back clipping plane. Clipdepth not applied.";
|
||||
|
||||
impl XclipCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -178,7 +184,19 @@ impl XclipCommand {
|
||||
|
||||
fn depth(&mut self, text: &str) -> CmdResult {
|
||||
match (self.step, text.trim().to_ascii_uppercase().as_str()) {
|
||||
(_, "R" | "REMOVE") => self.act(XclipAction::Depth { front: None, back: None }),
|
||||
// Remove clears only the plane being asked; the back one is still asked.
|
||||
(Step::Front, "R" | "REMOVE") => {
|
||||
self.front = None;
|
||||
self.step = Step::Back;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
// Back Remove after a front distance: the back plane goes, the
|
||||
// front is not applied.
|
||||
(Step::Back, "R" | "REMOVE") => self.act(XclipAction::Depth {
|
||||
front: self.front.is_none().then_some(None),
|
||||
back: Some(None),
|
||||
rejected: self.front.is_some(),
|
||||
}),
|
||||
(Step::Front, "D" | "DISTANCE") => {
|
||||
self.step = Step::FrontDistance;
|
||||
CmdResult::NeedPoint
|
||||
@@ -202,7 +220,13 @@ impl XclipCommand {
|
||||
self.step = Step::Back;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => self.act(XclipAction::Depth { front: self.front, back: Some(d) }),
|
||||
// A front plane behind the back one changes nothing.
|
||||
_ if self.front.is_some_and(|front| front < d) => self.act(XclipAction::Depth {
|
||||
front: None,
|
||||
back: None,
|
||||
rejected: true,
|
||||
}),
|
||||
_ => self.act(XclipAction::Depth { front: Some(self.front), back: Some(Some(d)), rejected: false }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,6 +482,16 @@ impl CadCommand for ClipCommand {
|
||||
self.inner = Some(Box::new(PdfClipCommand::for_underlay(handle, u)));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Some(EntityType::RasterImage(image)) => {
|
||||
self.inner = Some(Box::new(PdfClipCommand::for_image(handle, image)));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
// The layout's own paper-space viewport (id 1) is not clippable.
|
||||
Some(EntityType::Viewport(vp)) if vp.id != 1 => {
|
||||
let clipped = !vp.clip_boundary_handle.is_null();
|
||||
self.inner = Some(Box::new(MviewCommand::vpclip(handle, clipped)));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::ReportError("*Invalid selection*".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
+171
-5
@@ -1,4 +1,16 @@
|
||||
// MVIEW — interactive paper-space viewport creation.
|
||||
//
|
||||
// VPCLIP (and CLIP on a layout viewport) reuses the polygon and object
|
||||
// steps to clip an existing viewport:
|
||||
// Select viewport to clip:
|
||||
// Select clipping object or [Polygonal] <Polygonal>: (Delete also taken
|
||||
// when clipped)
|
||||
// Specify start point:
|
||||
// Specify next point or [Arc/Length/Undo]:
|
||||
// Specify next point or [Arc/Close/Length/Undo]:
|
||||
// Enter an arc boundary option
|
||||
// [Angle/CEnter/CLose/Direction/Line/Radius/Second pt/Undo/Endpoint of arc] <Endpoint>:
|
||||
// Specify length of line:
|
||||
|
||||
use codec::entities::{LwPolyline, LwVertex, Viewport};
|
||||
use codec::tables::View;
|
||||
@@ -37,6 +49,12 @@ enum Step {
|
||||
DefineNewFirst,
|
||||
DefineNewSecond,
|
||||
PlaceView,
|
||||
/// VPCLIP: the viewport to clip.
|
||||
ClipSelect,
|
||||
/// VPCLIP: a clipping object, or Polygonal / Delete.
|
||||
ClipChoice,
|
||||
/// VPCLIP Length: a line of that length along the last direction.
|
||||
ClipLength,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
@@ -56,6 +74,10 @@ pub struct MviewCommand {
|
||||
views: Vec<View>,
|
||||
paper_bounds: ((f64, f64), (f64, f64)),
|
||||
original_layout: String,
|
||||
/// VPCLIP: the viewport being clipped (NULL until chosen) and whether it
|
||||
/// already has a boundary.
|
||||
clip: Option<(Handle, bool)>,
|
||||
picked: Option<EntityType>,
|
||||
}
|
||||
|
||||
impl MviewCommand {
|
||||
@@ -75,6 +97,77 @@ impl MviewCommand {
|
||||
views,
|
||||
paper_bounds,
|
||||
original_layout,
|
||||
clip: None,
|
||||
picked: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// VPCLIP asking for the viewport.
|
||||
pub fn vpclip_select() -> Self {
|
||||
let mut command = Self::new(String::new(), ((0.0, 0.0), (0.0, 0.0)), Vec::new());
|
||||
command.clip = Some((Handle::NULL, false));
|
||||
command.step = Step::ClipSelect;
|
||||
command
|
||||
}
|
||||
|
||||
/// VPCLIP on a chosen viewport.
|
||||
pub fn vpclip(viewport: Handle, clipped: bool) -> Self {
|
||||
let mut command = Self::vpclip_select();
|
||||
command.clip = Some((viewport, clipped));
|
||||
command.step = Step::ClipChoice;
|
||||
command
|
||||
}
|
||||
|
||||
fn clip_target(&self) -> Handle {
|
||||
self.clip.map_or(Handle::NULL, |(handle, _)| handle)
|
||||
}
|
||||
|
||||
fn clip_text(&mut self, upper: &str) -> Option<CmdResult> {
|
||||
match self.step {
|
||||
Step::ClipChoice => match upper {
|
||||
"" | "P" | "POLYGONAL" => {
|
||||
self.step = Step::Polygon;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"D" | "DELETE" if self.clip.is_some_and(|(_, clipped)| clipped) => {
|
||||
Some(CmdResult::MviewCreateClipped {
|
||||
boundary: None,
|
||||
boundary_handle: Handle::NULL,
|
||||
target: self.clip_target(),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Step::ClipLength => {
|
||||
let length = upper.parse::<f64>().ok()?;
|
||||
let last = *self.polygon.last()?;
|
||||
let direction = self.polygon_last_tangent.map_or(DVec2::X, |t| t.as_dvec2());
|
||||
self.step = Step::Polygon;
|
||||
Some(self.on_point(last + DVec3::new(direction.x, direction.y, 0.0) * length))
|
||||
}
|
||||
Step::Polygon if self.polygon_mode == PolygonMode::Arc => match upper {
|
||||
"CL" | "CLOSE" if self.polygon.len() >= 2 => Some(self.finish_polygon()),
|
||||
"L" | "LINE" => {
|
||||
self.polygon_mode = PolygonMode::Line;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"U" | "UNDO" if !self.polygon.is_empty() => Some(self.undo_polygon()),
|
||||
_ => None,
|
||||
},
|
||||
Step::Polygon => match upper {
|
||||
"A" | "ARC" if !self.polygon.is_empty() => {
|
||||
self.polygon_mode = PolygonMode::Arc;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"C" | "CLOSE" if self.polygon.len() >= 3 => Some(self.finish_polygon()),
|
||||
"L" | "LENGTH" if !self.polygon.is_empty() => {
|
||||
self.step = Step::ClipLength;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"U" | "UNDO" if !self.polygon.is_empty() => Some(self.undo_polygon()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +251,7 @@ impl MviewCommand {
|
||||
Some(boundary) => CmdResult::MviewCreateClipped {
|
||||
boundary: Some(boundary),
|
||||
boundary_handle: Handle::NULL,
|
||||
target: self.clip_target(),
|
||||
},
|
||||
None => CmdResult::Cancel,
|
||||
}
|
||||
@@ -263,10 +357,24 @@ impl MviewCommand {
|
||||
|
||||
impl CadCommand for MviewCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"MVIEW"
|
||||
if self.clip.is_some() { "VPCLIP" } else { "MVIEW" }
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.clip.is_some() {
|
||||
return match self.step {
|
||||
Step::ClipSelect => "Select viewport to clip:",
|
||||
Step::ClipChoice => "Select clipping object or [Polygonal] <Polygonal>:",
|
||||
Step::ClipLength => "Specify length of line:",
|
||||
Step::Polygon if self.polygon.is_empty() => "Specify start point:",
|
||||
Step::Polygon if self.polygon_mode == PolygonMode::Arc => {
|
||||
"Enter an arc boundary option\n[Angle/CEnter/CLose/Direction/Line/Radius/Second pt/Undo/Endpoint of arc] <Endpoint>:"
|
||||
}
|
||||
Step::Polygon if self.polygon.len() < 3 => "Specify next point or [Arc/Length/Undo]:",
|
||||
_ => "Specify next point or [Arc/Close/Length/Undo]:",
|
||||
}
|
||||
.to_string();
|
||||
}
|
||||
match self.step {
|
||||
Step::RectangleFirst => t!(
|
||||
"MVIEW Specify corner of viewport or [Polygonal/Object/Fit/Insert view]:"
|
||||
@@ -302,10 +410,31 @@ impl CadCommand for MviewCommand {
|
||||
t!("MVIEW New view Specify opposite model-space corner:").into_owned()
|
||||
}
|
||||
Step::PlaceView => t!("MVIEW Insert view Specify placement point:").into_owned(),
|
||||
Step::ClipSelect | Step::ClipChoice | Step::ClipLength => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.clip.is_some() {
|
||||
return match self.step {
|
||||
Step::ClipChoice => vec![CmdOption::new("Polygonal", "P")],
|
||||
Step::Polygon if self.polygon_mode == PolygonMode::Arc => vec![
|
||||
CmdOption::new("CLose", "CL"),
|
||||
CmdOption::new("Line", "L"),
|
||||
CmdOption::new("Undo", "U"),
|
||||
],
|
||||
Step::Polygon if !self.polygon.is_empty() => {
|
||||
let mut options = vec![CmdOption::new("Arc", "A")];
|
||||
if self.polygon.len() >= 3 {
|
||||
options.push(CmdOption::new("Close", "C"));
|
||||
}
|
||||
options.push(CmdOption::new("Length", "L"));
|
||||
options.push(CmdOption::new("Undo", "U"));
|
||||
options
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
}
|
||||
match self.step {
|
||||
Step::RectangleFirst => vec![
|
||||
CmdOption::new(t!("Polygonal").as_ref(), "POLYGONAL"),
|
||||
@@ -427,12 +556,19 @@ impl CadCommand for MviewCommand {
|
||||
},
|
||||
None => CmdResult::Cancel,
|
||||
},
|
||||
Step::Object | Step::ChooseView => CmdResult::NeedPoint,
|
||||
Step::Object | Step::ChooseView | Step::ClipSelect | Step::ClipChoice => {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::ClipLength => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::ClipChoice => {
|
||||
self.step = Step::Polygon;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Polygon if self.polygon.len() >= 3 => self.finish_polygon(),
|
||||
Step::DefineNewFirst | Step::DefineNewSecond => {
|
||||
CmdResult::MviewCancelToLayout(self.original_layout.clone())
|
||||
@@ -451,23 +587,45 @@ impl CadCommand for MviewCommand {
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.step == Step::Object
|
||||
matches!(self.step, Step::Object | Step::ClipSelect | Step::ClipChoice)
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
self.clip.is_some()
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.step == Step::ClipSelect {
|
||||
return match self.picked.take() {
|
||||
Some(EntityType::Viewport(vp)) if vp.id != 1 => {
|
||||
self.clip = Some((handle, !vp.clip_boundary_handle.is_null()));
|
||||
self.step = Step::ClipChoice;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::ReportError("Object selected was not a viewport\n.".to_string()),
|
||||
};
|
||||
}
|
||||
CmdResult::MviewCreateClipped {
|
||||
boundary: None,
|
||||
boundary_handle: handle,
|
||||
target: self.clip_target(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.step == Step::ChooseView {
|
||||
if self.step == Step::ClipLength {
|
||||
InputKind::FreeText
|
||||
} else if self.step == Step::ChooseView {
|
||||
InputKind::FreeText
|
||||
} else if self.step == Step::RectangleFirst
|
||||
|| self.step == Step::ClipChoice
|
||||
|| (self.step == Step::Polygon && !self.polygon.is_empty())
|
||||
{
|
||||
InputKind::SingleToken
|
||||
@@ -478,6 +636,7 @@ impl CadCommand for MviewCommand {
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.step == Step::RectangleFirst
|
||||
|| self.step == Step::ClipChoice
|
||||
|| (self.step == Step::Polygon && !self.polygon.is_empty())
|
||||
}
|
||||
|
||||
@@ -492,6 +651,9 @@ impl CadCommand for MviewCommand {
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim();
|
||||
let upper = keyword.to_ascii_uppercase();
|
||||
if self.clip.is_some() {
|
||||
return self.clip_text(&upper);
|
||||
}
|
||||
match self.step {
|
||||
Step::RectangleFirst => match upper.as_str() {
|
||||
"P" | "POLYGONAL" => {
|
||||
@@ -541,6 +703,10 @@ impl CadCommand for MviewCommand {
|
||||
}
|
||||
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
if self.clip.is_some() && self.step == Step::ClipLength {
|
||||
self.step = Step::Polygon;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if self.step == Step::Polygon && !self.polygon.is_empty() {
|
||||
Some(self.undo_polygon())
|
||||
} else {
|
||||
@@ -580,4 +746,4 @@ impl CadCommand for MviewCommand {
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["MVIEW"] }); // MviewCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["MVIEW", "VPCLIP"] }); // MviewCommand
|
||||
@@ -33,10 +33,10 @@ fn quad_verts(corners: &[[f32; 3]; 4], corners_low: &[[f32; 3]; 4]) -> Vec<Image
|
||||
|
||||
/// Visible-region triangles in image PIXEL space (flat, groups of 3): the whole
|
||||
/// image rectangle when unclipped, else the clip rectangle or the triangulated
|
||||
/// clip polygon. An inverted (show-outside) boundary isn't a simple filled
|
||||
/// region, so it falls back to the whole image rather than mis-clip.
|
||||
/// clip polygon, or the image with the polygon cut out when the boundary
|
||||
/// hides its inside.
|
||||
fn clip_triangles_px(img: &codec::entities::RasterImage) -> Vec<[f64; 2]> {
|
||||
use codec::entities::{ClipMode, ClipType};
|
||||
use codec::entities::ClipMode;
|
||||
let w = img.size.x;
|
||||
let h = img.size.y;
|
||||
let quad = || {
|
||||
@@ -45,34 +45,16 @@ fn clip_triangles_px(img: &codec::entities::RasterImage) -> Vec<[f64; 2]> {
|
||||
[0.0, 0.0], [w, h], [0.0, h],
|
||||
]
|
||||
};
|
||||
if !img.clipping_enabled {
|
||||
let Some(poly) = crate::entities::raster_image::image_clip_polygon(img) else {
|
||||
return quad();
|
||||
}
|
||||
let cb = &img.clip_boundary;
|
||||
if cb.clip_mode == ClipMode::Inside {
|
||||
return quad();
|
||||
}
|
||||
// Clip-boundary Y is in image raster space (row 0 = top, Y increasing
|
||||
// downward), whereas this pixel space matches the image's v-vector (Y up
|
||||
// from the insertion corner). Flip each vertex's Y (`h - y`) so the clip
|
||||
// lands where AutoCAD draws it and samples the matching texels.
|
||||
let tris: Vec<[f64; 2]> = match cb.clip_type {
|
||||
ClipType::Rectangular if cb.vertices.len() >= 2 => {
|
||||
let (v0, v1) = (cb.vertices[0], cb.vertices[1]);
|
||||
let (xa, xb) = (v0.x.min(v1.x), v0.x.max(v1.x));
|
||||
let (y0, y1) = (h - v0.y, h - v1.y);
|
||||
let (ya, yb) = (y0.min(y1), y0.max(y1));
|
||||
vec![[xa, ya], [xb, ya], [xb, yb], [xa, ya], [xb, yb], [xa, yb]]
|
||||
}
|
||||
ClipType::Polygonal if cb.vertices.len() >= 3 => {
|
||||
let poly: Vec<[f64; 3]> = cb.vertices.iter().map(|v| [v.x, h - v.y, 0.0]).collect();
|
||||
crate::entities::mesh::triangulate_planar(&poly)
|
||||
.into_iter()
|
||||
.map(|p| [p[0], p[1]])
|
||||
.collect()
|
||||
}
|
||||
_ => quad(),
|
||||
};
|
||||
// Inside mode shows the image with the boundary cut out.
|
||||
let (points, triangles) = if img.clip_boundary.clip_mode == ClipMode::Inside {
|
||||
kernel::geom2d::triangulate(&[[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]], &[poly])
|
||||
} else {
|
||||
kernel::geom2d::triangulate(&poly, &[])
|
||||
};
|
||||
let tris: Vec<[f64; 2]> = triangles.into_iter().flat_map(|t| t.map(|i| points[i])).collect();
|
||||
if tris.is_empty() {
|
||||
quad()
|
||||
} else {
|
||||
|
||||
+12
-3
@@ -169,13 +169,22 @@ pub fn set_insert_clip_enabled(doc: &mut CadDocument, insert: Handle, on: bool)
|
||||
}
|
||||
|
||||
/// Front and back clipping planes (None = off) of an insert's clip.
|
||||
pub fn set_insert_clip_depth(doc: &mut CadDocument, insert: Handle, front: Option<f64>, back: Option<f64>) -> bool {
|
||||
pub fn set_insert_clip_depth(
|
||||
doc: &mut CadDocument,
|
||||
insert: Handle,
|
||||
front: Option<Option<f64>>,
|
||||
back: Option<Option<f64>>,
|
||||
) -> bool {
|
||||
let Some(spatial) = filter_handle(doc, insert) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(ObjectType::SpatialFilter(f)) = doc.objects.get_mut(&spatial) {
|
||||
f.front_clip = front;
|
||||
f.back_clip = back;
|
||||
if let Some(front) = front {
|
||||
f.front_clip = front;
|
||||
}
|
||||
if let Some(back) = back {
|
||||
f.back_clip = back;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user