Complete wipeout workflow and editing
This commit is contained in:
parent
76ad642d0c
commit
b7819481f0
14 changed files with 513 additions and 339 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0975677#0975677029f2b472759db00e9692421a5831ad00"
|
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=6b6b50a#6b6b50ad56ca6184823769db6769cc15f5cbe50d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=3d5de36#3d5de36cb5b391f17da924c5f80e34a0cdc1d948"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d674c55#d674c550cafda45dde9c96c9e13fc3fa632679d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6b6b50a", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "3d5de36", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d674c55", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", optional = true, features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6b6b50a", optional = true, features = ["serde"] }
|
||||||
|
|
||||||
# Runtime IPC and serialization (host feature only).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
|
|
@ -37,7 +37,7 @@ serde_json = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# acadrust is scanned at build time to generate the embedded type registry.
|
# acadrust is scanned at build time to generate the embedded type registry.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6b6b50a", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ publish = false
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6b6b50a", features = ["serde"] }
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|
|
||||||
|
|
@ -1014,7 +1014,10 @@ impl OpenCADStudio {
|
||||||
self.commit_undo_delta(i, pending);
|
self.commit_undo_delta(i, pending);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CmdResult::WipeoutFromPolyline(handle) => {
|
CmdResult::WipeoutFromPolyline {
|
||||||
|
handle,
|
||||||
|
erase_source,
|
||||||
|
} => {
|
||||||
let wipeout = {
|
let wipeout = {
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
scene
|
scene
|
||||||
|
|
@ -1026,16 +1029,23 @@ impl OpenCADStudio {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if let Some(wipeout) = wipeout {
|
if let Some(wipeout) = wipeout {
|
||||||
|
if erase_source {
|
||||||
|
return self.apply_cmd_result(CmdResult::ReplaceMany(
|
||||||
|
vec![(handle, Vec::new())],
|
||||||
|
vec![wipeout],
|
||||||
|
));
|
||||||
|
}
|
||||||
return self.apply_cmd_result(CmdResult::CommitAndExit(wipeout));
|
return self.apply_cmd_result(CmdResult::CommitAndExit(wipeout));
|
||||||
}
|
}
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
"WIPEOUT Polyline: select a closed planar polyline with at least 3 vertices.",
|
"WIPEOUT Polyline: select a straight, closed, planar 2D polyline with at least 3 non-intersecting vertices.",
|
||||||
);
|
);
|
||||||
if let Some(prompt) =
|
let command =
|
||||||
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
crate::modules::draw::draw::wipeout::WipeoutCommand::new_polyline();
|
||||||
{
|
self.command_line.push_info(
|
||||||
self.command_line.push_info(&prompt);
|
&crate::command::CadCommand::prompt(&command),
|
||||||
}
|
);
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
}
|
}
|
||||||
CmdResult::MviewSwitchLayout(layout) => {
|
CmdResult::MviewSwitchLayout(layout) => {
|
||||||
let task = self.on_layout_switch_preserving_command(layout);
|
let task = self.on_layout_switch_preserving_command(layout);
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,11 @@ impl OpenCADStudio {
|
||||||
let wo_cmd = match args.as_str() {
|
let wo_cmd = match args.as_str() {
|
||||||
"P" | "POLYLINE" => WipeoutCommand::new_polyline(),
|
"P" | "POLYLINE" => WipeoutCommand::new_polyline(),
|
||||||
"R" | "RECTANGULAR" => WipeoutCommand::new_rectangular(),
|
"R" | "RECTANGULAR" => WipeoutCommand::new_rectangular(),
|
||||||
_ => WipeoutCommand::new_polygonal(),
|
_ => WipeoutCommand::new_polygonal(
|
||||||
|
crate::modules::draw::draw::wipeout::wipeout_frame_mode(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
self.command_line.push_info(&wo_cmd.prompt());
|
self.command_line.push_info(&wo_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(wo_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(wo_cmd));
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,10 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||||
"TEXTQLTY",
|
"TEXTQLTY",
|
||||||
"SORTENTS",
|
"SORTENTS",
|
||||||
"XCLIPFRAME",
|
"XCLIPFRAME",
|
||||||
|
"WIPEOUTFRAME",
|
||||||
|
"FRAMES0",
|
||||||
|
"FRAMES1",
|
||||||
|
"FRAMES2",
|
||||||
"HALOGAP",
|
"HALOGAP",
|
||||||
"TRACEWID",
|
"TRACEWID",
|
||||||
"SKETCHINC",
|
"SKETCHINC",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ use super::*;
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
match cmd {
|
match cmd {
|
||||||
|
"FRAMES0" => return self.dispatch_styleprops("SETVAR WIPEOUTFRAME 0", i),
|
||||||
|
"FRAMES1" => return self.dispatch_styleprops("SETVAR WIPEOUTFRAME 1", i),
|
||||||
|
"FRAMES2" => return self.dispatch_styleprops("SETVAR WIPEOUTFRAME 2", i),
|
||||||
// COLOR <ByLayer|ByBlock|1-255|name> — the colour applied to new
|
// COLOR <ByLayer|ByBlock|1-255|name> — the colour applied to new
|
||||||
// objects (CECOLOR). Bare COLOR reports the current value.
|
// objects (CECOLOR). Bare COLOR reports the current value.
|
||||||
"COLOR" | "COLOUR" | "CECOLOR" | "DDCOLOR" => {
|
"COLOR" | "COLOUR" | "CECOLOR" | "DDCOLOR" => {
|
||||||
|
|
@ -920,6 +923,7 @@ impl OpenCADStudio {
|
||||||
| "TEXTQLTY"
|
| "TEXTQLTY"
|
||||||
| "SORTENTS"
|
| "SORTENTS"
|
||||||
| "XCLIPFRAME"
|
| "XCLIPFRAME"
|
||||||
|
| "WIPEOUTFRAME"
|
||||||
| "HALOGAP"
|
| "HALOGAP"
|
||||||
| "TRACEWID"
|
| "TRACEWID"
|
||||||
| "SKETCHINC"
|
| "SKETCHINC"
|
||||||
|
|
@ -948,6 +952,48 @@ impl OpenCADStudio {
|
||||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
if name == "WIPEOUTFRAME" {
|
||||||
|
match &value {
|
||||||
|
Some(value) => match value.parse::<i16>() {
|
||||||
|
Ok(mode @ 0..=2) => {
|
||||||
|
self.push_undo_snapshot(i, "WIPEOUTFRAME");
|
||||||
|
crate::modules::draw::draw::wipeout::set_wipeout_frame_mode(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
mode,
|
||||||
|
);
|
||||||
|
let changes: Vec<_> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| {
|
||||||
|
matches!(entity, acadrust::EntityType::Wipeout(_))
|
||||||
|
.then_some((
|
||||||
|
entity.common().handle,
|
||||||
|
crate::scene::ChangeKind::Modified,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line
|
||||||
|
.push_output(&format!("WIPEOUTFRAME = {mode}"));
|
||||||
|
}
|
||||||
|
_ => self.command_line.push_error(
|
||||||
|
"SETVAR: WIPEOUTFRAME requires 0, 1, or 2.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let mode = crate::modules::draw::draw::wipeout::wipeout_frame_mode(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
);
|
||||||
|
self.command_line.push_output(crate::tf!(
|
||||||
|
"Enter new value for WIPEOUTFRAME <{mode}>:"
|
||||||
|
).as_ref());
|
||||||
|
self.pending_setvar = Some(name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(self.finish_dispatch(cmd));
|
||||||
|
}
|
||||||
// Parse a boolean given as 0/1 or ON/OFF.
|
// Parse a boolean given as 0/1 or ON/OFF.
|
||||||
let parse_bool = |s: &str| match s.to_uppercase().as_str() {
|
let parse_bool = |s: &str| match s.to_uppercase().as_str() {
|
||||||
"1" | "ON" | "TRUE" => Some(true),
|
"1" | "ON" | "TRUE" => Some(true),
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,21 @@ fn plot_scene_content(
|
||||||
Vec<crate::scene::model::hatch_model::HatchModel>,
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
||||||
crate::io::pdf_export::PlotGroupSplits,
|
crate::io::pdf_export::PlotGroupSplits,
|
||||||
) {
|
) {
|
||||||
let (paper_wires, model_wires) = scene.plot_wire_groups(render_mode_override);
|
let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override);
|
||||||
|
let hide_mask_frames =
|
||||||
|
crate::modules::draw::draw::wipeout::wipeout_frame_mode(&scene.document) == 2;
|
||||||
|
if hide_mask_frames {
|
||||||
|
let mask_handles: std::collections::HashSet<String> = scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| {
|
||||||
|
matches!(entity, acadrust::EntityType::Wipeout(_))
|
||||||
|
.then_some(entity.common().handle.value().to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
paper_wires.retain(|wire| !mask_handles.contains(&wire.name));
|
||||||
|
model_wires.retain(|wire| !mask_handles.contains(&wire.name));
|
||||||
|
}
|
||||||
let paper_hatches = scene.paper_canvas_hatches().as_ref().clone();
|
let paper_hatches = scene.paper_canvas_hatches().as_ref().clone();
|
||||||
let paper_wipeouts = scene.paper_canvas_wipeouts().as_ref().clone();
|
let paper_wipeouts = scene.paper_canvas_wipeouts().as_ref().clone();
|
||||||
if scene.current_layout == "Model" {
|
if scene.current_layout == "Model" {
|
||||||
|
|
@ -184,8 +198,19 @@ fn plot_scene_content(
|
||||||
splits,
|
splits,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let (model_pattern_wires, model_hatches, model_wipeouts) =
|
let (mut model_pattern_wires, model_hatches, model_wipeouts) =
|
||||||
scene.viewport_plot_fills();
|
scene.viewport_plot_fills();
|
||||||
|
if hide_mask_frames {
|
||||||
|
let mask_handles: std::collections::HashSet<String> = scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| {
|
||||||
|
matches!(entity, acadrust::EntityType::Wipeout(_))
|
||||||
|
.then_some(entity.common().handle.value().to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
model_pattern_wires.retain(|wire| !mask_handles.contains(&wire.name));
|
||||||
|
}
|
||||||
|
|
||||||
let (wires, hatches, wipeouts, splits) = if paper_space_last {
|
let (wires, hatches, wipeouts, splits) = if paper_space_last {
|
||||||
let splits = crate::io::pdf_export::PlotGroupSplits {
|
let splits = crate::io::pdf_export::PlotGroupSplits {
|
||||||
|
|
|
||||||
|
|
@ -1297,7 +1297,11 @@ pub enum CmdResult {
|
||||||
boundary_handle: Handle,
|
boundary_handle: Handle,
|
||||||
},
|
},
|
||||||
/// Create a wipeout from an existing closed polyline in the active space.
|
/// Create a wipeout from an existing closed polyline in the active space.
|
||||||
WipeoutFromPolyline(Handle),
|
/// `erase_source` controls whether the source boundary is consumed.
|
||||||
|
WipeoutFromPolyline {
|
||||||
|
handle: Handle,
|
||||||
|
erase_source: bool,
|
||||||
|
},
|
||||||
/// Temporarily switch between paper and Model while MVIEW defines a new
|
/// Temporarily switch between paper and Model while MVIEW defines a new
|
||||||
/// model-space window, keeping the command active.
|
/// model-space window, keeping the command active.
|
||||||
MviewSwitchLayout(String),
|
MviewSwitchLayout(String),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::entities::text_support::{resolve_text_style, text_local_bounds};
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||||
use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject};
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||||
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
use crate::scene::text::lff;
|
use crate::scene::text::lff;
|
||||||
|
|
||||||
// ── Shared geometry helpers ───────────────────────────────────────────────────
|
// ── Shared geometry helpers ───────────────────────────────────────────────────
|
||||||
|
|
@ -416,62 +417,93 @@ impl Transformable for RasterImage {
|
||||||
|
|
||||||
// ── Wipeout ───────────────────────────────────────────────────────────────────
|
// ── Wipeout ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
impl RenderConvertible for Wipeout {
|
fn wipeout_is_polygonal(wipeout: &Wipeout) -> bool {
|
||||||
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
|
wipeout.clipping_enabled
|
||||||
let corners = image_corners(
|
&& wipeout.clip_boundary_vertices.len() >= 3
|
||||||
&self.insertion_point,
|
|
||||||
&self.u_vector,
|
|
||||||
&self.v_vector,
|
|
||||||
self.size.x,
|
|
||||||
self.size.y,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If clipping is enabled and there's a polygon boundary, show that.
|
|
||||||
let pts = if self.clipping_enabled
|
|
||||||
&& self.clip_boundary_vertices.len() >= 3
|
|
||||||
&& matches!(
|
&& matches!(
|
||||||
self.clip_type,
|
wipeout.clip_type,
|
||||||
acadrust::entities::WipeoutClipType::Polygonal
|
acadrust::entities::WipeoutClipType::Polygonal
|
||||||
) {
|
)
|
||||||
// Clip vertices are stored in image-pixel space, centred on the
|
}
|
||||||
// image (range ±size/2). The image's bottom-left corner sits at
|
|
||||||
// `insertion_point`, the image-Y axis points DOWN (per DXF
|
fn wipeout_clip_to_world(wipeout: &Wipeout, point: &acadrust::types::Vector2) -> [f64; 3] {
|
||||||
// "v_vector points down the image"), so map:
|
let x = point.x + wipeout.size.x * 0.5;
|
||||||
// x_off = (clip.x + size.x/2) × u_vector
|
let y = wipeout.size.y * 0.5 - point.y;
|
||||||
// y_off = (size.y/2 − clip.y) × v_vector ← y flipped
|
[
|
||||||
let ox = self.insertion_point.x;
|
wipeout.insertion_point.x + wipeout.u_vector.x * x + wipeout.v_vector.x * y,
|
||||||
let oy = self.insertion_point.y;
|
wipeout.insertion_point.y + wipeout.u_vector.y * x + wipeout.v_vector.y * y,
|
||||||
let oz = self.insertion_point.z;
|
wipeout.insertion_point.z + wipeout.u_vector.z * x + wipeout.v_vector.z * y,
|
||||||
let mut poly: Vec<[f64; 3]> = self
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wipeout_boundary(wipeout: &Wipeout) -> Vec<[f64; 3]> {
|
||||||
|
if wipeout_is_polygonal(wipeout) {
|
||||||
|
wipeout
|
||||||
.clip_boundary_vertices
|
.clip_boundary_vertices
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| {
|
.map(|point| wipeout_clip_to_world(wipeout, point))
|
||||||
let cx = v.x + self.size.x * 0.5;
|
.collect()
|
||||||
let cy = self.size.y * 0.5 - v.y;
|
|
||||||
let wx = self.u_vector.x * cx + self.v_vector.x * cy;
|
|
||||||
let wy = self.u_vector.y * cx + self.v_vector.y * cy;
|
|
||||||
let wz = self.u_vector.z * cx + self.v_vector.z * cy;
|
|
||||||
[ox + wx, oy + wy, oz + wz]
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
// Close the polygon.
|
|
||||||
if let Some(&first) = poly.first() {
|
|
||||||
poly.push(first);
|
|
||||||
}
|
|
||||||
poly
|
|
||||||
} else {
|
} else {
|
||||||
// Rectangular boundary — just the border, no diagonals (mask area).
|
image_corners(
|
||||||
image_wire(corners, false)
|
&wipeout.insertion_point,
|
||||||
};
|
&wipeout.u_vector,
|
||||||
|
&wipeout.v_vector,
|
||||||
|
wipeout.size.x,
|
||||||
|
wipeout.size.y,
|
||||||
|
)
|
||||||
|
.to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wipeout_world_to_clip(wipeout: &Wipeout, world: [f64; 3]) -> Option<acadrust::types::Vector2> {
|
||||||
|
let u = glam::DVec3::new(
|
||||||
|
wipeout.u_vector.x,
|
||||||
|
wipeout.u_vector.y,
|
||||||
|
wipeout.u_vector.z,
|
||||||
|
);
|
||||||
|
let v = glam::DVec3::new(
|
||||||
|
wipeout.v_vector.x,
|
||||||
|
wipeout.v_vector.y,
|
||||||
|
wipeout.v_vector.z,
|
||||||
|
);
|
||||||
|
let delta = glam::DVec3::from(world)
|
||||||
|
- glam::DVec3::new(
|
||||||
|
wipeout.insertion_point.x,
|
||||||
|
wipeout.insertion_point.y,
|
||||||
|
wipeout.insertion_point.z,
|
||||||
|
);
|
||||||
|
let uu = u.dot(u);
|
||||||
|
let uv = u.dot(v);
|
||||||
|
let vv = v.dot(v);
|
||||||
|
let det = uu * vv - uv * uv;
|
||||||
|
if det.abs() <= 1e-18 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let x = (delta.dot(u) * vv - delta.dot(v) * uv) / det;
|
||||||
|
let y = (delta.dot(v) * uu - delta.dot(u) * uv) / det;
|
||||||
|
Some(acadrust::types::Vector2::new(
|
||||||
|
x - wipeout.size.x * 0.5,
|
||||||
|
wipeout.size.y * 0.5 - y,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderConvertible for Wipeout {
|
||||||
|
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
|
||||||
|
let boundary = wipeout_boundary(self);
|
||||||
|
let mut pts = boundary.clone();
|
||||||
|
if let Some(&first) = pts.first() {
|
||||||
|
pts.push(first);
|
||||||
|
}
|
||||||
|
|
||||||
Some(RenderEntity {
|
Some(RenderEntity {
|
||||||
// Interior pick surface — a wipeout reads as a solid patch, so a
|
pick_tris: crate::entities::mesh::triangulate_planar(&boundary),
|
||||||
// click anywhere on it should select it.
|
|
||||||
pick_tris: crate::entities::common::quad_pick_tris(&corners),
|
|
||||||
object: RenderObject::Lines(pts),
|
object: RenderObject::Lines(pts),
|
||||||
snap_pts: vec![],
|
snap_pts: boundary
|
||||||
|
.iter()
|
||||||
|
.map(|point| (glam::DVec3::from(*point), SnapHint::Endpoint))
|
||||||
|
.collect(),
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
key_vertices: corners.to_vec(),
|
key_vertices: boundary,
|
||||||
fill_tris: vec![],
|
fill_tris: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -479,33 +511,15 @@ impl RenderConvertible for Wipeout {
|
||||||
|
|
||||||
impl Grippable for Wipeout {
|
impl Grippable for Wipeout {
|
||||||
fn grips(&self) -> Vec<GripDef> {
|
fn grips(&self) -> Vec<GripDef> {
|
||||||
// If polygonal clipping is active, expose individual polygon vertices as grips.
|
if wipeout_is_polygonal(self) {
|
||||||
let is_polygon = self.clipping_enabled
|
wipeout_boundary(self)
|
||||||
&& self.clip_boundary_vertices.len() >= 3
|
.into_iter()
|
||||||
&& matches!(
|
|
||||||
self.clip_type,
|
|
||||||
acadrust::entities::WipeoutClipType::Polygonal
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_polygon {
|
|
||||||
let ox = self.insertion_point.x;
|
|
||||||
let oy = self.insertion_point.y;
|
|
||||||
let oz = self.insertion_point.z;
|
|
||||||
// Same image-pixel-space → WCS mapping as `to_render` so grips
|
|
||||||
// sit exactly on the rendered polygon vertices.
|
|
||||||
self.clip_boundary_vertices
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, v)| {
|
.map(|(i, point)| {
|
||||||
let cx = v.x + self.size.x * 0.5;
|
|
||||||
let cy = self.size.y * 0.5 - v.y;
|
|
||||||
let wx = self.u_vector.x * cx + self.v_vector.x * cy;
|
|
||||||
let wy = self.u_vector.y * cx + self.v_vector.y * cy;
|
|
||||||
let wz = self.u_vector.z * cx + self.v_vector.z * cy;
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
square_grip(i, glam::DVec3::new(ox + wx, oy + wy, oz + wz))
|
square_grip(i, glam::DVec3::from(point))
|
||||||
} else {
|
} else {
|
||||||
center_grip(i, glam::DVec3::new(ox + wx, oy + wy, oz + wz))
|
center_grip(i, glam::DVec3::from(point))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -527,61 +541,84 @@ impl Grippable for Wipeout {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
let is_polygon = self.clipping_enabled
|
if wipeout_is_polygonal(self) {
|
||||||
&& self.clip_boundary_vertices.len() >= 3
|
if let Some(current) = self.clip_boundary_vertices.get(grip_id).cloned() {
|
||||||
&& matches!(
|
let current_world = wipeout_clip_to_world(self, ¤t);
|
||||||
self.clip_type,
|
|
||||||
acadrust::entities::WipeoutClipType::Polygonal
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_polygon {
|
|
||||||
// Move the clicked polygon vertex in world space → back-project to pixel space.
|
|
||||||
if let Some(v) = self.clip_boundary_vertices.get_mut(grip_id) {
|
|
||||||
// Compute current world position of this vertex.
|
|
||||||
let ox = self.insertion_point.x;
|
|
||||||
let oy = self.insertion_point.y;
|
|
||||||
let oz = self.insertion_point.z;
|
|
||||||
let cur_wx =
|
|
||||||
ox + self.u_vector.x * v.x * self.size.x + self.v_vector.x * v.y * self.size.y;
|
|
||||||
let cur_wy =
|
|
||||||
oy + self.u_vector.y * v.x * self.size.x + self.v_vector.y * v.y * self.size.y;
|
|
||||||
let cur_wz =
|
|
||||||
oz + self.u_vector.z * v.x * self.size.x + self.v_vector.z * v.y * self.size.y;
|
|
||||||
let new_w = match apply {
|
let new_w = match apply {
|
||||||
GripApply::Translate(d) => [
|
GripApply::Translate(d) => [
|
||||||
cur_wx + d.x as f64,
|
current_world[0] + d.x as f64,
|
||||||
cur_wy + d.y as f64,
|
current_world[1] + d.y as f64,
|
||||||
cur_wz + d.z as f64,
|
current_world[2] + d.z as f64,
|
||||||
],
|
],
|
||||||
GripApply::Absolute(p) => [p.x as f64, p.y as f64, p.z as f64],
|
GripApply::Absolute(p) => [p.x as f64, p.y as f64, p.z as f64],
|
||||||
};
|
};
|
||||||
// Back-project: solve for pixel coords using u_vector and v_vector.
|
if let Some(clip) = wipeout_world_to_clip(self, new_w) {
|
||||||
// In 2D (XY plane): new_w - insertion_point = u_vec * vx * sx + v_vec * vy * sy
|
self.clip_boundary_vertices[grip_id] = clip;
|
||||||
let dx = new_w[0] - self.insertion_point.x;
|
|
||||||
let dy = new_w[1] - self.insertion_point.y;
|
|
||||||
let ux = self.u_vector.x * self.size.x;
|
|
||||||
let uy = self.u_vector.y * self.size.x;
|
|
||||||
let vx = self.v_vector.x * self.size.y;
|
|
||||||
let vy = self.v_vector.y * self.size.y;
|
|
||||||
let det = ux * vy - uy * vx;
|
|
||||||
if det.abs() > 1e-12 {
|
|
||||||
v.x = (dx * vy - dy * vx) / det;
|
|
||||||
v.y = (ux * dy - uy * dx) / det;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if grip_id == 0 {
|
return;
|
||||||
match apply {
|
|
||||||
GripApply::Translate(d) => {
|
|
||||||
self.insertion_point.x += d.x as f64;
|
|
||||||
self.insertion_point.y += d.y as f64;
|
|
||||||
self.insertion_point.z += d.z as f64;
|
|
||||||
}
|
}
|
||||||
GripApply::Absolute(p) => {
|
|
||||||
self.insertion_point.x = p.x as f64;
|
let corners = wipeout_boundary(self);
|
||||||
self.insertion_point.y = p.y as f64;
|
let Some(current) = corners.get(grip_id).copied() else {
|
||||||
self.insertion_point.z = p.z as f64;
|
return;
|
||||||
|
};
|
||||||
|
let target = match apply {
|
||||||
|
GripApply::Translate(delta) => glam::DVec3::from(current) + delta,
|
||||||
|
GripApply::Absolute(point) => point,
|
||||||
|
};
|
||||||
|
let u_hat = glam::DVec3::new(self.u_vector.x, self.u_vector.y, self.u_vector.z)
|
||||||
|
.normalize_or_zero();
|
||||||
|
let v_hat = glam::DVec3::new(self.v_vector.x, self.v_vector.y, self.v_vector.z)
|
||||||
|
.normalize_or_zero();
|
||||||
|
if u_hat == glam::DVec3::ZERO || v_hat == glam::DVec3::ZERO {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
let (width, height, insertion) = match grip_id {
|
||||||
|
0 => {
|
||||||
|
let fixed = glam::DVec3::from(corners[2]);
|
||||||
|
let delta = fixed - target;
|
||||||
|
(delta.dot(u_hat), delta.dot(v_hat), target)
|
||||||
}
|
}
|
||||||
|
1 => {
|
||||||
|
let fixed = glam::DVec3::from(corners[3]);
|
||||||
|
let delta = target - fixed;
|
||||||
|
let width = delta.dot(u_hat);
|
||||||
|
let height = -delta.dot(v_hat);
|
||||||
|
(width, height, fixed - v_hat * height)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let fixed = glam::DVec3::from(corners[0]);
|
||||||
|
let delta = target - fixed;
|
||||||
|
(delta.dot(u_hat), delta.dot(v_hat), fixed)
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
let fixed = glam::DVec3::from(corners[1]);
|
||||||
|
let delta = target - fixed;
|
||||||
|
let width = -delta.dot(u_hat);
|
||||||
|
let height = delta.dot(v_hat);
|
||||||
|
(width, height, fixed - u_hat * width)
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
if width.abs() > 1e-9 && height.abs() > 1e-9 {
|
||||||
|
self.insertion_point = acadrust::types::Vector3::new(
|
||||||
|
insertion.x,
|
||||||
|
insertion.y,
|
||||||
|
insertion.z,
|
||||||
|
);
|
||||||
|
let sx = self.size.x.abs().max(1e-9);
|
||||||
|
let sy = self.size.y.abs().max(1e-9);
|
||||||
|
self.u_vector = acadrust::types::Vector3::new(
|
||||||
|
u_hat.x * width / sx,
|
||||||
|
u_hat.y * width / sx,
|
||||||
|
u_hat.z * width / sx,
|
||||||
|
);
|
||||||
|
self.v_vector = acadrust::types::Vector3::new(
|
||||||
|
v_hat.x * height / sy,
|
||||||
|
v_hat.y * height / sy,
|
||||||
|
v_hat.z * height / sy,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -607,76 +644,23 @@ impl PropertyEditable for Wipeout {
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Misc").into_owned(),
|
title: t!("Misc").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
Property {
|
ro(t!("Show image").as_ref(), "wo_show_image", if show_image { t!("Yes") } else { t!("No") }),
|
||||||
label: t!("Show image").into_owned(),
|
ro(t!("Show clipped").as_ref(), "wo_show_clipped", if show_clipped { t!("Yes") } else { t!("No") }),
|
||||||
field: "wo_show_image",
|
ro(t!("Background transparency").as_ref(), "wo_bg_transparency", if bg_transparency { t!("Yes") } else { t!("No") }),
|
||||||
value: PropValue::BoolToggle {
|
],
|
||||||
field: "wo_show_image",
|
|
||||||
value: show_image,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Property {
|
|
||||||
label: t!("Show clipped").into_owned(),
|
|
||||||
field: "wo_show_clipped",
|
|
||||||
value: PropValue::BoolToggle {
|
|
||||||
field: "wo_show_clipped",
|
|
||||||
value: show_clipped,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Property {
|
|
||||||
label: t!("Background transparency").into_owned(),
|
|
||||||
field: "wo_bg_transparency",
|
|
||||||
value: PropValue::BoolToggle {
|
|
||||||
field: "wo_bg_transparency",
|
|
||||||
value: bg_transparency,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
PropSection {
|
||||||
|
title: t!("Image Adjust").into_owned(),
|
||||||
|
props: vec![
|
||||||
|
ro(t!("Brightness").as_ref(), "wo_brightness", self.brightness.to_string()),
|
||||||
|
ro(t!("Contrast").as_ref(), "wo_contrast", self.contrast.to_string()),
|
||||||
|
ro(t!("Fade").as_ref(), "wo_fade", self.fade.to_string()),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||||
match field {
|
|
||||||
"wo_show_image" => {
|
|
||||||
let on = if value == "toggle" {
|
|
||||||
!self.flags.contains(acadrust::entities::WipeoutDisplayFlags::SHOW_IMAGE)
|
|
||||||
} else {
|
|
||||||
value == "true"
|
|
||||||
};
|
|
||||||
self.set_frame_visible(on);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
"wo_show_clipped" => {
|
|
||||||
let on = if value == "toggle" {
|
|
||||||
!self
|
|
||||||
.flags
|
|
||||||
.contains(acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY)
|
|
||||||
} else {
|
|
||||||
value == "true"
|
|
||||||
};
|
|
||||||
if on {
|
|
||||||
self.flags |= acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY;
|
|
||||||
} else {
|
|
||||||
self.flags -= acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
"wo_bg_transparency" => {
|
|
||||||
let on = if value == "toggle" {
|
|
||||||
!self.flags.contains(acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON)
|
|
||||||
} else {
|
|
||||||
value == "true"
|
|
||||||
};
|
|
||||||
if on {
|
|
||||||
self.flags |= acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON;
|
|
||||||
} else {
|
|
||||||
self.flags -= acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
let Ok(v) = value.trim().parse::<f64>() else {
|
let Ok(v) = value.trim().parse::<f64>() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// WIPEOUT command — draw a polygonal mask or derive one from a closed polyline.
|
// WIPEOUT command — draw a polygonal mask or derive one from a closed polyline.
|
||||||
|
|
||||||
use acadrust::entities::{Wipeout, WipeoutClipType};
|
use acadrust::entities::{Wipeout, WipeoutClipType};
|
||||||
|
use acadrust::objects::{ObjectType, WipeoutVariables};
|
||||||
use acadrust::types::{Vector2, Vector3};
|
use acadrust::types::{Vector2, Vector3};
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{CadDocument, EntityType, Handle};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
|
|
@ -26,6 +27,8 @@ pub struct WipeoutCommand {
|
||||||
first: Option<DVec3>,
|
first: Option<DVec3>,
|
||||||
points: Vec<DVec3>,
|
points: Vec<DVec3>,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
|
selected_polyline: Option<Handle>,
|
||||||
|
frame_mode: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
|
@ -33,15 +36,19 @@ enum WipeoutMode {
|
||||||
Draw,
|
Draw,
|
||||||
Polyline,
|
Polyline,
|
||||||
Rectangular,
|
Rectangular,
|
||||||
|
Frames,
|
||||||
|
ErasePolyline,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WipeoutCommand {
|
impl WipeoutCommand {
|
||||||
pub fn new_polygonal() -> Self {
|
pub fn new_polygonal(frame_mode: i16) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mode: WipeoutMode::Draw,
|
mode: WipeoutMode::Draw,
|
||||||
first: None,
|
first: None,
|
||||||
points: Vec::new(),
|
points: Vec::new(),
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
|
selected_polyline: None,
|
||||||
|
frame_mode: frame_mode.clamp(0, 2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,6 +58,8 @@ impl WipeoutCommand {
|
||||||
first: None,
|
first: None,
|
||||||
points: Vec::new(),
|
points: Vec::new(),
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
|
selected_polyline: None,
|
||||||
|
frame_mode: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +70,8 @@ impl WipeoutCommand {
|
||||||
first: None,
|
first: None,
|
||||||
points: Vec::new(),
|
points: Vec::new(),
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
|
selected_polyline: None,
|
||||||
|
frame_mode: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +105,7 @@ impl CadCommand for WipeoutCommand {
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
WipeoutMode::Draw if self.points.is_empty() => {
|
WipeoutMode::Draw if self.points.is_empty() => {
|
||||||
t!("WIPEOUT Specify first point or [Polyline]:").into_owned()
|
t!("WIPEOUT Specify first point or [Frames/Polyline] <Polyline>:").into_owned()
|
||||||
}
|
}
|
||||||
WipeoutMode::Draw => {
|
WipeoutMode::Draw => {
|
||||||
let n = self.points.len();
|
let n = self.points.len();
|
||||||
|
|
@ -113,18 +124,38 @@ impl CadCommand for WipeoutCommand {
|
||||||
WipeoutMode::Rectangular => {
|
WipeoutMode::Rectangular => {
|
||||||
t!("WIPEOUT Rectangular Specify opposite corner:").into_owned()
|
t!("WIPEOUT Rectangular Specify opposite corner:").into_owned()
|
||||||
}
|
}
|
||||||
|
WipeoutMode::Frames => t!(
|
||||||
|
"WIPEOUT Frames Enter frame mode [Off/On/DisplayButNotPlot] <%{mode}>:",
|
||||||
|
mode = self.frame_mode
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
WipeoutMode::ErasePolyline => {
|
||||||
|
t!("WIPEOUT Polyline Erase source polyline? [Yes/No] <No>:").into_owned()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
WipeoutMode::Draw if self.points.is_empty() => {
|
WipeoutMode::Draw if self.points.is_empty() => {
|
||||||
vec![CmdOption::new(t!("Polyline").as_ref(), "P")]
|
vec![
|
||||||
|
CmdOption::new(t!("Frames").as_ref(), "F"),
|
||||||
|
CmdOption::new(t!("Polyline").as_ref(), "P"),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
WipeoutMode::Draw => vec![
|
WipeoutMode::Draw => vec![
|
||||||
CmdOption::new(t!("Undo").as_ref(), "U"),
|
CmdOption::new(t!("Undo").as_ref(), "U"),
|
||||||
CmdOption::new(t!("Close").as_ref(), "C"),
|
CmdOption::new(t!("Close").as_ref(), "C"),
|
||||||
],
|
],
|
||||||
|
WipeoutMode::Frames => vec![
|
||||||
|
CmdOption::new(t!("Off").as_ref(), "OFF"),
|
||||||
|
CmdOption::new(t!("On").as_ref(), "ON"),
|
||||||
|
CmdOption::new(t!("Display but not plot").as_ref(), "D"),
|
||||||
|
],
|
||||||
|
WipeoutMode::ErasePolyline => vec![
|
||||||
|
CmdOption::new(t!("Yes").as_ref(), "Y"),
|
||||||
|
CmdOption::new(t!("No").as_ref(), "N"),
|
||||||
|
],
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,13 +185,23 @@ impl CadCommand for WipeoutCommand {
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WipeoutMode::Polyline => CmdResult::NeedPoint,
|
WipeoutMode::Polyline | WipeoutMode::Frames | WipeoutMode::ErasePolyline => {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
|
WipeoutMode::Draw if self.points.is_empty() => {
|
||||||
|
self.mode = WipeoutMode::Polyline;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
WipeoutMode::Draw if self.points.len() >= 3 => self.finish_draw(),
|
WipeoutMode::Draw if self.points.len() >= 3 => self.finish_draw(),
|
||||||
|
WipeoutMode::Frames => {
|
||||||
|
CmdResult::Dispatch(format!("WIPEOUTFRAME {}", self.frame_mode))
|
||||||
|
}
|
||||||
|
WipeoutMode::ErasePolyline => self.finish_polyline(false),
|
||||||
_ => CmdResult::Cancel,
|
_ => CmdResult::Cancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,34 +218,56 @@ impl CadCommand for WipeoutCommand {
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
} else {
|
} else {
|
||||||
CmdResult::WipeoutFromPolyline(handle)
|
self.selected_polyline = Some(handle);
|
||||||
|
self.mode = WipeoutMode::ErasePolyline;
|
||||||
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
self.mode == WipeoutMode::Draw
|
matches!(
|
||||||
|
self.mode,
|
||||||
|
WipeoutMode::Draw | WipeoutMode::Frames | WipeoutMode::ErasePolyline
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
self.mode == WipeoutMode::Draw
|
matches!(
|
||||||
|
self.mode,
|
||||||
|
WipeoutMode::Draw | WipeoutMode::Frames | WipeoutMode::ErasePolyline
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if self.mode != WipeoutMode::Draw {
|
let text = text.trim().to_ascii_uppercase();
|
||||||
return None;
|
match self.mode {
|
||||||
|
WipeoutMode::Draw => match text.as_str() {
|
||||||
|
"F" | "FRAMES" if self.points.is_empty() => {
|
||||||
|
self.mode = WipeoutMode::Frames;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
match text.trim().to_ascii_uppercase().as_str() {
|
|
||||||
"P" | "POLYLINE" if self.points.is_empty() => {
|
"P" | "POLYLINE" if self.points.is_empty() => {
|
||||||
self.mode = WipeoutMode::Polyline;
|
self.mode = WipeoutMode::Polyline;
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
"U" | "UNDO" if !self.points.is_empty() => {
|
"U" | "UNDO" if !self.points.is_empty() => Some(self.undo_point()),
|
||||||
Some(self.undo_point())
|
"C" | "CLOSE" if self.points.len() >= 3 => Some(self.finish_draw()),
|
||||||
}
|
_ => None,
|
||||||
"C" | "CLOSE" if self.points.len() >= 3 => {
|
},
|
||||||
Some(self.finish_draw())
|
WipeoutMode::Frames => match text.as_str() {
|
||||||
|
"0" | "OFF" => Some(CmdResult::Dispatch("WIPEOUTFRAME 0".into())),
|
||||||
|
"1" | "ON" => Some(CmdResult::Dispatch("WIPEOUTFRAME 1".into())),
|
||||||
|
"2" | "D" | "DISPLAYBUTNOTPLOT" => {
|
||||||
|
Some(CmdResult::Dispatch("WIPEOUTFRAME 2".into()))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
|
},
|
||||||
|
WipeoutMode::ErasePolyline => match text.as_str() {
|
||||||
|
"Y" | "YES" => Some(self.finish_polyline(true)),
|
||||||
|
"N" | "NO" => Some(self.finish_polyline(false)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,11 +327,61 @@ impl CadCommand for WipeoutCommand {
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
WipeoutMode::Polyline => None,
|
WipeoutMode::Polyline | WipeoutMode::Frames | WipeoutMode::ErasePolyline => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WipeoutCommand {
|
||||||
|
fn finish_polyline(&self, erase_source: bool) -> CmdResult {
|
||||||
|
self.selected_polyline.map_or(CmdResult::Cancel, |handle| {
|
||||||
|
CmdResult::WipeoutFromPolyline {
|
||||||
|
handle,
|
||||||
|
erase_source,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn wipeout_frame_mode(document: &CadDocument) -> i16 {
|
||||||
|
document
|
||||||
|
.objects
|
||||||
|
.values()
|
||||||
|
.find_map(|object| match object {
|
||||||
|
ObjectType::WipeoutVariables(value) => Some(value.display_frame),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(1)
|
||||||
|
.clamp(0, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_wipeout_frame_mode(document: &mut CadDocument, mode: i16) {
|
||||||
|
let mode = mode.clamp(0, 2);
|
||||||
|
if let Some(value) = document.objects.values_mut().find_map(|object| match object {
|
||||||
|
ObjectType::WipeoutVariables(value) => Some(value),
|
||||||
|
_ => None,
|
||||||
|
}) {
|
||||||
|
value.display_frame = mode;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let owner = crate::scene::annotative::root_named_dict_handle(document);
|
||||||
|
let handle = document.allocate_handle();
|
||||||
|
let mut value = WipeoutVariables::new();
|
||||||
|
value.handle = handle;
|
||||||
|
value.owner = owner;
|
||||||
|
value.display_frame = mode;
|
||||||
|
document
|
||||||
|
.objects
|
||||||
|
.insert(handle, ObjectType::WipeoutVariables(value));
|
||||||
|
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&owner) {
|
||||||
|
dictionary
|
||||||
|
.entries
|
||||||
|
.retain(|(name, _)| !name.eq_ignore_ascii_case("ACAD_WIPEOUT_VARS"));
|
||||||
|
dictionary.add_entry("ACAD_WIPEOUT_VARS", handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn make_rect_wipeout(first: DVec3, second: DVec3) -> EntityType {
|
fn make_rect_wipeout(first: DVec3, second: DVec3) -> EntityType {
|
||||||
let corner1 = Vector3::new(
|
let corner1 = Vector3::new(
|
||||||
first.x.min(second.x),
|
first.x.min(second.x),
|
||||||
|
|
@ -300,11 +413,64 @@ fn clean_boundary(points: &[DVec3]) -> Vec<DVec3> {
|
||||||
clean
|
clean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn boundary_is_simple(points: &[DVec3]) -> bool {
|
||||||
|
fn orient(a: DVec3, b: DVec3, c: DVec3) -> f64 {
|
||||||
|
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
|
||||||
|
}
|
||||||
|
fn on_segment(a: DVec3, b: DVec3, p: DVec3) -> bool {
|
||||||
|
let eps = 1e-9;
|
||||||
|
orient(a, b, p).abs() <= eps
|
||||||
|
&& p.x >= a.x.min(b.x) - eps
|
||||||
|
&& p.x <= a.x.max(b.x) + eps
|
||||||
|
&& p.y >= a.y.min(b.y) - eps
|
||||||
|
&& p.y <= a.y.max(b.y) + eps
|
||||||
|
}
|
||||||
|
fn intersects(a: DVec3, b: DVec3, c: DVec3, d: DVec3) -> bool {
|
||||||
|
let ab_c = orient(a, b, c);
|
||||||
|
let ab_d = orient(a, b, d);
|
||||||
|
let cd_a = orient(c, d, a);
|
||||||
|
let cd_b = orient(c, d, b);
|
||||||
|
((ab_c > 0.0 && ab_d < 0.0) || (ab_c < 0.0 && ab_d > 0.0))
|
||||||
|
&& ((cd_a > 0.0 && cd_b < 0.0) || (cd_a < 0.0 && cd_b > 0.0))
|
||||||
|
|| on_segment(a, b, c)
|
||||||
|
|| on_segment(a, b, d)
|
||||||
|
|| on_segment(c, d, a)
|
||||||
|
|| on_segment(c, d, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = points.len();
|
||||||
|
for first in 0..count {
|
||||||
|
let first_next = (first + 1) % count;
|
||||||
|
for second in first + 1..count {
|
||||||
|
let second_next = (second + 1) % count;
|
||||||
|
if first == second
|
||||||
|
|| first_next == second
|
||||||
|
|| second_next == first
|
||||||
|
|| (first == 0 && second_next == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if intersects(
|
||||||
|
points[first],
|
||||||
|
points[first_next],
|
||||||
|
points[second],
|
||||||
|
points[second_next],
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn make_poly_wipeout(points: &[DVec3]) -> Option<EntityType> {
|
fn make_poly_wipeout(points: &[DVec3]) -> Option<EntityType> {
|
||||||
let points = clean_boundary(points);
|
let points = clean_boundary(points);
|
||||||
if points.len() < 3 {
|
if points.len() < 3 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if !boundary_is_simple(&points) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let z = points[0].z;
|
let z = points[0].z;
|
||||||
if points.iter().any(|point| (point.z - z).abs() > 1e-7) {
|
if points.iter().any(|point| (point.z - z).abs() > 1e-7) {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -364,50 +530,27 @@ fn explicitly_closed(points: &[DVec3]) -> bool {
|
||||||
points.len() >= 4 && same_point(points[0], *points.last().unwrap())
|
points.len() >= 4 && same_point(points[0], *points.last().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_2d_polyline(
|
/// Build a wipeout boundary from a picked closed polyline without consuming
|
||||||
vertices: &[([f64; 2], f64)],
|
/// the source entity. Only straight, closed 2D polygonal boundaries qualify.
|
||||||
elevation: f64,
|
pub(crate) fn wipeout_from_polyline(entity: &EntityType) -> Option<EntityType> {
|
||||||
normal: (f64, f64, f64),
|
fn from_ocs(points: &[DVec3], normal: (f64, f64, f64)) -> Option<EntityType> {
|
||||||
) -> Vec<DVec3> {
|
let origin = crate::scene::view::transform::ocs_point_to_wcs((0.0, 0.0, 0.0), normal);
|
||||||
if vertices.len() < 3 {
|
let x = crate::scene::view::transform::ocs_point_to_wcs((1.0, 0.0, 0.0), normal);
|
||||||
return Vec::new();
|
let y = crate::scene::view::transform::ocs_point_to_wcs((0.0, 1.0, 0.0), normal);
|
||||||
}
|
let origin = DVec3::new(origin.0, origin.1, origin.2);
|
||||||
let to_wcs = |point: [f64; 2]| {
|
let plane = WorkingPlane::new(
|
||||||
let point = crate::scene::view::transform::ocs_point_to_wcs(
|
origin,
|
||||||
(point[0], point[1], elevation),
|
DVec3::new(x.0, x.1, x.2) - origin,
|
||||||
normal,
|
DVec3::new(y.0, y.1, y.2) - origin,
|
||||||
);
|
);
|
||||||
DVec3::new(point.0, point.1, point.2)
|
make_poly_wipeout(points).map(|entity| plane.place_entity(entity))
|
||||||
};
|
|
||||||
let mut points = vec![to_wcs(vertices[0].0)];
|
|
||||||
for index in 0..vertices.len() {
|
|
||||||
let (start, bulge) = vertices[index];
|
|
||||||
let end_index = (index + 1) % vertices.len();
|
|
||||||
let end = vertices[end_index].0;
|
|
||||||
if let Some(arc) =
|
|
||||||
crate::entities::common::BulgeArc::from_bulge(start, end, bulge)
|
|
||||||
{
|
|
||||||
let steps = ((arc.sweep.abs() / std::f64::consts::TAU * 64.0).ceil()
|
|
||||||
as usize)
|
|
||||||
.clamp(4, 64);
|
|
||||||
for step in 1..=steps {
|
|
||||||
if end_index == 0 && step == steps {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
points.push(to_wcs(arc.sample(step as f64 / steps as f64)));
|
|
||||||
}
|
|
||||||
} else if end_index != 0 {
|
|
||||||
points.push(to_wcs(end));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
points
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a wipeout boundary from a picked closed polyline without consuming
|
match entity {
|
||||||
/// the source entity. Bulged 2D segments are sampled into the polygonal mask.
|
|
||||||
pub(crate) fn wipeout_from_polyline(entity: &EntityType) -> Option<EntityType> {
|
|
||||||
let points = match entity {
|
|
||||||
EntityType::LwPolyline(polyline) => {
|
EntityType::LwPolyline(polyline) => {
|
||||||
|
if polyline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1e-12) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let raw: Vec<DVec3> = polyline
|
let raw: Vec<DVec3> = polyline
|
||||||
.vertices
|
.vertices
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -416,27 +559,15 @@ pub(crate) fn wipeout_from_polyline(entity: &EntityType) -> Option<EntityType> {
|
||||||
if !polyline.is_closed && !explicitly_closed(&raw) {
|
if !polyline.is_closed && !explicitly_closed(&raw) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let vertices: Vec<([f64; 2], f64)> = polyline
|
from_ocs(
|
||||||
.vertices
|
&raw,
|
||||||
.iter()
|
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
|
||||||
.map(|vertex| {
|
|
||||||
(
|
|
||||||
[vertex.location.x, vertex.location.y],
|
|
||||||
vertex.bulge,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
sample_2d_polyline(
|
|
||||||
&vertices,
|
|
||||||
polyline.elevation,
|
|
||||||
(
|
|
||||||
polyline.normal.x,
|
|
||||||
polyline.normal.y,
|
|
||||||
polyline.normal.z,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
EntityType::Polyline2D(polyline) => {
|
EntityType::Polyline2D(polyline) => {
|
||||||
|
if polyline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1e-12) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let raw: Vec<DVec3> = polyline
|
let raw: Vec<DVec3> = polyline
|
||||||
.vertices
|
.vertices
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -451,63 +582,13 @@ pub(crate) fn wipeout_from_polyline(entity: &EntityType) -> Option<EntityType> {
|
||||||
if !polyline.is_closed() && !explicitly_closed(&raw) {
|
if !polyline.is_closed() && !explicitly_closed(&raw) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let vertices: Vec<([f64; 2], f64)> = polyline
|
from_ocs(
|
||||||
.vertices
|
&raw,
|
||||||
.iter()
|
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
|
||||||
.map(|vertex| {
|
|
||||||
(
|
|
||||||
[vertex.location.x, vertex.location.y],
|
|
||||||
vertex.bulge,
|
|
||||||
)
|
)
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
sample_2d_polyline(
|
|
||||||
&vertices,
|
|
||||||
polyline.elevation,
|
|
||||||
(
|
|
||||||
polyline.normal.x,
|
|
||||||
polyline.normal.y,
|
|
||||||
polyline.normal.z,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
EntityType::Polyline(polyline) => {
|
|
||||||
let points: Vec<DVec3> = polyline
|
|
||||||
.vertices
|
|
||||||
.iter()
|
|
||||||
.map(|vertex| {
|
|
||||||
DVec3::new(
|
|
||||||
vertex.location.x,
|
|
||||||
vertex.location.y,
|
|
||||||
vertex.location.z,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if !polyline.is_closed() && !explicitly_closed(&points) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
points
|
|
||||||
}
|
|
||||||
EntityType::Polyline3D(polyline) => {
|
|
||||||
let points: Vec<DVec3> = polyline
|
|
||||||
.vertices
|
|
||||||
.iter()
|
|
||||||
.map(|vertex| {
|
|
||||||
DVec3::new(
|
|
||||||
vertex.position.x,
|
|
||||||
vertex.position.y,
|
|
||||||
vertex.position.z,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if !polyline.flags.closed && !explicitly_closed(&points) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
points
|
|
||||||
}
|
}
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
}
|
||||||
make_poly_wipeout(&points)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ impl CadModule for InsertModule {
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"FRAMES2",
|
"FRAMES2",
|
||||||
"Frames & Print",
|
"Frames On, Not Plotted",
|
||||||
IconKind::Svg(include_bytes!(
|
IconKind::Svg(include_bytes!(
|
||||||
"../../../assets/icons/underlay_frames.svg"
|
"../../../assets/icons/underlay_frames.svg"
|
||||||
)),
|
)),
|
||||||
|
|
|
||||||
|
|
@ -1208,6 +1208,22 @@ pub(crate) fn tessellate_entity(
|
||||||
if b.text_verts.is_empty() {
|
if b.text_verts.is_empty() {
|
||||||
set_wire_aabb(b, aabb);
|
set_wire_aabb(b, aabb);
|
||||||
}
|
}
|
||||||
|
if matches!(e, EntityType::Wipeout(_)) {
|
||||||
|
b.depth_override = Some(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hidden mask frame remains selectable and appears while selected, but
|
||||||
|
// contributes no visible line work during normal display. The interior
|
||||||
|
// pick triangles remain intact.
|
||||||
|
if matches!(e, EntityType::Wipeout(_))
|
||||||
|
&& crate::modules::draw::draw::wipeout::wipeout_frame_mode(document) == 0
|
||||||
|
&& !sel
|
||||||
|
{
|
||||||
|
for base in &mut bases {
|
||||||
|
base.points.clear();
|
||||||
|
base.points_low.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complex linetypes (with embedded shapes / text) expand the *base*
|
// Complex linetypes (with embedded shapes / text) expand the *base*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue