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]]
|
||||
name = "acadrust"
|
||||
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 = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
|
|
@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||
[[package]]
|
||||
name = "cadkernel"
|
||||
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 = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||
rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "3d5de36", features = ["acis", "offset"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6b6b50a", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d674c55", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
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
|
||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||
# 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).
|
||||
interprocess = { version = "2", optional = true }
|
||||
|
|
@ -37,7 +37,7 @@ serde_json = "1"
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
cargo-lock = "11"
|
||||
# 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]
|
||||
serde_json = "1"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ publish = false
|
|||
crate-type = ["cdylib"]
|
||||
|
||||
[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"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
|
|
|||
|
|
@ -1014,7 +1014,10 @@ impl OpenCADStudio {
|
|||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
CmdResult::WipeoutFromPolyline(handle) => {
|
||||
CmdResult::WipeoutFromPolyline {
|
||||
handle,
|
||||
erase_source,
|
||||
} => {
|
||||
let wipeout = {
|
||||
let scene = &self.tabs[i].scene;
|
||||
scene
|
||||
|
|
@ -1026,16 +1029,23 @@ impl OpenCADStudio {
|
|||
)
|
||||
};
|
||||
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));
|
||||
}
|
||||
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) =
|
||||
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
||||
{
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
let command =
|
||||
crate::modules::draw::draw::wipeout::WipeoutCommand::new_polyline();
|
||||
self.command_line.push_info(
|
||||
&crate::command::CadCommand::prompt(&command),
|
||||
);
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
CmdResult::MviewSwitchLayout(layout) => {
|
||||
let task = self.on_layout_switch_preserving_command(layout);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,11 @@ impl OpenCADStudio {
|
|||
let wo_cmd = match args.as_str() {
|
||||
"P" | "POLYLINE" => WipeoutCommand::new_polyline(),
|
||||
"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.tabs[i].active_cmd = Some(Box::new(wo_cmd));
|
||||
|
|
|
|||
|
|
@ -515,6 +515,10 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||
"TEXTQLTY",
|
||||
"SORTENTS",
|
||||
"XCLIPFRAME",
|
||||
"WIPEOUTFRAME",
|
||||
"FRAMES0",
|
||||
"FRAMES1",
|
||||
"FRAMES2",
|
||||
"HALOGAP",
|
||||
"TRACEWID",
|
||||
"SKETCHINC",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ use super::*;
|
|||
impl OpenCADStudio {
|
||||
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
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
|
||||
// objects (CECOLOR). Bare COLOR reports the current value.
|
||||
"COLOR" | "COLOUR" | "CECOLOR" | "DDCOLOR" => {
|
||||
|
|
@ -920,6 +923,7 @@ impl OpenCADStudio {
|
|||
| "TEXTQLTY"
|
||||
| "SORTENTS"
|
||||
| "XCLIPFRAME"
|
||||
| "WIPEOUTFRAME"
|
||||
| "HALOGAP"
|
||||
| "TRACEWID"
|
||||
| "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)",
|
||||
);
|
||||
} 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.
|
||||
let parse_bool = |s: &str| match s.to_uppercase().as_str() {
|
||||
"1" | "ON" | "TRUE" => Some(true),
|
||||
|
|
|
|||
|
|
@ -168,7 +168,21 @@ fn plot_scene_content(
|
|||
Vec<crate::scene::model::hatch_model::HatchModel>,
|
||||
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_wipeouts = scene.paper_canvas_wipeouts().as_ref().clone();
|
||||
if scene.current_layout == "Model" {
|
||||
|
|
@ -184,8 +198,19 @@ fn plot_scene_content(
|
|||
splits,
|
||||
);
|
||||
}
|
||||
let (model_pattern_wires, model_hatches, model_wipeouts) =
|
||||
let (mut model_pattern_wires, model_hatches, model_wipeouts) =
|
||||
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 splits = crate::io::pdf_export::PlotGroupSplits {
|
||||
|
|
|
|||
|
|
@ -1297,7 +1297,11 @@ pub enum CmdResult {
|
|||
boundary_handle: Handle,
|
||||
},
|
||||
/// 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
|
||||
/// model-space window, keeping the command active.
|
||||
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::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
use crate::scene::text::lff;
|
||||
|
||||
// ── Shared geometry helpers ───────────────────────────────────────────────────
|
||||
|
|
@ -416,62 +417,93 @@ impl Transformable for RasterImage {
|
|||
|
||||
// ── Wipeout ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn wipeout_is_polygonal(wipeout: &Wipeout) -> bool {
|
||||
wipeout.clipping_enabled
|
||||
&& wipeout.clip_boundary_vertices.len() >= 3
|
||||
&& matches!(
|
||||
wipeout.clip_type,
|
||||
acadrust::entities::WipeoutClipType::Polygonal
|
||||
)
|
||||
}
|
||||
|
||||
fn wipeout_clip_to_world(wipeout: &Wipeout, point: &acadrust::types::Vector2) -> [f64; 3] {
|
||||
let x = point.x + wipeout.size.x * 0.5;
|
||||
let y = wipeout.size.y * 0.5 - point.y;
|
||||
[
|
||||
wipeout.insertion_point.x + wipeout.u_vector.x * x + wipeout.v_vector.x * y,
|
||||
wipeout.insertion_point.y + wipeout.u_vector.y * x + wipeout.v_vector.y * y,
|
||||
wipeout.insertion_point.z + wipeout.u_vector.z * x + wipeout.v_vector.z * y,
|
||||
]
|
||||
}
|
||||
|
||||
fn wipeout_boundary(wipeout: &Wipeout) -> Vec<[f64; 3]> {
|
||||
if wipeout_is_polygonal(wipeout) {
|
||||
wipeout
|
||||
.clip_boundary_vertices
|
||||
.iter()
|
||||
.map(|point| wipeout_clip_to_world(wipeout, point))
|
||||
.collect()
|
||||
} else {
|
||||
image_corners(
|
||||
&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 corners = image_corners(
|
||||
&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!(
|
||||
self.clip_type,
|
||||
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
|
||||
// "v_vector points down the image"), so map:
|
||||
// x_off = (clip.x + size.x/2) × u_vector
|
||||
// y_off = (size.y/2 − clip.y) × v_vector ← y flipped
|
||||
let ox = self.insertion_point.x;
|
||||
let oy = self.insertion_point.y;
|
||||
let oz = self.insertion_point.z;
|
||||
let mut poly: Vec<[f64; 3]> = self
|
||||
.clip_boundary_vertices
|
||||
.iter()
|
||||
.map(|v| {
|
||||
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;
|
||||
[ox + wx, oy + wy, oz + wz]
|
||||
})
|
||||
.collect();
|
||||
// Close the polygon.
|
||||
if let Some(&first) = poly.first() {
|
||||
poly.push(first);
|
||||
}
|
||||
poly
|
||||
} else {
|
||||
// Rectangular boundary — just the border, no diagonals (mask area).
|
||||
image_wire(corners, false)
|
||||
};
|
||||
let boundary = wipeout_boundary(self);
|
||||
let mut pts = boundary.clone();
|
||||
if let Some(&first) = pts.first() {
|
||||
pts.push(first);
|
||||
}
|
||||
|
||||
Some(RenderEntity {
|
||||
// Interior pick surface — a wipeout reads as a solid patch, so a
|
||||
// click anywhere on it should select it.
|
||||
pick_tris: crate::entities::common::quad_pick_tris(&corners),
|
||||
pick_tris: crate::entities::mesh::triangulate_planar(&boundary),
|
||||
object: RenderObject::Lines(pts),
|
||||
snap_pts: vec![],
|
||||
snap_pts: boundary
|
||||
.iter()
|
||||
.map(|point| (glam::DVec3::from(*point), SnapHint::Endpoint))
|
||||
.collect(),
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: corners.to_vec(),
|
||||
key_vertices: boundary,
|
||||
fill_tris: vec![],
|
||||
})
|
||||
}
|
||||
|
|
@ -479,33 +511,15 @@ impl RenderConvertible for Wipeout {
|
|||
|
||||
impl Grippable for Wipeout {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
// If polygonal clipping is active, expose individual polygon vertices as grips.
|
||||
let is_polygon = self.clipping_enabled
|
||||
&& self.clip_boundary_vertices.len() >= 3
|
||||
&& 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()
|
||||
if wipeout_is_polygonal(self) {
|
||||
wipeout_boundary(self)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
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;
|
||||
.map(|(i, point)| {
|
||||
if i == 0 {
|
||||
square_grip(i, glam::DVec3::new(ox + wx, oy + wy, oz + wz))
|
||||
square_grip(i, glam::DVec3::from(point))
|
||||
} else {
|
||||
center_grip(i, glam::DVec3::new(ox + wx, oy + wy, oz + wz))
|
||||
center_grip(i, glam::DVec3::from(point))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -527,61 +541,84 @@ impl Grippable for Wipeout {
|
|||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
let is_polygon = self.clipping_enabled
|
||||
&& self.clip_boundary_vertices.len() >= 3
|
||||
&& matches!(
|
||||
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;
|
||||
if wipeout_is_polygonal(self) {
|
||||
if let Some(current) = self.clip_boundary_vertices.get(grip_id).cloned() {
|
||||
let current_world = wipeout_clip_to_world(self, ¤t);
|
||||
let new_w = match apply {
|
||||
GripApply::Translate(d) => [
|
||||
cur_wx + d.x as f64,
|
||||
cur_wy + d.y as f64,
|
||||
cur_wz + d.z as f64,
|
||||
current_world[0] + d.x as f64,
|
||||
current_world[1] + d.y as f64,
|
||||
current_world[2] + d.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.
|
||||
// In 2D (XY plane): new_w - insertion_point = u_vec * vx * sx + v_vec * vy * sy
|
||||
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;
|
||||
if let Some(clip) = wipeout_world_to_clip(self, new_w) {
|
||||
self.clip_boundary_vertices[grip_id] = clip;
|
||||
}
|
||||
}
|
||||
} else if grip_id == 0 {
|
||||
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;
|
||||
self.insertion_point.y = p.y as f64;
|
||||
self.insertion_point.z = p.z as f64;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let corners = wipeout_boundary(self);
|
||||
let Some(current) = corners.get(grip_id).copied() else {
|
||||
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 {
|
||||
title: t!("Misc").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Show image").into_owned(),
|
||||
field: "wo_show_image",
|
||||
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,
|
||||
},
|
||||
},
|
||||
ro(t!("Show image").as_ref(), "wo_show_image", if show_image { t!("Yes") } else { t!("No") }),
|
||||
ro(t!("Show clipped").as_ref(), "wo_show_clipped", if show_clipped { t!("Yes") } else { t!("No") }),
|
||||
ro(t!("Background transparency").as_ref(), "wo_bg_transparency", if bg_transparency { t!("Yes") } else { t!("No") }),
|
||||
],
|
||||
},
|
||||
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) {
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// WIPEOUT command — draw a polygonal mask or derive one from a closed polyline.
|
||||
|
||||
use acadrust::entities::{Wipeout, WipeoutClipType};
|
||||
use acadrust::objects::{ObjectType, WipeoutVariables};
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
|
|
@ -26,6 +27,8 @@ pub struct WipeoutCommand {
|
|||
first: Option<DVec3>,
|
||||
points: Vec<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
selected_polyline: Option<Handle>,
|
||||
frame_mode: i16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
|
|
@ -33,15 +36,19 @@ enum WipeoutMode {
|
|||
Draw,
|
||||
Polyline,
|
||||
Rectangular,
|
||||
Frames,
|
||||
ErasePolyline,
|
||||
}
|
||||
|
||||
impl WipeoutCommand {
|
||||
pub fn new_polygonal() -> Self {
|
||||
pub fn new_polygonal(frame_mode: i16) -> Self {
|
||||
Self {
|
||||
mode: WipeoutMode::Draw,
|
||||
first: None,
|
||||
points: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
selected_polyline: None,
|
||||
frame_mode: frame_mode.clamp(0, 2),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +58,8 @@ impl WipeoutCommand {
|
|||
first: None,
|
||||
points: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
selected_polyline: None,
|
||||
frame_mode: 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +70,8 @@ impl WipeoutCommand {
|
|||
first: None,
|
||||
points: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
selected_polyline: None,
|
||||
frame_mode: 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +105,7 @@ impl CadCommand for WipeoutCommand {
|
|||
fn prompt(&self) -> String {
|
||||
match self.mode {
|
||||
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 => {
|
||||
let n = self.points.len();
|
||||
|
|
@ -113,18 +124,38 @@ impl CadCommand for WipeoutCommand {
|
|||
WipeoutMode::Rectangular => {
|
||||
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> {
|
||||
match self.mode {
|
||||
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![
|
||||
CmdOption::new(t!("Undo").as_ref(), "U"),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
|
@ -154,13 +185,23 @@ impl CadCommand for WipeoutCommand {
|
|||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
WipeoutMode::Polyline => CmdResult::NeedPoint,
|
||||
WipeoutMode::Polyline | WipeoutMode::Frames | WipeoutMode::ErasePolyline => {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
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::Frames => {
|
||||
CmdResult::Dispatch(format!("WIPEOUTFRAME {}", self.frame_mode))
|
||||
}
|
||||
WipeoutMode::ErasePolyline => self.finish_polyline(false),
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
|
@ -177,33 +218,55 @@ impl CadCommand for WipeoutCommand {
|
|||
if handle.is_null() {
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::WipeoutFromPolyline(handle)
|
||||
self.selected_polyline = Some(handle);
|
||||
self.mode = WipeoutMode::ErasePolyline;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
self.mode == WipeoutMode::Draw
|
||||
matches!(
|
||||
self.mode,
|
||||
WipeoutMode::Draw | WipeoutMode::Frames | WipeoutMode::ErasePolyline
|
||||
)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.mode != WipeoutMode::Draw {
|
||||
return None;
|
||||
}
|
||||
match text.trim().to_ascii_uppercase().as_str() {
|
||||
"P" | "POLYLINE" if self.points.is_empty() => {
|
||||
self.mode = WipeoutMode::Polyline;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"U" | "UNDO" if !self.points.is_empty() => {
|
||||
Some(self.undo_point())
|
||||
}
|
||||
"C" | "CLOSE" if self.points.len() >= 3 => {
|
||||
Some(self.finish_draw())
|
||||
}
|
||||
let text = text.trim().to_ascii_uppercase();
|
||||
match self.mode {
|
||||
WipeoutMode::Draw => match text.as_str() {
|
||||
"F" | "FRAMES" if self.points.is_empty() => {
|
||||
self.mode = WipeoutMode::Frames;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"P" | "POLYLINE" if self.points.is_empty() => {
|
||||
self.mode = WipeoutMode::Polyline;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"U" | "UNDO" if !self.points.is_empty() => Some(self.undo_point()),
|
||||
"C" | "CLOSE" if self.points.len() >= 3 => Some(self.finish_draw()),
|
||||
_ => None,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
))
|
||||
}
|
||||
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 {
|
||||
let corner1 = Vector3::new(
|
||||
first.x.min(second.x),
|
||||
|
|
@ -300,11 +413,64 @@ fn clean_boundary(points: &[DVec3]) -> Vec<DVec3> {
|
|||
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> {
|
||||
let points = clean_boundary(points);
|
||||
if points.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
if !boundary_is_simple(&points) {
|
||||
return None;
|
||||
}
|
||||
let z = points[0].z;
|
||||
if points.iter().any(|point| (point.z - z).abs() > 1e-7) {
|
||||
return None;
|
||||
|
|
@ -364,50 +530,27 @@ fn explicitly_closed(points: &[DVec3]) -> bool {
|
|||
points.len() >= 4 && same_point(points[0], *points.last().unwrap())
|
||||
}
|
||||
|
||||
fn sample_2d_polyline(
|
||||
vertices: &[([f64; 2], f64)],
|
||||
elevation: f64,
|
||||
normal: (f64, f64, f64),
|
||||
) -> Vec<DVec3> {
|
||||
if vertices.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
let to_wcs = |point: [f64; 2]| {
|
||||
let point = crate::scene::view::transform::ocs_point_to_wcs(
|
||||
(point[0], point[1], elevation),
|
||||
normal,
|
||||
);
|
||||
DVec3::new(point.0, point.1, point.2)
|
||||
};
|
||||
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
|
||||
/// the source entity. Bulged 2D segments are sampled into the polygonal mask.
|
||||
/// the source entity. Only straight, closed 2D polygonal boundaries qualify.
|
||||
pub(crate) fn wipeout_from_polyline(entity: &EntityType) -> Option<EntityType> {
|
||||
let points = match entity {
|
||||
fn from_ocs(points: &[DVec3], normal: (f64, f64, f64)) -> Option<EntityType> {
|
||||
let origin = crate::scene::view::transform::ocs_point_to_wcs((0.0, 0.0, 0.0), normal);
|
||||
let x = crate::scene::view::transform::ocs_point_to_wcs((1.0, 0.0, 0.0), normal);
|
||||
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 plane = WorkingPlane::new(
|
||||
origin,
|
||||
DVec3::new(x.0, x.1, x.2) - origin,
|
||||
DVec3::new(y.0, y.1, y.2) - origin,
|
||||
);
|
||||
make_poly_wipeout(points).map(|entity| plane.place_entity(entity))
|
||||
}
|
||||
|
||||
match entity {
|
||||
EntityType::LwPolyline(polyline) => {
|
||||
if polyline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1e-12) {
|
||||
return None;
|
||||
}
|
||||
let raw: Vec<DVec3> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
|
|
@ -416,27 +559,15 @@ pub(crate) fn wipeout_from_polyline(entity: &EntityType) -> Option<EntityType> {
|
|||
if !polyline.is_closed && !explicitly_closed(&raw) {
|
||||
return None;
|
||||
}
|
||||
let vertices: Vec<([f64; 2], f64)> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.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,
|
||||
),
|
||||
from_ocs(
|
||||
&raw,
|
||||
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
|
||||
)
|
||||
}
|
||||
EntityType::Polyline2D(polyline) => {
|
||||
if polyline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1e-12) {
|
||||
return None;
|
||||
}
|
||||
let raw: Vec<DVec3> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
|
|
@ -451,63 +582,13 @@ pub(crate) fn wipeout_from_polyline(entity: &EntityType) -> Option<EntityType> {
|
|||
if !polyline.is_closed() && !explicitly_closed(&raw) {
|
||||
return None;
|
||||
}
|
||||
let vertices: Vec<([f64; 2], f64)> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.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,
|
||||
),
|
||||
from_ocs(
|
||||
&raw,
|
||||
(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,
|
||||
};
|
||||
make_poly_wipeout(&points)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ impl CadModule for InsertModule {
|
|||
),
|
||||
(
|
||||
"FRAMES2",
|
||||
"Frames & Print",
|
||||
"Frames On, Not Plotted",
|
||||
IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/underlay_frames.svg"
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -1208,6 +1208,22 @@ pub(crate) fn tessellate_entity(
|
|||
if b.text_verts.is_empty() {
|
||||
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*
|
||||
|
|
|
|||
Loading…
Reference in a new issue