feat(ucs): integrate coordinate systems

Route coordinate input, geometry creation, transforms, properties,\nviews, and viewport persistence through a shared working plane.\n\nRefs #484
This commit is contained in:
Hakan Seven 2026-08-04 23:17:54 +03:00
commit 657d023fb7
61 changed files with 2218 additions and 747 deletions

View file

@ -505,6 +505,59 @@ mod tests {
); );
} }
#[test]
fn translated_and_rotated_ucs_resolves_absolute_relative_and_polar_input() {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app.automation_op(r#"{"op":"run","cmd":"UCS ORIGIN 100,200,300"}"#);
app.automation_op(r#"{"op":"run","cmd":"UCS Z 90"}"#);
app.automation_op(r#"{"op":"run","cmd":"LINE 2,3 @5<0"}"#);
let line = app.tabs[app.active_tab]
.scene
.document
.entities()
.find_map(|entity| match entity {
acadrust::EntityType::Line(line) => Some(line),
_ => None,
})
.expect("LINE should create one segment");
let close = |a: f64, b: f64| (a - b).abs() < 1e-9;
assert!(close(line.start.x, 97.0));
assert!(close(line.start.y, 202.0));
assert!(close(line.start.z, 300.0));
assert!(close(line.end.x, 97.0));
assert!(close(line.end.y, 207.0));
assert!(close(line.end.z, 300.0));
}
#[test]
fn tilted_ucs_places_planar_entities_with_the_plane_normal() {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app.automation_op(
r#"{"op":"run","cmd":"UCS 3POINT 0,0,0 1,0,0 0,0,1"}"#,
);
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 2,3 1"}"#);
let circle = app.tabs[app.active_tab]
.scene
.document
.entities()
.find_map(|entity| match entity {
acadrust::EntityType::Circle(circle) => Some(circle),
_ => None,
})
.expect("CIRCLE should create one entity");
let close = |a: f64, b: f64| (a - b).abs() < 1e-9;
assert!(close(circle.center.x, 2.0));
assert!(close(circle.center.y, 0.0));
assert!(close(circle.center.z, 3.0));
assert!(close(circle.normal.x, 0.0));
assert!(close(circle.normal.y, -1.0));
assert!(close(circle.normal.z, 0.0));
}
#[test] #[test]
fn value_prompt_commands_inline_args() { fn value_prompt_commands_inline_args() {
// A single-value setting command entered with its value on one line // A single-value setting command entered with its value on one line

View file

@ -1888,6 +1888,7 @@ impl OpenCADStudio {
&handles, &handles,
&crate::command::EntityTransform::Rotate { &crate::command::EntityTransform::Rotate {
center: glam::DVec3::ZERO, center: glam::DVec3::ZERO,
axis: glam::DVec3::Z,
angle_rad, angle_rad,
}, },
); );

View file

@ -610,7 +610,12 @@ impl OpenCADStudio {
if nums.len() < 3 { if nums.len() < 3 {
return Some(Task::none()); return Some(Task::none());
} }
let place = glam::DVec3::new(nums[0], nums[1], nums[2]); let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
let place = plane.to_local(glam::DVec3::new(nums[0], nums[1], nums[2]));
let handles: Vec<acadrust::Handle> = self.tabs[i] let handles: Vec<acadrust::Handle> = self.tabs[i]
.scene .scene
.selected_entities() .selected_entities()
@ -623,6 +628,9 @@ impl OpenCADStudio {
qdim_collect_points(e, &mut pts); qdim_collect_points(e, &mut pts);
} }
} }
for point in &mut pts {
*point = plane.to_local(*point);
}
if pts.len() < 2 { if pts.len() < 2 {
self.command_line self.command_line
.push_error(crate::t!("QDIM: no dimensionable endpoints in the selection.").as_ref()); .push_error(crate::t!("QDIM: no dimensionable endpoints in the selection.").as_ref());
@ -671,9 +679,10 @@ impl OpenCADStudio {
dim.definition_point = v(def); dim.definition_point = v(def);
dim.base.definition_point = v(def); dim.base.definition_point = v(def);
dim.base.actual_measurement = dim.measurement(); dim.base.actual_measurement = dim.measurement();
self.commit_entity(acadrust::EntityType::Dimension( let entity = acadrust::EntityType::Dimension(
acadrust::entities::Dimension::Linear(dim), acadrust::entities::Dimension::Linear(dim),
)); );
self.commit_entity(plane.place_entity(entity));
made += 1; made += 1;
} }
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
@ -1325,6 +1334,13 @@ fn find_last_linear_dim(
fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::DVec3>) { fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::DVec3>) {
use acadrust::EntityType as ET; use acadrust::EntityType as ET;
let p = |v: &acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z); let p = |v: &acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
let ocs = |point: (f64, f64, f64), normal: acadrust::types::Vector3| {
let world = crate::scene::view::transform::ocs_point_to_wcs(
point,
(normal.x, normal.y, normal.z),
);
glam::DVec3::new(world.0, world.1, world.2)
};
match e { match e {
ET::Line(l) => { ET::Line(l) => {
out.push(p(&l.start)); out.push(p(&l.start));
@ -1332,11 +1348,12 @@ fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::DVec3>) {
} }
ET::LwPolyline(pl) => { ET::LwPolyline(pl) => {
for v in &pl.vertices { for v in &pl.vertices {
out.push(glam::DVec3::new( out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal));
v.location.x, }
v.location.y, }
pl.elevation, ET::Polyline2D(pl) => {
)); for v in &pl.vertices {
out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal));
} }
} }
ET::Polyline(pl) => { ET::Polyline(pl) => {
@ -1346,10 +1363,13 @@ fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::DVec3>) {
} }
ET::Arc(a) => { ET::Arc(a) => {
for &ang in &[a.start_angle, a.end_angle] { for &ang in &[a.start_angle, a.end_angle] {
out.push(glam::DVec3::new( out.push(ocs(
a.center.x + a.radius * ang.cos(), (
a.center.y + a.radius * ang.sin(), a.center.x + a.radius * ang.cos(),
a.center.z, a.center.y + a.radius * ang.sin(),
a.center.z,
),
a.normal,
)); ));
} }
} }

View file

@ -220,7 +220,7 @@ impl OpenCADStudio {
self.command_line.push_error(crate::t!("Usage: UCS SAVE <name>").as_ref()); self.command_line.push_error(crate::t!("Usage: UCS SAVE <name>").as_ref());
} else { } else {
// Save the current active UCS under this name. // Save the current active UCS under this name.
let ucs = match &self.tabs[i].active_ucs { let mut ucs = match &self.tabs[i].active_ucs {
Some(u) => { Some(u) => {
let mut saved = u.clone(); let mut saved = u.clone();
saved.name = name.clone(); saved.name = name.clone();
@ -228,7 +228,23 @@ impl OpenCADStudio {
} }
None => Ucs::new(&name), // save WCS (identity) None => Ucs::new(&name), // save WCS (identity)
}; };
self.tabs[i].scene.document.ucss.add_or_replace(ucs); ucs.handle = self.tabs[i]
.scene
.document
.ucss
.get(&name)
.map(|existing| existing.handle)
.filter(|handle| !handle.is_null())
.unwrap_or_else(|| {
self.tabs[i].scene.document.allocate_handle()
});
self.tabs[i]
.scene
.document
.ucss
.add_or_replace(ucs.clone());
self.tabs[i].active_ucs = Some(ucs);
active_changed = true;
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line self.command_line
.push_output(crate::tf!("UCS '{}' saved.", name).as_ref()); .push_output(crate::tf!("UCS '{}' saved.", name).as_ref());
@ -238,7 +254,30 @@ impl OpenCADStudio {
let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
if name.is_empty() { if name.is_empty() {
self.command_line.push_error(crate::t!("Usage: UCS DELETE <name>").as_ref()); self.command_line.push_error(crate::t!("Usage: UCS DELETE <name>").as_ref());
} else if self.tabs[i].scene.document.ucss.remove(&name).is_some() { } else if let Some(removed) =
self.tabs[i].scene.document.ucss.remove(&name)
{
let removed_handle = removed.handle;
for entity in self.tabs[i].scene.document.entities_mut() {
if let acadrust::EntityType::Viewport(viewport) = entity {
if viewport.ucs_handle == removed_handle {
viewport.ucs_handle = acadrust::Handle::NULL;
}
}
}
let active_matches = self.tabs[i].active_ucs.as_ref().is_some_and(|ucs| {
(!removed_handle.is_null() && ucs.handle == removed_handle)
|| ucs.name.eq_ignore_ascii_case(&name)
});
if active_matches {
if let Some(active) = self.tabs[i].active_ucs.as_mut() {
active.name = "*ACTIVE*".to_string();
active.handle = acadrust::Handle::NULL;
active.named_ucs_handle = acadrust::Handle::NULL;
active.base_ucs_handle = acadrust::Handle::NULL;
}
active_changed = true;
}
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line self.command_line
.push_output(crate::tf!("UCS '{}' deleted.", name).as_ref()); .push_output(crate::tf!("UCS '{}' deleted.", name).as_ref());
@ -253,14 +292,73 @@ impl OpenCADStudio {
self.command_line self.command_line
.push_output(crate::t!("UCS reset to World Coordinate System.").as_ref()); .push_output(crate::t!("UCS reset to World Coordinate System.").as_ref());
} }
"VIEW" | "V" => {
let rotation = self.tabs[i].scene.active_camera_rotation();
let origin = self.tabs[i]
.active_ucs
.as_ref()
.map(|ucs| ucs.origin)
.unwrap_or(Vector3::ZERO);
let x = rotation * glam::Vec3::X;
let y = rotation * glam::Vec3::Y;
let mut ucs = Ucs::new("*VIEW*");
ucs.origin = origin;
ucs.x_axis = Vector3::new(x.x as f64, x.y as f64, x.z as f64);
ucs.y_axis = Vector3::new(y.x as f64, y.y as f64, y.z as f64);
self.tabs[i].active_ucs = Some(ucs);
active_changed = true;
self.command_line
.push_output(crate::t!("UCS aligned to the current view.").as_ref());
}
"3POINTW" => {
let raw = parts.get(2).copied().unwrap_or("");
let points: Vec<glam::DVec3> = raw
.split('|')
.filter_map(|value| {
super::super::helpers::parse_coord(value).map(|(point, _)| point)
})
.collect();
if points.len() == 3 {
let x = (points[1] - points[0]).normalize_or_zero();
let toward_y = points[2] - points[0];
let z = x.cross(toward_y).normalize_or_zero();
let y = z.cross(x).normalize_or_zero();
if x.length_squared() > 1e-12
&& y.length_squared() > 1e-12
&& z.length_squared() > 1e-12
{
let mut ucs = Ucs::new("*ACTIVE*");
ucs.origin = Vector3::new(points[0].x, points[0].y, points[0].z);
ucs.x_axis = Vector3::new(x.x, x.y, x.z);
ucs.y_axis = Vector3::new(y.x, y.y, y.z);
self.tabs[i].active_ucs = Some(ucs);
active_changed = true;
self.command_line.push_output(
crate::t!("UCS defined from three points.").as_ref(),
);
} else {
self.command_line.push_error(
crate::t!("UCS points must define two non-collinear axes.")
.as_ref(),
);
}
} else {
self.command_line.push_error(
crate::t!("UCS requires origin, X-axis point and XY-plane point.")
.as_ref(),
);
}
}
// UCS ORIGIN x,y,z — shift the active UCS origin, keep axes // UCS ORIGIN x,y,z — shift the active UCS origin, keep axes
"ORIGIN" | "O" => { "ORIGIN" | "O" | "ORIGINW" => {
let coord_str = parts.get(2).copied().unwrap_or(""); let coord_str = parts.get(2).copied().unwrap_or("");
if let Some((pt, _)) = super::super::helpers::parse_coord(coord_str) { if let Some((pt, _)) = super::super::helpers::parse_coord(coord_str) {
// `pt` is in current UCS space; convert to WCS. // `pt` is in current UCS space; convert to WCS.
// The @/# relative-coordinate prefix is ignored // The @/# relative-coordinate prefix is ignored
// here — a UCS origin is always absolute. // here — a UCS origin is always absolute.
let wcs_origin = if let Some(ref ucs) = self.tabs[i].active_ucs { let wcs_origin = if sub == "ORIGINW" {
pt
} else if let Some(ref ucs) = self.tabs[i].active_ucs {
ucs_to_wcs(pt, ucs) ucs_to_wcs(pt, ucs)
} else { } else {
pt pt
@ -401,9 +499,6 @@ impl OpenCADStudio {
} }
// ── Named Views (VIEW command) ──────────────────────────────── // ── Named Views (VIEW command) ────────────────────────────────
// PLAN — look straight down at the drawing (top view). The optional
// World/Ucs/Current keyword is accepted; all map to the world top
// view for now.
// PLAN — plan view of a UCS (#326). Shows its options right on // PLAN — plan view of a UCS (#326). Shows its options right on
// activation instead of flipping the view like a cube click; the // activation instead of flipping the view like a cube click; the
// view only changes once an option (or the Current default) runs. // view only changes once an option (or the Current default) runs.

View file

@ -1787,6 +1787,14 @@ impl OpenCADStudio {
{ {
self.tabs[i].active_layer = new_name.clone(); self.tabs[i].active_layer = new_name.clone();
} }
if type_str == "UCS" {
if let Some(active) = self.tabs[i].active_ucs.as_mut() {
if active.name.eq_ignore_ascii_case(&old_name) {
active.name = new_name.clone();
self.tabs[i].persist_active_ucs();
}
}
}
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line self.command_line
.push_output(crate::tf!("RENAME: '{}' → '{}'.", old_name, new_name).as_ref()); .push_output(crate::tf!("RENAME: '{}' → '{}'.", old_name, new_name).as_ref());
@ -2357,7 +2365,15 @@ fn rename_symbol(doc: &mut acadrust::CadDocument, ty: &str, old: &str, new: &str
} }
true true
} }
"UCS" => rekey(&mut doc.ucss, old, new), "UCS" => {
if !rekey(&mut doc.ucss, old, new) {
return false;
}
if doc.header.model_space_ucs_name.eq_ignore_ascii_case(old) {
doc.header.model_space_ucs_name = new.to_string();
}
true
}
"VIEW" => rekey(&mut doc.views, old, new), "VIEW" => rekey(&mut doc.views, old, new),
_ => false, _ => false,
} }

View file

@ -257,11 +257,22 @@ impl DocumentTab {
/// is identity (plain WCS). /// is identity (plain WCS).
pub(super) fn model_ucs_from_header(&self) -> Option<Ucs> { pub(super) fn model_ucs_from_header(&self) -> Option<Ucs> {
let h = &self.scene.document.header; let h = &self.scene.document.header;
let mut u = Ucs::new(h.model_space_ucs_name.clone()); let mut u = if h.model_space_ucs_name.is_empty() {
Ucs::new("*ACTIVE*")
} else {
self.scene
.document
.ucss
.get(&h.model_space_ucs_name)
.cloned()
.unwrap_or_else(|| Ucs::new(h.model_space_ucs_name.clone()))
};
u.origin = h.model_space_ucs_origin; u.origin = h.model_space_ucs_origin;
u.x_axis = h.model_space_ucs_x_axis; u.x_axis = h.model_space_ucs_x_axis;
u.y_axis = h.model_space_ucs_y_axis; u.y_axis = h.model_space_ucs_y_axis;
if super::helpers::UcsXform::from_ucs(&u).is_identity() { if h.model_space_ucs_name.is_empty()
&& super::helpers::UcsXform::from_ucs(&u).is_identity()
{
None None
} else { } else {
Some(u) Some(u)
@ -278,11 +289,21 @@ impl DocumentTab {
if !vp.ucs_per_viewport { if !vp.ucs_per_viewport {
return None; return None;
} }
let mut u = Ucs::new("*VPUCS*"); let mut u = if vp.ucs_handle.is_null() {
Ucs::new("*VPUCS*")
} else {
self.scene
.document
.ucss
.iter()
.find(|ucs| ucs.handle == vp.ucs_handle)
.cloned()
.unwrap_or_else(|| Ucs::new("*VPUCS*"))
};
u.origin = vp.ucs_origin; u.origin = vp.ucs_origin;
u.x_axis = vp.ucs_x_axis; u.x_axis = vp.ucs_x_axis;
u.y_axis = vp.ucs_y_axis; u.y_axis = vp.ucs_y_axis;
if super::helpers::UcsXform::from_ucs(&u).is_identity() { if vp.ucs_handle.is_null() && super::helpers::UcsXform::from_ucs(&u).is_identity() {
None None
} else { } else {
Some(u) Some(u)
@ -369,34 +390,6 @@ impl DocumentTab {
self.scene.viewcube_ucs = self.ucs_xform().rotation_mat(); self.scene.viewcube_ucs = self.ucs_xform().rotation_mat();
} }
/// UCS→render(wire)-space affine for commands that build axis-aligned
/// geometry. Columns are the UCS axes; translation is the UCS origin in wire
/// space. Identity outside model space (no UCS there).
pub(super) fn ucs_wire_affine(&self) -> glam::Mat4 {
if !self.editing_model_space() {
return glam::Mat4::IDENTITY;
}
let (o, x, y, z) = self.ucs_xform().axes();
let origin = glam::Vec3::new(o.x as f32, o.y as f32, o.z as f32);
glam::Mat4::from_cols(
x.as_vec3().extend(0.0),
y.as_vec3().extend(0.0),
z.as_vec3().extend(0.0),
origin.extend(1.0),
)
}
/// World-space rotation angle (radians) of the active UCS X axis — the
/// default rotation for new text-bearing objects so their text aligns to
/// the user's coordinate system. Zero outside model space / with no UCS.
pub(super) fn ucs_rotation_angle(&self) -> f64 {
if !self.editing_model_space() {
return 0.0;
}
let (_, x, ..) = self.ucs_xform().axes();
(x.y as f64).atan2(x.x as f64)
}
/// Grid origin (render/wire space) and UCS→world rotation for grid snap and /// Grid origin (render/wire space) and UCS→world rotation for grid snap and
/// the grid overlay. Identity / origin-at-zero outside model space. /// the grid overlay. Identity / origin-at-zero outside model space.
pub(super) fn ucs_grid_basis(&self) -> (glam::Vec3, glam::Mat4) { pub(super) fn ucs_grid_basis(&self) -> (glam::Vec3, glam::Mat4) {

View file

@ -5,7 +5,7 @@ use acadrust::tables::Ucs;
/// How a typed coordinate should be interpreted relative to the last /// How a typed coordinate should be interpreted relative to the last
/// input point. /// input point.
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum CoordKind { pub(crate) enum CoordKind {
/// `@x,y` prefix — offset from the last input point. /// `@x,y` prefix — offset from the last input point.
Relative, Relative,
/// `#x,y` prefix — world/UCS absolute, overriding DYN. /// `#x,y` prefix — world/UCS absolute, overriding DYN.
@ -14,12 +14,13 @@ pub(super) enum CoordKind {
Default, Default,
} }
/// Parse a typed coordinate string into a Vec3 plus its interpretation. /// Parse a typed coordinate string into a local Vec3 plus its interpretation.
/// Accepts "x,y" → Vec3(x, y, 0) /// Accepts Cartesian, polar, cylindrical and spherical forms:
/// "x,y,z" → Vec3(x, y, z) /// `x,y`, `x,y,z`, `distance<angle`, `distance<angle,z`, and
/// `distance<angle<elevation`.
/// A leading `@` marks the value relative to the last point; a leading /// A leading `@` marks the value relative to the last point; a leading
/// `#` forces absolute. Separators: comma or semicolon. /// `#` forces absolute. Separators: comma or semicolon.
pub(super) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> { pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
let trimmed = text.trim(); let trimmed = text.trim();
let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') { let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') {
(CoordKind::Relative, r) (CoordKind::Relative, r)
@ -28,6 +29,34 @@ pub(super) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
} else { } else {
(CoordKind::Default, trimmed) (CoordKind::Default, trimmed)
}; };
let number = |value: &str| value.trim().parse::<f64>().ok();
if let Some((distance, angles)) = rest.split_once('<') {
let distance = number(distance)?;
if let Some((azimuth, elevation)) = angles.split_once('<') {
let azimuth = number(azimuth)?.to_radians();
let elevation = number(elevation)?.to_radians();
let horizontal = distance * elevation.cos();
return Some((
glam::DVec3::new(
horizontal * azimuth.cos(),
horizontal * azimuth.sin(),
distance * elevation.sin(),
),
kind,
));
}
let angle_and_z: Vec<&str> = angles.split(|c| c == ',' || c == ';').collect();
let (angle, z) = match angle_and_z.as_slice() {
[angle] => (number(angle)?, 0.0),
[angle, z] => (number(angle)?, number(z)?),
_ => return None,
};
let angle = angle.to_radians();
return Some((
glam::DVec3::new(distance * angle.cos(), distance * angle.sin(), z),
kind,
));
}
let parts: Vec<f64> = rest let parts: Vec<f64> = rest
.split(|c| c == ',' || c == ';') .split(|c| c == ',' || c == ';')
.map(|s| s.trim()) .map(|s| s.trim())
@ -40,6 +69,23 @@ pub(super) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
} }
} }
#[cfg(test)]
mod coordinate_parsing_tests {
use super::*;
#[test]
fn parses_all_coordinate_forms() {
let close = |a: glam::DVec3, b: glam::DVec3| (a - b).length() < 1e-9;
assert!(close(parse_coord("1,2").unwrap().0, glam::dvec3(1.0, 2.0, 0.0)));
assert!(close(parse_coord("1,2,3").unwrap().0, glam::dvec3(1.0, 2.0, 3.0)));
assert!(close(parse_coord("10<90").unwrap().0, glam::dvec3(0.0, 10.0, 0.0)));
assert!(close(parse_coord("10<90,4").unwrap().0, glam::dvec3(0.0, 10.0, 4.0)));
assert!(close(parse_coord("10<0<30").unwrap().0, glam::dvec3(5.0 * 3.0_f64.sqrt(), 0.0, 5.0)));
assert_eq!(parse_coord("@10<0").unwrap().1, CoordKind::Relative);
assert_eq!(parse_coord("#1,2").unwrap().1, CoordKind::Absolute);
}
}
// ── UCS ↔ WCS converter ───────────────────────────────────────────────────── // ── UCS ↔ WCS converter ─────────────────────────────────────────────────────
/// The single bridge between WCS (how geometry and the file are stored) and the /// The single bridge between WCS (how geometry and the file are stored) and the
@ -122,6 +168,10 @@ impl UcsXform {
(self.origin, self.x, self.y, self.z) (self.origin, self.x, self.y, self.z)
} }
pub(super) fn working_plane(&self) -> crate::command::WorkingPlane {
crate::command::WorkingPlane::new(self.origin, self.x, self.y)
}
/// UCS→world rotation matrix (columns = UCS axes). For consumers that take /// UCS→world rotation matrix (columns = UCS axes). For consumers that take
/// a `Mat4` rotation directly (ViewCube, OTRACK ray directions). The GPU / /// a `Mat4` rotation directly (ViewCube, OTRACK ray directions). The GPU /
/// screen layer is f32, so the axes downcast here at the boundary. /// screen layer is f32, so the axes downcast here at the boundary.

View file

@ -9,7 +9,7 @@ pub(crate) mod commands;
mod document; mod document;
mod expr_eval; mod expr_eval;
mod find_replace; mod find_replace;
mod helpers; pub(crate) mod helpers;
mod history; mod history;
mod layers; mod layers;
mod model_ops; mod model_ops;

View file

@ -1146,10 +1146,24 @@ impl super::OpenCADStudio {
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]); .bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
} else { } else {
// Align new MText to the active UCS (text runs along the UCS X axis). let plane = if self.tabs[i].editing_model_space() {
mt.rotation = self.tabs[i].ucs_rotation_angle(); self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
let position = plane.to_local(glam::DVec3::new(
mt.insertion_point.x,
mt.insertion_point.y,
mt.insertion_point.z,
));
mt.insertion_point = acadrust::types::Vector3::new(
position.x,
position.y,
position.z,
);
mt.rotation = 0.0;
self.push_undo_snapshot(i, "MTEXT"); self.push_undo_snapshot(i, "MTEXT");
let handle = self.commit_entity_handle(EntityType::MText(mt)); let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
if annotative { if annotative {
let scale = self.tabs[i].scene.current_annotation_scale_handle(); let scale = self.tabs[i].scene.current_annotation_scale_handle();
if let (Some(handle), Some(scale)) = (handle, scale) { if let (Some(handle), Some(scale)) = (handle, scale) {
@ -1217,9 +1231,24 @@ impl super::OpenCADStudio {
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]); .bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
} else { } else {
mt.rotation = self.tabs[i].ucs_rotation_angle(); let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
let position = plane.to_local(glam::DVec3::new(
mt.insertion_point.x,
mt.insertion_point.y,
mt.insertion_point.z,
));
mt.insertion_point = acadrust::types::Vector3::new(
position.x,
position.y,
position.z,
);
mt.rotation = 0.0;
self.push_undo_snapshot(i, "MTEXT"); self.push_undo_snapshot(i, "MTEXT");
let handle = self.commit_entity_handle(EntityType::MText(mt)); let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
if annotative { if annotative {
let scale = self.tabs[i].scene.current_annotation_scale_handle(); let scale = self.tabs[i].scene.current_annotation_scale_handle();

View file

@ -375,10 +375,31 @@ impl OpenCADStudio {
source_entity, source_entity,
annotation_scale_handle, annotation_scale_handle,
); );
let entity = contextual.as_ref(); let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane);
let entity = &display_entity;
let group_names = self.tabs[i].scene.group_names_for_entity(handle); let group_names = self.tabs[i].scene.group_names_for_entity(handle);
let mut sections = let mut sections =
dispatch::properties_sectioned(handle, entity, &text_style_names); dispatch::properties_sectioned(handle, entity, &text_style_names);
sections.insert(
0,
crate::scene::model::object::PropSection {
title: t!("Coordinates").into_owned(),
props: vec![crate::entities::common::ro_prop(
t!("Coordinate system").as_ref(),
"coordinate_system",
self.tabs[i]
.active_ucs
.as_ref()
.map(|ucs| ucs.name.clone())
.unwrap_or_else(|| "WCS".to_string()),
)],
},
);
// Turn the Material row into an editable picker: the source // Turn the Material row into an editable picker: the source
// options (ByLayer / ByBlock) plus every named material the // options (ByLayer / ByBlock) plus every named material the
@ -1528,7 +1549,37 @@ impl OpenCADStudio {
}) })
.unwrap_or_default(); .unwrap_or_default();
let sections = aggregate_sections(&filtered, &text_style_names); let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
let local_entities: Vec<(Handle, EntityType)> = filtered
.iter()
.map(|(handle, entity)| {
(*handle, dispatch::entity_in_working_plane(entity, plane))
})
.collect();
let local_refs: Vec<(Handle, &EntityType)> = local_entities
.iter()
.map(|(handle, entity)| (*handle, entity))
.collect();
let mut sections = aggregate_sections(&local_refs, &text_style_names);
sections.insert(
0,
crate::scene::model::object::PropSection {
title: t!("Coordinates").into_owned(),
props: vec![crate::entities::common::ro_prop(
t!("Coordinate system").as_ref(),
"coordinate_system",
self.tabs[i]
.active_ucs
.as_ref()
.map(|ucs| ucs.name.clone())
.unwrap_or_else(|| "WCS".to_string()),
)],
},
);
ui::PropertiesPanel { ui::PropertiesPanel {
choice_combos: sections choice_combos: sections
.iter() .iter()

View file

@ -211,9 +211,15 @@ impl super::OpenCADStudio {
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]); .bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
} else { } else {
let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
let position = plane.to_local(ed.pos);
let mut t = Text::with_value( let mut t = Text::with_value(
&ed.value, &ed.value,
Vector3::new(ed.pos.x, ed.pos.y, ed.pos.z), Vector3::new(position.x, position.y, position.z),
) )
.with_height(ed.height); .with_height(ed.height);
// New text inherits the document's current text style (STYLE), not // New text inherits the document's current text style (STYLE), not
@ -231,10 +237,8 @@ impl super::OpenCADStudio {
&self.tabs[i].scene.document, &self.tabs[i].scene.document,
&t.style, &t.style,
); );
// Align new text to the active UCS (baseline along the UCS X axis).
t.rotation = self.tabs[i].ucs_rotation_angle();
self.push_undo_snapshot(i, "TEXT"); self.push_undo_snapshot(i, "TEXT");
let handle = self.commit_entity_handle(EntityType::Text(t)); let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t)));
if annotative { if annotative {
let scale = self.tabs[i].scene.current_annotation_scale_handle(); let scale = self.tabs[i].scene.current_annotation_scale_handle();
if let (Some(handle), Some(scale)) = (handle, scale) { if let (Some(handle), Some(scale)) = (handle, scale) {

View file

@ -417,8 +417,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
// independent of DYN: `@` forces relative, `#` forces // independent of DYN: `@` forces relative, `#` forces
// absolute, and a bare value is absolute. (Relative-by- // absolute, and a bare value is absolute. (Relative-by-
// default lives in the DYN tooltip path — see // default lives in the DYN tooltip path — see
// `dyn_resolve_point` — matching AutoCAD, where the // `dyn_resolve_point`; command-line coordinates stay
// command line stays absolute regardless of DYN.) // absolute regardless of the DYN setting.)
let want_relative = matches!(kind, CoordKind::Relative); let want_relative = matches!(kind, CoordKind::Relative);
let ucs = self.tabs[i].active_ucs.clone(); let ucs = self.tabs[i].active_ucs.clone();
let wcs_pt = match (want_relative, self.last_point) { let wcs_pt = match (want_relative, self.last_point) {
@ -474,7 +474,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
} }
self.command_line.push_error(crate::tf!( self.command_line.push_error(crate::tf!(
"Expected coordinates (x,y) or a number, got: \"{text}\"" "Expected Cartesian, polar, cylindrical or spherical coordinates, or a number; got: \"{text}\""
).as_ref()); ).as_ref());
return self.focus_cmd_input(); return self.focus_cmd_input();
} }
@ -1874,10 +1874,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
} }
} }
} else { } else {
let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
for &handle in &handles { for &handle in &handles {
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
{ {
crate::scene::view::dispatch::apply_geom_prop(entity, field, &value); crate::scene::view::dispatch::apply_geom_prop_in_working_plane(
entity,
field,
&value,
plane,
);
} }
} }
} }
@ -1936,6 +1946,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
crate::scene::view::dispatch::set_prop_current_vertex( crate::scene::view::dispatch::set_prop_current_vertex(
self.tabs[i].properties.prop_vertex, self.tabs[i].properties.prop_vertex,
); );
let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
for &handle in &handles { for &handle in &handles {
// Skip objects on a locked layer. // Skip objects on a locked layer.
if self.tabs[i].scene.is_layer_locked(handle) { if self.tabs[i].scene.is_layer_locked(handle) {
@ -2007,8 +2022,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
if let Some(entity) = if let Some(entity) =
self.tabs[i].scene.document.get_entity_mut(handle) self.tabs[i].scene.document.get_entity_mut(handle)
{ {
crate::scene::view::dispatch::apply_geom_prop( crate::scene::view::dispatch::apply_geom_prop_in_working_plane(
entity, field, &val, entity,
field,
&val,
plane,
); );
} }
} }

View file

@ -188,13 +188,15 @@ impl OpenCADStudio {
/// values. Locked fields use their typed buffer; the rest fall back to /// values. Locked fields use their typed buffer; the rest fall back to
/// the live cursor-derived value. Returns `None` when the field set /// the live cursor-derived value. Returns `None` when the field set
/// isn't one we know how to turn into a point. /// isn't one we know how to turn into a point.
/// Hand the active command the current UCS (as a UCS→wire affine) so /// Hand the active command the current full-precision coordinate frame.
/// axis-aligned constructions build square to the user's coordinate system.
/// No-op for commands that don't override `set_ucs`.
pub(in crate::app) fn push_ucs_to_cmd(&mut self, i: usize) { pub(in crate::app) fn push_ucs_to_cmd(&mut self, i: usize) {
let ucs = self.tabs[i].ucs_wire_affine(); let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
if let Some(c) = self.tabs[i].active_cmd.as_mut() { if let Some(c) = self.tabs[i].active_cmd.as_mut() {
c.set_ucs(ucs); c.set_working_plane(plane);
} }
} }

View file

@ -2086,7 +2086,11 @@ impl OpenCADStudio {
match (a, b) { match (a, b) {
(None, None) => true, (None, None) => true,
(Some(a), Some(b)) => { (Some(a), Some(b)) => {
a.origin == b.origin && a.x_axis == b.x_axis && a.y_axis == b.y_axis a.origin == b.origin
&& a.x_axis == b.x_axis
&& a.y_axis == b.y_axis
&& a.handle == b.handle
&& a.name.eq_ignore_ascii_case(&b.name)
} }
_ => false, _ => false,
} }

View file

@ -11,6 +11,132 @@ use crate::scene::Scene;
use acadrust::{EntityType, Handle}; use acadrust::{EntityType, Handle};
use glam::DVec3; use glam::DVec3;
// ── Working plane ─────────────────────────────────────────────────────────
/// Full-precision coordinate frame used by interactive commands.
///
/// Points delivered to commands remain WCS points. Commands that construct
/// planar geometry use this frame for their local calculations, then convert
/// the result back to WCS for storage and preview.
#[derive(Clone, Copy, Debug)]
pub struct WorkingPlane {
pub origin: DVec3,
pub x: DVec3,
pub y: DVec3,
pub z: DVec3,
}
impl Default for WorkingPlane {
fn default() -> Self {
Self {
origin: DVec3::ZERO,
x: DVec3::X,
y: DVec3::Y,
z: DVec3::Z,
}
}
}
impl WorkingPlane {
pub fn is_identity(self) -> bool {
self.origin.abs_diff_eq(DVec3::ZERO, 1e-12)
&& self.x.abs_diff_eq(DVec3::X, 1e-12)
&& self.y.abs_diff_eq(DVec3::Y, 1e-12)
&& self.z.abs_diff_eq(DVec3::Z, 1e-12)
}
pub fn new(origin: DVec3, x: DVec3, y: DVec3) -> Self {
let x = x.normalize_or(DVec3::X);
let raw_y = y.normalize_or(DVec3::Y);
let fallback = if x.dot(DVec3::Z).abs() < 0.999 {
DVec3::Z
} else {
DVec3::Y
};
let z = x.cross(raw_y).normalize_or(x.cross(fallback).normalize());
let y = z.cross(x).normalize();
Self { origin, x, y, z }
}
pub fn to_world(self, point: DVec3) -> DVec3 {
self.origin + self.x * point.x + self.y * point.y + self.z * point.z
}
pub fn to_local(self, point: DVec3) -> DVec3 {
let delta = point - self.origin;
DVec3::new(delta.dot(self.x), delta.dot(self.y), delta.dot(self.z))
}
pub fn vector_to_world(self, vector: DVec3) -> DVec3 {
self.x * vector.x + self.y * vector.y + self.z * vector.z
}
pub fn vector_to_local(self, vector: DVec3) -> DVec3 {
DVec3::new(vector.dot(self.x), vector.dot(self.y), vector.dot(self.z))
}
pub fn angle(self, from: DVec3, to: DVec3) -> Option<f64> {
let direction = self.vector_to_local(to - from);
(direction.x.hypot(direction.y) > f64::EPSILON)
.then(|| direction.y.atan2(direction.x))
}
pub fn to_world_transform(self) -> acadrust::types::Transform {
use acadrust::types::{Matrix4, Transform};
Transform::from_matrix(Matrix4 {
m: [
[self.x.x, self.y.x, self.z.x, self.origin.x],
[self.x.y, self.y.y, self.z.y, self.origin.y],
[self.x.z, self.y.z, self.z.z, self.origin.z],
[0.0, 0.0, 0.0, 1.0],
],
})
}
pub fn to_local_transform(self) -> acadrust::types::Transform {
use acadrust::types::{Matrix4, Transform};
Transform::from_matrix(Matrix4 {
m: [
[self.x.x, self.x.y, self.x.z, -self.origin.dot(self.x)],
[self.y.x, self.y.y, self.y.z, -self.origin.dot(self.y)],
[self.z.x, self.z.y, self.z.z, -self.origin.dot(self.z)],
[0.0, 0.0, 0.0, 1.0],
],
})
}
pub fn place_entity(self, mut entity: EntityType) -> EntityType {
crate::scene::view::dispatch::apply_transform(
&mut entity,
&EntityTransform::Affine(self.to_world_transform()),
);
entity
}
}
#[cfg(test)]
mod working_plane_tests {
use super::*;
#[test]
fn translated_and_rotated_frame_round_trips_points_and_vectors() {
let plane = WorkingPlane::new(
DVec3::new(125_000.25, -42_000.5, 810.75),
DVec3::new(0.0, 1.0, 0.0),
DVec3::new(0.0, 0.0, 1.0),
);
let local = DVec3::new(12.5, -3.25, 7.75);
let vector = DVec3::new(-2.0, 5.0, 1.5);
assert!(plane
.to_local(plane.to_world(local))
.abs_diff_eq(local, 1e-9));
assert!(plane
.vector_to_local(plane.vector_to_world(vector))
.abs_diff_eq(vector, 1e-12));
}
}
/// Domain object resolved under the cursor for ObjectPick snapping. /// Domain object resolved under the cursor for ObjectPick snapping.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct ObjectPickHit { pub struct ObjectPickHit {
@ -44,14 +170,22 @@ pub struct AreaPreviewRegion {
/// A geometric transformation applied to existing entities. /// A geometric transformation applied to existing entities.
#[derive(Clone)] #[derive(Clone)]
pub enum EntityTransform { pub enum EntityTransform {
/// Move every point by the given world-space delta (world XY plane). /// Move every point by the given world-space delta.
Translate(DVec3), Translate(DVec3),
/// Rotate around `center` by `angle_rad` in the world XY plane. /// Rotate around the axis through `center`.
Rotate { center: DVec3, angle_rad: f64 }, Rotate {
center: DVec3,
axis: DVec3,
angle_rad: f64,
},
/// Uniform scale from `center` by `factor`. /// Uniform scale from `center` by `factor`.
Scale { center: DVec3, factor: f64 }, Scale { center: DVec3, factor: f64 },
/// Mirror across the line through `p1`→`p2` in the world XY plane. /// Mirror through the plane containing `p1`→`p2` and the working normal.
Mirror { p1: DVec3, p2: DVec3 }, Mirror {
p1: DVec3,
p2: DVec3,
working_normal: DVec3,
},
/// General affine transform. Used when a complete UCS basis must be baked /// General affine transform. Used when a complete UCS basis must be baked
/// into block-local geometry instead of stored as drawing UCS state. /// into block-local geometry instead of stored as drawing UCS state.
Affine(acadrust::types::Transform), Affine(acadrust::types::Transform),
@ -1296,12 +1430,10 @@ pub trait CadCommand: Send {
Vec::new() Vec::new()
} }
/// Push the active UCS into the command as a UCS→render(wire)-space affine /// Push the active coordinate frame in full precision. Geometry commands
/// (identity = plain WCS). Commands that build axis-aligned geometry (RECT, /// use it for plane-local construction; inquiry and modify commands use it
/// rectangular ARRAY, …) override this to store it and rotate their implicit /// for local deltas, angles and transformation axes.
/// axes into the UCS; most commands work purely from picked points and fn set_working_plane(&mut self, _plane: WorkingPlane) {}
/// ignore it. Called before each point / preview dispatch.
fn set_ucs(&mut self, _ucs: glam::Mat4) {}
/// Push the live Ctrl-key state into the command before each preview/commit /// Push the live Ctrl-key state into the command before each preview/commit
/// dispatch. Commands that offer a Ctrl toggle (e.g. arc-direction flip on /// dispatch. Commands that offer a Ctrl toggle (e.g. arc-direction flip on

View file

@ -628,14 +628,34 @@ fn apply_transform(dim: &mut Dimension, t: &EntityTransform) {
EntityTransform::Translate(d) => dim.translate(acadrust::types::Vector3::new( EntityTransform::Translate(d) => dim.translate(acadrust::types::Vector3::new(
d.x as f64, d.y as f64, d.z as f64, d.x as f64, d.y as f64, d.z as f64,
)), )),
EntityTransform::Rotate { center, angle_rad } => { EntityTransform::Rotate { center, axis, angle_rad } => {
transform_dimension_points(dim, |pt| rotate_point(pt, *center, *angle_rad)) if axis.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) {
transform_dimension_points(dim, |pt| rotate_point(pt, *center, *angle_rad))
} else {
crate::scene::view::transform::apply_standard_transform(
dim,
*center,
*axis,
*angle_rad,
);
}
} }
EntityTransform::Scale { center, factor } => { EntityTransform::Scale { center, factor } => {
transform_dimension_points(dim, |pt| scale_point(pt, *center, *factor)) transform_dimension_points(dim, |pt| scale_point(pt, *center, *factor))
} }
EntityTransform::Mirror { p1, p2 } => { EntityTransform::Mirror { p1, p2, working_normal } => {
transform_dimension_points(dim, |pt| mirror_point(pt, *p1, *p2)) if working_normal.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) {
transform_dimension_points(dim, |pt| mirror_point(pt, *p1, *p2))
} else {
acadrust::Entity::apply_transform(
dim,
&crate::scene::view::transform::reflection_about_working_line(
*p1,
*p2,
*working_normal,
),
);
}
} }
EntityTransform::Affine(transform) => { EntityTransform::Affine(transform) => {
let old_normal = dim.base().normal; let old_normal = dim.base().normal;

View file

@ -1305,9 +1305,12 @@ fn entity_transform(transform: &EntityTransform) -> Transform {
EntityTransform::Translate(delta) => { EntityTransform::Translate(delta) => {
Transform::from_translation(Vector3::new(delta.x, delta.y, delta.z)) Transform::from_translation(Vector3::new(delta.x, delta.y, delta.z))
} }
EntityTransform::Rotate { center, angle_rad } => { EntityTransform::Rotate { center, axis, angle_rad } => {
Transform::from_translation(Vector3::new(-center.x, -center.y, -center.z)) Transform::from_translation(Vector3::new(-center.x, -center.y, -center.z))
.then(&Transform::from_rotation(Vector3::UNIT_Z, *angle_rad)) .then(&Transform::from_rotation(
Vector3::new(axis.x, axis.y, axis.z),
*angle_rad,
))
.then(&Transform::from_translation(Vector3::new( .then(&Transform::from_translation(Vector3::new(
center.x, center.y, center.z, center.x, center.y, center.z,
))) )))
@ -1316,8 +1319,12 @@ fn entity_transform(transform: &EntityTransform) -> Transform {
Vector3::new(*factor, *factor, *factor), Vector3::new(*factor, *factor, *factor),
Vector3::new(center.x, center.y, center.z), Vector3::new(center.x, center.y, center.z),
), ),
EntityTransform::Mirror { p1, p2 } => { EntityTransform::Mirror { p1, p2, working_normal } => {
crate::scene::view::transform::reflection_about_xy_line(*p1, *p2) crate::scene::view::transform::reflection_about_working_line(
*p1,
*p2,
*working_normal,
)
} }
EntityTransform::Affine(transform) => *transform, EntityTransform::Affine(transform) => *transform,
} }

View file

@ -145,14 +145,21 @@ fn apply_transform(line: &mut Line, t: &EntityTransform) {
d.x as f64, d.y as f64, d.z as f64, d.x as f64, d.y as f64, d.z as f64,
)); ));
} }
EntityTransform::Rotate { center, angle_rad } => { EntityTransform::Rotate { center, axis, angle_rad } => {
crate::scene::view::transform::apply_standard_transform(line, *center, *angle_rad); crate::scene::view::transform::apply_standard_transform(line, *center, *axis, *angle_rad);
} }
EntityTransform::Scale { center, factor } => { EntityTransform::Scale { center, factor } => {
crate::scene::view::transform::apply_standard_scale(line, *center, *factor); crate::scene::view::transform::apply_standard_scale(line, *center, *factor);
} }
EntityTransform::Mirror { p1, p2 } => { EntityTransform::Mirror { p1, p2, working_normal } => {
crate::scene::view::transform::mirror_xy_line(line, *p1, *p2); acadrust::Entity::apply_transform(
line,
&crate::scene::view::transform::reflection_about_working_line(
*p1,
*p2,
*working_normal,
),
);
} }
EntityTransform::Affine(transform) => { EntityTransform::Affine(transform) => {
acadrust::Entity::apply_transform(line, transform); acadrust::Entity::apply_transform(line, transform);

View file

@ -6,6 +6,7 @@
use acadrust::entities::{Underlay, UnderlayDisplayFlags}; use acadrust::entities::{Underlay, UnderlayDisplayFlags};
use crate::t; use crate::t;
use glam::DVec3;
use crate::command::EntityTransform; use crate::command::EntityTransform;
use crate::entities::common::{center_grip, edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::common::{center_grip, edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip};
@ -383,7 +384,18 @@ impl Transformable for Underlay {
self.insertion_point.y += d.y as f64; self.insertion_point.y += d.y as f64;
self.insertion_point.z += d.z as f64; self.insertion_point.z += d.z as f64;
} }
EntityTransform::Mirror { p1, p2 } => { EntityTransform::Mirror { p1, p2, working_normal } => {
if !working_normal.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) {
acadrust::Entity::apply_transform(
self,
&crate::scene::view::transform::reflection_about_working_line(
*p1,
*p2,
*working_normal,
),
);
return;
}
reflect_xy_point( reflect_xy_point(
&mut self.insertion_point.x, &mut self.insertion_point.x,
&mut self.insertion_point.y, &mut self.insertion_point.y,
@ -408,7 +420,16 @@ impl Transformable for Underlay {
self.y_scale *= f; self.y_scale *= f;
self.z_scale *= f; self.z_scale *= f;
} }
EntityTransform::Rotate { center, angle_rad } => { EntityTransform::Rotate { center, axis, angle_rad } => {
if !axis.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) {
crate::scene::view::transform::apply_standard_transform(
self,
*center,
*axis,
*angle_rad,
);
return;
}
let bx = center.x as f64; let bx = center.x as f64;
let by = center.y as f64; let by = center.y as f64;
let a = *angle_rad as f64; let a = *angle_rad as f64;

View file

@ -5,7 +5,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::DVec3; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::t; use crate::t;
@ -29,6 +29,7 @@ enum Step {
pub struct AlignedDimensionCommand { pub struct AlignedDimensionCommand {
step: Step, step: Step,
plane: WorkingPlane,
/// Optional text that replaces the measured value (None = measurement). /// Optional text that replaces the measured value (None = measurement).
text_override: Option<String>, text_override: Option<String>,
/// True while the next typed line is captured as the text override. /// True while the next typed line is captured as the text override.
@ -43,6 +44,7 @@ impl AlignedDimensionCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: Step::First, step: Step::First,
plane: WorkingPlane::default(),
text_override: None, text_override: None,
awaiting_text: false, awaiting_text: false,
text_angle: None, text_angle: None,
@ -52,6 +54,10 @@ impl AlignedDimensionCommand {
} }
impl CadCommand for AlignedDimensionCommand { impl CadCommand for AlignedDimensionCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"DIMALIGNED" "DIMALIGNED"
} }
@ -85,6 +91,9 @@ impl CadCommand for AlignedDimensionCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::DimLine { p1, p2 } => { Step::DimLine { p1, p2 } => {
let p1 = self.plane.to_local(p1);
let p2 = self.plane.to_local(p2);
let pt = self.plane.to_local(pt);
let mut dim = DimensionAligned::new(v3(p1), v3(p2)); let mut dim = DimensionAligned::new(v3(p1), v3(p2));
// The dimension line runs through the cursor: store it as the // The dimension line runs through the cursor: store it as the
// definition point and let the renderer project it onto the // definition point and let the renderer project it onto the
@ -106,7 +115,9 @@ impl CadCommand for AlignedDimensionCommand {
if let Some(a) = self.text_angle { if let Some(a) = self.text_angle {
dim.base.text_rotation = a; dim.base.text_rotation = a;
} }
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Aligned(dim))) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension(
Dimension::Aligned(dim),
)))
} }
} }
} }
@ -180,7 +191,29 @@ impl CadCommand for AlignedDimensionCommand {
Step::First => return None, Step::First => return None,
Step::Second(p1) => (p1, pt), Step::Second(p1) => (p1, pt),
Step::DimLine { p1, p2 } => { Step::DimLine { p1, p2 } => {
return Some(preview_aligned(p1, p2, pt)); let p1 = self.plane.to_local(p1);
let p2 = self.plane.to_local(p2);
let pt = self.plane.to_local(pt);
let mut preview = preview_aligned(p1, p2, pt);
preview.points = preview
.points
.iter()
.map(|point| {
if point[0].is_nan() {
*point
} else {
self.plane
.to_world(DVec3::new(
point[0] as f64,
point[1] as f64,
point[2] as f64,
))
.as_vec3()
.to_array()
}
})
.collect();
return Some(preview);
} }
}; };
// Preview WireModel points are screen/GPU-side: downcast to f32. // Preview WireModel points are screen/GPU-side: downcast to f32.

View file

@ -2,7 +2,7 @@ use acadrust::entities::{Dimension, DimensionAngular3Pt};
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
@ -35,15 +35,23 @@ enum Step {
pub struct AngularDimensionCommand { pub struct AngularDimensionCommand {
step: Step, step: Step,
plane: WorkingPlane,
} }
impl AngularDimensionCommand { impl AngularDimensionCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { step: Step::Vertex } Self {
step: Step::Vertex,
plane: WorkingPlane::default(),
}
} }
} }
impl CadCommand for AngularDimensionCommand { impl CadCommand for AngularDimensionCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"DIMANGULAR" "DIMANGULAR"
} }
@ -84,13 +92,19 @@ impl CadCommand for AngularDimensionCommand {
first, first,
second, second,
} => { } => {
let vertex = self.plane.to_local(vertex);
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let pt = self.plane.to_local(pt);
let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second)); let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second));
dim.definition_point = v3(pt); dim.definition_point = v3(pt);
dim.base.definition_point = v3(pt); dim.base.definition_point = v3(pt);
dim.base.text_middle_point = v3(pt); dim.base.text_middle_point = v3(pt);
dim.base.insertion_point = v3(pt); dim.base.insertion_point = v3(pt);
dim.base.actual_measurement = dim.measurement_degrees(); dim.base.actual_measurement = dim.measurement_degrees();
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Angular3Pt(dim))) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension(
Dimension::Angular3Pt(dim),
)))
} }
} }
} }
@ -119,12 +133,24 @@ impl CadCommand for AngularDimensionCommand {
vertex, vertex,
first, first,
second, second,
} => Some(preview_wire(angular_preview( } => {
vertex.as_vec3(), let points = angular_preview(
first.as_vec3(), self.plane.to_local(vertex).as_vec3(),
second.as_vec3(), self.plane.to_local(first).as_vec3(),
pt, self.plane.to_local(second).as_vec3(),
))), self.plane.to_local(pt.as_dvec3()).as_vec3(),
)
.into_iter()
.map(|point| {
if point.x.is_nan() {
point
} else {
self.plane.to_world(point.as_dvec3()).as_vec3()
}
})
.collect();
Some(preview_wire(points))
}
} }
} }
} }

View file

@ -5,7 +5,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::t; use crate::t;
@ -29,6 +29,7 @@ enum Step {
pub struct DiameterDimensionCommand { pub struct DiameterDimensionCommand {
step: Step, step: Step,
plane: WorkingPlane,
/// Optional text that replaces the measured value (None = measurement). /// Optional text that replaces the measured value (None = measurement).
text_override: Option<String>, text_override: Option<String>,
/// True while the next typed line is captured as the text override. /// True while the next typed line is captured as the text override.
@ -43,6 +44,7 @@ impl DiameterDimensionCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: Step::CenterPoint, step: Step::CenterPoint,
plane: WorkingPlane::default(),
text_override: None, text_override: None,
awaiting_text: false, awaiting_text: false,
text_angle: None, text_angle: None,
@ -52,6 +54,10 @@ impl DiameterDimensionCommand {
} }
impl CadCommand for DiameterDimensionCommand { impl CadCommand for DiameterDimensionCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"DIMDIAMETER" "DIMDIAMETER"
} }
@ -83,6 +89,9 @@ impl CadCommand for DiameterDimensionCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::TextPoint { center, arc_pt } => { Step::TextPoint { center, arc_pt } => {
let center = self.plane.to_local(center);
let arc_pt = self.plane.to_local(arc_pt);
let pt = self.plane.to_local(pt);
let mut dim = DimensionDiameter::new(v3(center), v3(arc_pt)); let mut dim = DimensionDiameter::new(v3(center), v3(arc_pt));
dim.base.definition_point = v3(arc_pt); dim.base.definition_point = v3(arc_pt);
dim.base.text_middle_point = v3(pt); dim.base.text_middle_point = v3(pt);
@ -94,7 +103,9 @@ impl CadCommand for DiameterDimensionCommand {
if let Some(a) = self.text_angle { if let Some(a) = self.text_angle {
dim.base.text_rotation = a; dim.base.text_rotation = a;
} }
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Diameter(dim))) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension(
Dimension::Diameter(dim),
)))
} }
} }
} }

View file

@ -15,7 +15,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Mat4, Vec3}; use glam::{DVec3, Mat4, Vec3};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::t; use crate::t;
@ -33,7 +33,7 @@ pub fn tool() -> ToolDef {
pub struct LeaderCommand { pub struct LeaderCommand {
verts: Vec<DVec3>, verts: Vec<DVec3>,
ucs: Mat4, plane: WorkingPlane,
dimension_style: String, dimension_style: String,
text_style: String, text_style: String,
text_height: f64, text_height: f64,
@ -55,7 +55,7 @@ impl LeaderCommand {
}; };
Self { Self {
verts: Vec::new(), verts: Vec::new(),
ucs: Mat4::IDENTITY, plane: WorkingPlane::default(),
dimension_style: defaults.style_name, dimension_style: defaults.style_name,
text_style: defaults.text_style_name, text_style: defaults.text_style_name,
text_height: defaults.text_height, text_height: defaults.text_height,
@ -72,8 +72,8 @@ impl CadCommand for LeaderCommand {
"LEADER" "LEADER"
} }
fn set_ucs(&mut self, ucs: Mat4) { fn set_working_plane(&mut self, plane: WorkingPlane) {
self.ucs = ucs; self.plane = plane;
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
@ -99,16 +99,21 @@ impl CadCommand for LeaderCommand {
} }
// Place the leader plus an empty MText annotation, link them, then open // Place the leader plus an empty MText annotation, link them, then open
// the in-place MText editor so the user types the annotation text. // the in-place MText editor so the user types the annotation text.
let local: Vec<DVec3> = self
.verts
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let leader = build_leader( let leader = build_leader(
&self.verts, &local,
self.ucs, Mat4::IDENTITY,
&self.dimension_style, &self.dimension_style,
self.text_height, self.text_height,
self.gap, self.gap,
self.arrow_size, self.arrow_size,
); );
let displayed_height = self.text_height * self.display_scale; let displayed_height = self.text_height * self.display_scale;
let (anchor, attach) = annotation_anchor(&self.verts, displayed_height, self.ucs); let (anchor, attach) = annotation_anchor(&local, displayed_height, Mat4::IDENTITY);
let mtext_height = if self.annotative { let mtext_height = if self.annotative {
self.text_height self.text_height
} else { } else {
@ -119,12 +124,15 @@ impl CadCommand for LeaderCommand {
anchor, anchor,
mtext_height, mtext_height,
attach, attach,
self.ucs, Mat4::IDENTITY,
&self.text_style, &self.text_style,
self.annotative, self.annotative,
); );
CmdResult::CommitManyAndEditText { CmdResult::CommitManyAndEditText {
entities: vec![EntityType::Leader(leader), EntityType::MText(mtext)], entities: vec![
self.plane.place_entity(EntityType::Leader(leader)),
self.plane.place_entity(EntityType::MText(mtext)),
],
edit_index: 1, edit_index: 1,
} }
} }
@ -137,12 +145,31 @@ impl CadCommand for LeaderCommand {
if self.verts.is_empty() { if self.verts.is_empty() {
return None; return None;
} }
let mut pts: Vec<Vec3> = self.verts.iter().map(|p| p.as_vec3()).collect(); let mut pts: Vec<Vec3> = self
pts.push(pt.as_vec3()); .verts
Some(preview_wire( .iter()
.map(|point| self.plane.to_local(*point).as_vec3())
.collect();
pts.push(self.plane.to_local(pt).as_vec3());
let mut preview = preview_wire(
&pts, &pts,
(self.arrow_size * self.display_scale) as f32, (self.arrow_size * self.display_scale) as f32,
)) );
preview.points = preview
.points
.iter()
.map(|point| {
if point[0].is_nan() {
*point
} else {
self.plane
.to_world(Vec3::from_array(*point).as_dvec3())
.as_vec3()
.to_array()
}
})
.collect();
Some(preview)
} }
} }

View file

@ -2,23 +2,18 @@ use acadrust::entities::{Dimension, DimensionLinear};
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Mat4, Vec3}; use glam::DVec3;
use crate::t; use crate::t;
/// World-space measurement axis for a linear dimension between `first` and fn measure_axis(first: DVec3, second: DVec3) -> DVec3 {
/// `second`, chosen as the UCS X or Y axis (whichever the span is closer to). let delta = second - first;
/// `ucs` is the UCS→wire affine; identity gives the world X/Y behaviour. if delta.x.abs() >= delta.y.abs() {
fn measure_axis(first: DVec3, second: DVec3, ucs: Mat4) -> DVec3 { DVec3::X
let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X).as_dvec3();
let uy = ucs.transform_vector3(Vec3::Y).normalize_or(Vec3::Y).as_dvec3();
let du = ucs.inverse().transform_vector3((second - first).as_vec3());
if du.x.abs() >= du.y.abs() {
ux
} else { } else {
uy DVec3::Y
} }
} }
@ -41,7 +36,7 @@ enum Step {
pub struct LinearDimensionCommand { pub struct LinearDimensionCommand {
step: Step, step: Step,
ucs: Mat4, plane: WorkingPlane,
/// Optional text that replaces the measured value (None = measurement). /// Optional text that replaces the measured value (None = measurement).
text_override: Option<String>, text_override: Option<String>,
/// True while the next typed line is captured as the text override. /// True while the next typed line is captured as the text override.
@ -56,7 +51,7 @@ impl LinearDimensionCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: Step::FirstPoint, step: Step::FirstPoint,
ucs: Mat4::IDENTITY, plane: WorkingPlane::default(),
text_override: None, text_override: None,
awaiting_text: false, awaiting_text: false,
text_angle: None, text_angle: None,
@ -70,8 +65,8 @@ impl CadCommand for LinearDimensionCommand {
"DIMLINEAR" "DIMLINEAR"
} }
fn set_ucs(&mut self, ucs: Mat4) { fn set_working_plane(&mut self, plane: WorkingPlane) {
self.ucs = ucs; self.plane = plane;
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
@ -103,21 +98,12 @@ impl CadCommand for LinearDimensionCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::DimensionLine { first, second } => { Step::DimensionLine { first, second } => {
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let pt = self.plane.to_local(pt);
let mut dim = DimensionLinear::new(v3(first), v3(second)); let mut dim = DimensionLinear::new(v3(first), v3(second));
// Measure along the UCS axis the span is closest to; the DXF let axis = measure_axis(first, second);
// rotation is that axis's angle in world space.
let axis = measure_axis(first, second, self.ucs);
dim.rotation = axis.y.atan2(axis.x); dim.rotation = axis.y.atan2(axis.x);
// Bake the UCS X-axis angle as the text rotation so the
// measurement text reads horizontally in the UCS (i.e. square on
// screen) regardless of the style's force-horizontal flags,
// which would otherwise pin it to world-horizontal and tilt it
// under a rotated UCS. Left at 0 (natural) when there's no UCS.
let ux = self.ucs.transform_vector3(Vec3::X);
let ucs_ang = (ux.y as f64).atan2(ux.x as f64);
if ucs_ang.abs() > 1e-9 {
dim.base.text_rotation = ucs_ang;
}
dim.definition_point = v3(pt); dim.definition_point = v3(pt);
dim.base.definition_point = v3(pt); dim.base.definition_point = v3(pt);
dim.base.text_middle_point = v3(linear_text_pos(first, second, pt, axis)); dim.base.text_middle_point = v3(linear_text_pos(first, second, pt, axis));
@ -128,7 +114,9 @@ impl CadCommand for LinearDimensionCommand {
if let Some(a) = self.text_angle { if let Some(a) = self.text_angle {
dim.base.text_rotation = a; dim.base.text_rotation = a;
} }
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Linear(dim))) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension(
Dimension::Linear(dim),
)))
} }
} }
} }
@ -206,8 +194,15 @@ impl CadCommand for LinearDimensionCommand {
Step::FirstPoint => None, Step::FirstPoint => None,
Step::SecondPoint(first) => Some(preview_wire(vec![first, pt])), Step::SecondPoint(first) => Some(preview_wire(vec![first, pt])),
Step::DimensionLine { first, second } => { Step::DimensionLine { first, second } => {
let axis = measure_axis(first, second, self.ucs); let first = self.plane.to_local(first);
Some(preview_wire(linear_dimension_preview(first, second, pt, axis))) let second = self.plane.to_local(second);
let pt = self.plane.to_local(pt);
let axis = measure_axis(first, second);
let points = linear_dimension_preview(first, second, pt, axis)
.into_iter()
.map(|point| self.plane.to_world(point))
.collect();
Some(preview_wire(points))
} }
} }
} }

View file

@ -10,7 +10,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Mat4, Vec3}; use glam::{DVec3, Mat4, Vec3};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::t; use crate::t;
@ -28,7 +28,7 @@ pub fn tool() -> ToolDef {
pub struct MLeaderCommand { pub struct MLeaderCommand {
verts: Vec<DVec3>, verts: Vec<DVec3>,
ucs: Mat4, plane: WorkingPlane,
style: Option<acadrust::objects::MultiLeaderStyle>, style: Option<acadrust::objects::MultiLeaderStyle>,
display_scale: f64, display_scale: f64,
} }
@ -37,7 +37,7 @@ impl MLeaderCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
verts: Vec::new(), verts: Vec::new(),
ucs: Mat4::IDENTITY, plane: WorkingPlane::default(),
style: None, style: None,
display_scale: 1.0, display_scale: 1.0,
} }
@ -54,7 +54,7 @@ impl MLeaderCommand {
}; };
Self { Self {
verts: Vec::new(), verts: Vec::new(),
ucs: Mat4::IDENTITY, plane: WorkingPlane::default(),
style: Some(style), style: Some(style),
display_scale, display_scale,
} }
@ -66,8 +66,8 @@ impl CadCommand for MLeaderCommand {
"MLEADER" "MLEADER"
} }
fn set_ucs(&mut self, ucs: Mat4) { fn set_working_plane(&mut self, plane: WorkingPlane) {
self.ucs = ucs; self.plane = plane;
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
@ -93,14 +93,21 @@ impl CadCommand for MLeaderCommand {
} }
// Place the leader with empty text, then open the in-place MText editor // Place the leader with empty text, then open the in-place MText editor
// so the user types the annotation into the rich editor. // so the user types the annotation into the rich editor.
let local: Vec<DVec3> = self
.verts
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let ml = build_mleader( let ml = build_mleader(
"", "",
&self.verts, &local,
self.ucs, Mat4::IDENTITY,
self.style.as_ref(), self.style.as_ref(),
self.display_scale, self.display_scale,
); );
CmdResult::CommitAndEditText(EntityType::MultiLeader(ml)) CmdResult::CommitAndEditText(
self.plane.place_entity(EntityType::MultiLeader(ml)),
)
} }
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
@ -112,14 +119,33 @@ impl CadCommand for MLeaderCommand {
return None; return None;
} }
// Preview / rubber-band is GPU screen-space: downcast to f32 here. // Preview / rubber-band is GPU screen-space: downcast to f32 here.
let mut pts: Vec<Vec3> = self.verts.iter().map(|p| p.as_vec3()).collect(); let mut pts: Vec<Vec3> = self
pts.push(pt.as_vec3()); .verts
.iter()
.map(|point| self.plane.to_local(*point).as_vec3())
.collect();
pts.push(self.plane.to_local(pt).as_vec3());
let arrow_size = self let arrow_size = self
.style .style
.as_ref() .as_ref()
.map_or(2.5, |style| style.arrowhead_size) .map_or(2.5, |style| style.arrowhead_size)
* self.display_scale; * self.display_scale;
Some(preview_wire(&pts, arrow_size as f32)) let mut preview = preview_wire(&pts, arrow_size as f32);
preview.points = preview
.points
.iter()
.map(|point| {
if point[0].is_nan() {
*point
} else {
self.plane
.to_world(Vec3::from_array(*point).as_dvec3())
.as_vec3()
.to_array()
}
})
.collect();
Some(preview)
} }
} }

View file

@ -13,7 +13,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::t; use crate::t;
@ -34,17 +34,23 @@ enum Step {
pub struct OrdinateDimCommand { pub struct OrdinateDimCommand {
step: Step, step: Step,
plane: WorkingPlane,
} }
impl OrdinateDimCommand { impl OrdinateDimCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: Step::FeaturePoint, step: Step::FeaturePoint,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for OrdinateDimCommand { impl CadCommand for OrdinateDimCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"DIMORDINATE" "DIMORDINATE"
} }
@ -63,6 +69,8 @@ impl CadCommand for OrdinateDimCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::LeaderEndpoint { feature } => { Step::LeaderEndpoint { feature } => {
let feature = self.plane.to_local(feature);
let pt = self.plane.to_local(pt);
let is_x = is_x_type(feature, pt); let is_x = is_x_type(feature, pt);
let elbow = ordinate_elbow(feature, pt, is_x); let elbow = ordinate_elbow(feature, pt, is_x);
let mut dim = DimensionOrdinate::new(v3(feature), v3(pt), is_x); let mut dim = DimensionOrdinate::new(v3(feature), v3(pt), is_x);
@ -73,7 +81,9 @@ impl CadCommand for OrdinateDimCommand {
// the wrong (XZ) plane, dropping the Y coordinate. (#150) // the wrong (XZ) plane, dropping the Y coordinate. (#150)
dim.definition_point = v3(elbow); dim.definition_point = v3(elbow);
dim.base.definition_point = v3(elbow); dim.base.definition_point = v3(elbow);
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Ordinate(dim))) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension(
Dimension::Ordinate(dim),
)))
} }
} }
} }
@ -86,8 +96,13 @@ impl CadCommand for OrdinateDimCommand {
Step::LeaderEndpoint { feature } => feature, Step::LeaderEndpoint { feature } => feature,
_ => return None, _ => return None,
}; };
let feature = self.plane.to_local(feature);
let pt = self.plane.to_local(pt);
let is_x = is_x_type(feature, pt); let is_x = is_x_type(feature, pt);
let elbow = ordinate_elbow(feature, pt, is_x); let elbow = ordinate_elbow(feature, pt, is_x);
let feature = self.plane.to_world(feature);
let elbow = self.plane.to_world(elbow);
let pt = self.plane.to_world(pt);
// Screen-only rubber band: downcast to f32 at the preview boundary. // Screen-only rubber band: downcast to f32 at the preview boundary.
Some(preview_wire(vec![ Some(preview_wire(vec![
feature.as_vec3(), feature.as_vec3(),

View file

@ -2,7 +2,7 @@ use acadrust::entities::{Dimension, DimensionRadius};
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
@ -27,6 +27,7 @@ enum Step {
pub struct RadiusDimensionCommand { pub struct RadiusDimensionCommand {
step: Step, step: Step,
plane: WorkingPlane,
/// Optional text that replaces the measured value (None = measurement). /// Optional text that replaces the measured value (None = measurement).
text_override: Option<String>, text_override: Option<String>,
/// True while the next typed line is captured as the text override. /// True while the next typed line is captured as the text override.
@ -41,6 +42,7 @@ impl RadiusDimensionCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: Step::CenterPoint, step: Step::CenterPoint,
plane: WorkingPlane::default(),
text_override: None, text_override: None,
awaiting_text: false, awaiting_text: false,
text_angle: None, text_angle: None,
@ -50,6 +52,10 @@ impl RadiusDimensionCommand {
} }
impl CadCommand for RadiusDimensionCommand { impl CadCommand for RadiusDimensionCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"DIMRADIUS" "DIMRADIUS"
} }
@ -81,6 +87,9 @@ impl CadCommand for RadiusDimensionCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::TextPoint { center, point } => { Step::TextPoint { center, point } => {
let center = self.plane.to_local(center);
let point = self.plane.to_local(point);
let pt = self.plane.to_local(pt);
let mut dim = DimensionRadius::new(v3(center), v3(point)); let mut dim = DimensionRadius::new(v3(center), v3(point));
dim.base.definition_point = v3(point); dim.base.definition_point = v3(point);
dim.base.text_middle_point = v3(pt); dim.base.text_middle_point = v3(pt);
@ -92,7 +101,9 @@ impl CadCommand for RadiusDimensionCommand {
if let Some(a) = self.text_angle { if let Some(a) = self.text_angle {
dim.base.text_rotation = a; dim.base.text_rotation = a;
} }
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Radius(dim))) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension(
Dimension::Radius(dim),
)))
} }
} }
} }

View file

@ -12,7 +12,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::DVec3; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::t; use crate::t;
@ -45,6 +45,7 @@ pub struct TableCommand {
column_width: f64, column_width: f64,
row_height: f64, row_height: f64,
preview_scale: f64, preview_scale: f64,
plane: WorkingPlane,
} }
impl TableCommand { impl TableCommand {
@ -55,6 +56,7 @@ impl TableCommand {
column_width: COL_WIDTH, column_width: COL_WIDTH,
row_height: ROW_HEIGHT, row_height: ROW_HEIGHT,
preview_scale: 1.0, preview_scale: 1.0,
plane: WorkingPlane::default(),
} }
} }
@ -81,11 +83,16 @@ impl TableCommand {
} else { } else {
1.0 1.0
}, },
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for TableCommand { impl CadCommand for TableCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"TABLE" "TABLE"
} }
@ -163,27 +170,34 @@ impl CadCommand for TableCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::Insertion { cols, rows } = self.step { if let Step::Insertion { cols, rows } = self.step {
let ins = Vector3::new(pt.x, pt.y, pt.z); let point = self.plane.to_local(pt);
let ins = Vector3::new(point.x, point.y, point.z);
let mut table = TableBuilder::new(rows, cols) let mut table = TableBuilder::new(rows, cols)
.at(ins) .at(ins)
.row_height(self.row_height) .row_height(self.row_height)
.column_width(self.column_width) .column_width(self.column_width)
.build(); .build();
table.table_style_handle = self.style_handle; table.table_style_handle = self.style_handle;
CmdResult::CommitAndExit(EntityType::Table(table)) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Table(table)))
} else { } else {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3(); fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if let Step::Insertion { cols, rows } = self.step { if let Step::Insertion { cols, rows } = self.step {
// Preview: outline of the table bounding box. // Preview: outline of the table bounding box.
let w = (cols as f32) * (self.column_width * self.preview_scale) as f32; let w = (cols as f32) * (self.column_width * self.preview_scale) as f32;
let h = (rows as f32) * (self.row_height * self.preview_scale) as f32; let h = (rows as f32) * (self.row_height * self.preview_scale) as f32;
let x = pt.x; let point = self.plane.to_local(pt);
let y = pt.y; let corners = [
let z = pt.z; point,
point + DVec3::X * w as f64,
point + DVec3::new(w as f64, -(h as f64), 0.0),
point - DVec3::Y * h as f64,
]
.map(|corner| self.plane.to_world(corner).as_vec3().to_array());
let [p0, p1, p2, p3] = corners;
Some(WireModel { Some(WireModel {
taper_widths: Vec::new(), taper_widths: Vec::new(),
world_width: 0.0, world_width: 0.0,
@ -197,14 +211,7 @@ impl CadCommand for TableCommand {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "table_preview".into(), name: "table_preview".into(),
points: vec![ points: vec![
[x, y, z], p0, p1, p1, p2, p2, p3, p3, p0,
[x + w, y, z],
[x + w, y, z],
[x + w, y, z - h],
[x + w, y, z - h],
[x, y, z - h],
[x, y, z - h],
[x, y, z],
], ],
points_low: Vec::new(), points_low: Vec::new(),
color: WireModel::CYAN, color: WireModel::CYAN,

View file

@ -9,7 +9,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::DVec3; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::t; use crate::t;
@ -32,15 +32,23 @@ enum Step {
pub struct ToleranceCommand { pub struct ToleranceCommand {
step: Step, step: Step,
plane: WorkingPlane,
} }
impl ToleranceCommand { impl ToleranceCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { step: Step::Text } Self {
step: Step::Text,
plane: WorkingPlane::default(),
}
} }
} }
impl CadCommand for ToleranceCommand { impl CadCommand for ToleranceCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"TOLERANCE" "TOLERANCE"
} }
@ -69,9 +77,10 @@ impl CadCommand for ToleranceCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::Insertion { text } = &self.step { if let Step::Insertion { text } = &self.step {
let ins = Vector3::new(pt.x, pt.y, pt.z); let point = self.plane.to_local(pt);
let ins = Vector3::new(point.x, point.y, point.z);
let tol = Tolerance::with_text(ins, text.clone()); let tol = Tolerance::with_text(ins, text.clone());
CmdResult::CommitAndExit(EntityType::Tolerance(tol)) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Tolerance(tol)))
} else { } else {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -81,11 +90,18 @@ impl CadCommand for ToleranceCommand {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3(); fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if !matches!(self.step, Step::Insertion { .. }) { if !matches!(self.step, Step::Insertion { .. }) {
return None; return None;
} }
let d = 0.15_f32; let d = 0.15;
let points = [
pt - self.plane.x * d,
pt + self.plane.x * d,
DVec3::splat(f64::NAN),
pt - self.plane.y * d,
pt + self.plane.y * d,
];
Some(WireModel { Some(WireModel {
taper_widths: Vec::new(), taper_widths: Vec::new(),
world_width: 0.0, world_width: 0.0,
@ -98,13 +114,10 @@ impl CadCommand for ToleranceCommand {
dash_align_end: None, dash_align_end: None,
text_verts: Vec::new(), text_verts: Vec::new(),
name: "tolerance_preview".into(), name: "tolerance_preview".into(),
points: vec![ points: points
[pt.x - d, pt.y, pt.z], .iter()
[pt.x + d, pt.y, pt.z], .map(|point| point.as_vec3().to_array())
[f32::NAN, 0.0, 0.0], .collect(),
[pt.x, pt.y, pt.z - d],
[pt.x, pt.y, pt.z + d],
],
points_low: Vec::new(), points_low: Vec::new(),
color: WireModel::CYAN, color: WireModel::CYAN,
selected: false, selected: false,

View file

@ -17,7 +17,7 @@ use acadrust::types::Vector3;
use acadrust::{Arc as CadArc, EntityType}; use acadrust::{Arc as CadArc, EntityType};
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::DVec3; use glam::DVec3;
@ -72,12 +72,18 @@ pub const ICON: IconKind = ICON_3P;
// ── Shared math helpers ──────────────────────────────────────────────────── // ── Shared math helpers ────────────────────────────────────────────────────
/// Angle in radians from `center` to `pt`. /// Angle in radians from `center` to `pt`.
fn angle_xy(center: DVec3, pt: DVec3) -> f64 { fn angle_xy(center: DVec3, pt: DVec3, plane: WorkingPlane) -> f64 {
(pt.y - center.y).atan2(pt.x - center.x) plane.angle(center, pt).unwrap_or(0.0)
} }
/// Build a CCW arc polyline from `start_angle` to `end_angle` (radians). /// Build a CCW arc polyline from `start_angle` to `end_angle` (radians).
fn arc_preview(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> WireModel { fn arc_preview(
center: DVec3,
radius: f64,
start_angle: f64,
end_angle: f64,
plane: WorkingPlane,
) -> WireModel {
let mut ea = end_angle; let mut ea = end_angle;
while ea < start_angle { while ea < start_angle {
ea += TAU; ea += TAU;
@ -87,32 +93,36 @@ fn arc_preview(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) ->
let pts: Vec<[f64; 3]> = (0..=segs) let pts: Vec<[f64; 3]> = (0..=segs)
.map(|i| { .map(|i| {
let a = start_angle + span * (i as f64 / segs as f64); let a = start_angle + span * (i as f64 / segs as f64);
[ let point = center + plane.x * (radius * a.cos()) + plane.y * (radius * a.sin());
center.x + radius * a.cos(), [point.x, point.y, point.z]
center.y + radius * a.sin(),
center.z,
]
}) })
.collect(); .collect();
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false) WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
} }
fn make_arc(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> EntityType { fn make_arc(
EntityType::Arc(CadArc { center: DVec3,
radius: f64,
start_angle: f64,
end_angle: f64,
plane: WorkingPlane,
) -> EntityType {
let center = plane.to_local(center);
plane.place_entity(EntityType::Arc(CadArc {
center: Vector3::new(center.x, center.y, center.z), center: Vector3::new(center.x, center.y, center.z),
radius, radius,
start_angle, start_angle,
end_angle, end_angle,
..Default::default() ..Default::default()
}) }))
} }
/// Signed rotation from `prev` to `curr` around `center`. /// Signed rotation from `prev` to `curr` around `center`.
/// Positive = CCW, negative = CW. /// Positive = CCW, negative = CW.
/// Signed angle (radians) swept from `prev` to `curr` about `center`. /// Signed angle (radians) swept from `prev` to `curr` about `center`.
fn rot_delta(center: DVec3, prev: DVec3, curr: DVec3) -> f64 { fn rot_delta(center: DVec3, prev: DVec3, curr: DVec3, plane: WorkingPlane) -> f64 {
let p = prev - center; let p = plane.vector_to_local(prev - center);
let c = curr - center; let c = plane.vector_to_local(curr - center);
(p.x * c.y - p.y * c.x).atan2(p.x * c.x + p.y * c.y) (p.x * c.y - p.y * c.x).atan2(p.x * c.x + p.y * c.y)
} }
@ -130,7 +140,13 @@ fn line_wire(a: DVec3, b: DVec3) -> WireModel {
} }
/// Circumscribed circle through three points. /// Circumscribed circle through three points.
fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> { fn circumcircle(
a: DVec3,
b: DVec3,
c: DVec3,
plane: WorkingPlane,
) -> Option<(DVec3, f64)> {
let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c));
let d = 2.0 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)); let d = 2.0 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
if d.abs() < 1e-9 { if d.abs() < 1e-9 {
return None; return None;
@ -144,7 +160,7 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
+ (c.x * c.x + c.y * c.y) * (b.x - a.x)) + (c.x * c.x + c.y * c.y) * (b.x - a.x))
/ d; / d;
let center = DVec3::new(ux, uy, a.z); let center = DVec3::new(ux, uy, a.z);
Some((center, center.distance(a))) Some((plane.to_world(center), center.distance(a)))
} }
/// True if `angle` lies inside the CCW arc [start..end] (radians). /// True if `angle` lies inside the CCW arc [start..end] (radians).
@ -160,7 +176,13 @@ fn ccw_contains(angle: f64, start: f64, end: f64) -> bool {
} }
/// Arc center+radius from two endpoints and a cursor (sagitta / bow-toward-cursor). /// Arc center+radius from two endpoints and a cursor (sagitta / bow-toward-cursor).
fn arc_from_sagitta(s: DVec3, e: DVec3, cursor: DVec3) -> Option<(DVec3, f64)> { fn arc_from_sagitta(
s: DVec3,
e: DVec3,
cursor: DVec3,
plane: WorkingPlane,
) -> Option<(DVec3, f64)> {
let (s, e, cursor) = (plane.to_local(s), plane.to_local(e), plane.to_local(cursor));
let chord_vec = e - s; let chord_vec = e - s;
let chord_len = (chord_vec.x * chord_vec.x + chord_vec.y * chord_vec.y).sqrt(); let chord_len = (chord_vec.x * chord_vec.x + chord_vec.y * chord_vec.y).sqrt();
if chord_len < 1e-6 { if chord_len < 1e-6 {
@ -177,28 +199,48 @@ fn arc_from_sagitta(s: DVec3, e: DVec3, cursor: DVec3) -> Option<(DVec3, f64)> {
let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5)) let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5))
.max(0.0) .max(0.0)
.sqrt(); .sqrt();
Some((mid - perp * h.signum() * d, r)) Some((plane.to_world(mid - perp * h.signum() * d), r))
} }
/// Arc center+radius from start, end, and a radius-magnitude point (dist = dist(pt, start)). /// Arc center+radius from start, end, and a radius-magnitude point (dist = dist(pt, start)).
fn arc_from_se_radius(s: DVec3, e: DVec3, radius_pt: DVec3) -> Option<(DVec3, f64)> { fn arc_from_se_radius(
s: DVec3,
e: DVec3,
radius_pt: DVec3,
plane: WorkingPlane,
) -> Option<(DVec3, f64)> {
let (s, e, radius_pt) = (
plane.to_local(s),
plane.to_local(e),
plane.to_local(radius_pt),
);
let r = s.distance(radius_pt).max(1e-3); let r = s.distance(radius_pt).max(1e-3);
let chord_len = s.distance(e); let chord_len = s.distance(e);
if r < chord_len * 0.5 { if r < chord_len * 0.5 {
return None; return None;
} }
let unit_chord = (e - s) / chord_len; let unit_chord = (e - s) / chord_len;
let perp = DVec3::new(-unit_chord.y, 0.0, unit_chord.x); let perp = DVec3::new(-unit_chord.y, unit_chord.x, 0.0);
let mid = (s + e) * 0.5; let mid = (s + e) * 0.5;
let h_sign = (radius_pt - mid).dot(perp).signum(); let h_sign = (radius_pt - mid).dot(perp).signum();
let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5)) let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5))
.max(0.0) .max(0.0)
.sqrt(); .sqrt();
Some((mid - perp * h_sign * d, r)) Some((plane.to_world(mid - perp * h_sign * d), r))
} }
/// Arc center+radius from start, end, and a tangent-direction point at start. /// Arc center+radius from start, end, and a tangent-direction point at start.
fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)> { fn arc_from_direction(
s: DVec3,
e: DVec3,
dir_pt: DVec3,
plane: WorkingPlane,
) -> Option<(DVec3, f64)> {
let (s, e, dir_pt) = (
plane.to_local(s),
plane.to_local(e),
plane.to_local(dir_pt),
);
let t = (dir_pt - s).normalize_or_zero(); let t = (dir_pt - s).normalize_or_zero();
if t.length_squared() < 1e-12 { if t.length_squared() < 1e-12 {
return None; return None;
@ -211,7 +253,7 @@ fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)>
} }
let lambda = chord.length_squared() * 0.5 / denom; let lambda = chord.length_squared() * 0.5 / denom;
let center = s + perp_t * lambda; let center = s + perp_t * lambda;
Some((center, center.distance(s))) Some((plane.to_world(center), center.distance(s)))
} }
/// Tangent-continuing arc: starts at `s` leaving in unit direction `t` (forward, /// Tangent-continuing arc: starts at `s` leaving in unit direction `t` (forward,
@ -225,18 +267,25 @@ fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)>
/// checking the circle's CCW travel direction at `s` and swapping the angles /// checking the circle's CCW travel direction at `s` and swapping the angles
/// when it opposes `+t`. `flip` (Ctrl) selects the complementary arc — the other /// when it opposes `+t`. `flip` (Ctrl) selects the complementary arc — the other
/// way around the same circle. /// way around the same circle.
fn arc_continue(s: DVec3, t: DVec3, e: DVec3, flip: bool) -> Option<(DVec3, f64, f64, f64)> { fn arc_continue(
let (center, radius) = arc_from_direction(s, e, s + t)?; s: DVec3,
t: DVec3,
e: DVec3,
flip: bool,
plane: WorkingPlane,
) -> Option<(DVec3, f64, f64, f64)> {
let (center, radius) = arc_from_direction(s, e, s + t, plane)?;
// CCW travel direction on this circle at s = (s - center) rotated +90°. // CCW travel direction on this circle at s = (s - center) rotated +90°.
let rs = s - center; let rs = plane.vector_to_local(s - center);
let t = plane.vector_to_local(t);
let travel_s = DVec3::new(-rs.y, rs.x, 0.0); let travel_s = DVec3::new(-rs.y, rs.x, 0.0);
let forward = travel_s.dot(t) >= 0.0; let forward = travel_s.dot(t) >= 0.0;
// Draw CCW s→e when the CCW-from-s arc leaves along +t; else CCW e→s. `flip` // Draw CCW s→e when the CCW-from-s arc leaves along +t; else CCW e→s. `flip`
// inverts the choice to draw the complementary arc. // inverts the choice to draw the complementary arc.
let (sa, ea) = if forward ^ flip { let (sa, ea) = if forward ^ flip {
(angle_xy(center, s), angle_xy(center, e)) (angle_xy(center, s, plane), angle_xy(center, e, plane))
} else { } else {
(angle_xy(center, e), angle_xy(center, s)) (angle_xy(center, e, plane), angle_xy(center, s, plane))
}; };
Some((center, radius, sa, ea)) Some((center, radius, sa, ea))
} }
@ -264,12 +313,22 @@ pub fn continue_anchor(entity: &EntityType, last: Option<DVec3>) -> Option<(DVec
let ep_v = a.end_point(); let ep_v = a.end_point();
let sp = DVec3::new(sp_v.x, sp_v.y, sp_v.z); let sp = DVec3::new(sp_v.x, sp_v.y, sp_v.z);
let ep = DVec3::new(ep_v.x, ep_v.y, ep_v.z); let ep = DVec3::new(ep_v.x, ep_v.y, ep_v.z);
let normal = (a.normal.x, a.normal.y, a.normal.z);
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
let tangent = |angle: f64, ccw: bool| {
let sign = if ccw { 1.0 } else { -1.0 };
DVec3::new(
sign * (-angle.sin() * ax.0 + angle.cos() * ay.0),
sign * (-angle.sin() * ax.1 + angle.cos() * ay.1),
sign * (-angle.sin() * ax.2 + angle.cos() * ay.2),
)
};
if nearer_first(ep, sp) { if nearer_first(ep, sp) {
// Ended at end_angle: outward tangent = CCW travel there. // Ended at end_angle: outward tangent = CCW travel there.
Some((ep, DVec3::new(-a.end_angle.sin(), a.end_angle.cos(), 0.0))) Some((ep, tangent(a.end_angle, true)))
} else { } else {
// Ended at start_angle: outward tangent = CW travel there. // Ended at start_angle: outward tangent = CW travel there.
Some((sp, DVec3::new(a.start_angle.sin(), -a.start_angle.cos(), 0.0))) Some((sp, tangent(a.start_angle, false)))
} }
} }
_ => None, _ => None,
@ -292,6 +351,7 @@ pub struct ArcCommand {
sa: f64, sa: f64,
prev_pt: Option<DVec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
plane: WorkingPlane,
} }
impl ArcCommand { impl ArcCommand {
@ -303,11 +363,16 @@ impl ArcCommand {
sa: 0.0, sa: 0.0,
prev_pt: None, prev_pt: None,
cw: false, cw: false,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcCommand { impl CadCommand for ArcCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC" "ARC"
} }
@ -360,16 +425,16 @@ impl CadCommand for ArcCommand {
} }
1 => { 1 => {
self.r = self.c.distance(pt); self.r = self.c.distance(pt);
self.sa = angle_xy(self.c, pt); self.sa = angle_xy(self.c, pt, self.plane);
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let ea = angle_xy(self.c, pt); let ea = angle_xy(self.c, pt, self.plane);
let e = if self.cw { let e = if self.cw {
make_arc(self.c, self.r, ea, self.sa) make_arc(self.c, self.r, ea, self.sa, self.plane)
} else { } else {
make_arc(self.c, self.r, self.sa, ea) make_arc(self.c, self.r, self.sa, ea, self.plane)
}; };
CmdResult::CommitAndExit(e) CmdResult::CommitAndExit(e)
} }
@ -408,7 +473,7 @@ impl CadCommand for ArcCommand {
// Only flip the sweep once the cursor has moved a clear // Only flip the sweep once the cursor has moved a clear
// angular step; keep the reference point until then so slow // angular step; keep the reference point until then so slow
// moves accumulate and jitter is ignored. // moves accumulate and jitter is ignored.
let d = rot_delta(self.c, prev, pt); let d = rot_delta(self.c, prev, pt, self.plane);
if d.abs() > DIR_TOL { if d.abs() > DIR_TOL {
self.cw = d < 0.0; self.cw = d < 0.0;
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
@ -416,11 +481,11 @@ impl CadCommand for ArcCommand {
} else { } else {
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
} }
let ea = angle_xy(self.c, pt); let ea = angle_xy(self.c, pt, self.plane);
Some(if self.cw { Some(if self.cw {
arc_preview(self.c, self.r, ea, self.sa) arc_preview(self.c, self.r, ea, self.sa, self.plane)
} else { } else {
arc_preview(self.c, self.r, self.sa, ea) arc_preview(self.c, self.r, self.sa, ea, self.plane)
}) })
} }
_ => None, _ => None,
@ -432,15 +497,23 @@ impl CadCommand for ArcCommand {
pub struct Arc3PCommand { pub struct Arc3PCommand {
pts: Vec<DVec3>, pts: Vec<DVec3>,
plane: WorkingPlane,
} }
impl Arc3PCommand { impl Arc3PCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { pts: Vec::new() } Self {
pts: Vec::new(),
plane: WorkingPlane::default(),
}
} }
} }
impl CadCommand for Arc3PCommand { impl CadCommand for Arc3PCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_3P" "ARC_3P"
} }
@ -457,22 +530,22 @@ impl CadCommand for Arc3PCommand {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
let (p1, p2, p3) = (self.pts[0], self.pts[1], self.pts[2]); let (p1, p2, p3) = (self.pts[0], self.pts[1], self.pts[2]);
match circumcircle(p1, p2, p3) { match circumcircle(p1, p2, p3, self.plane) {
None => { None => {
self.pts.pop(); self.pts.pop();
CmdResult::NeedPoint CmdResult::NeedPoint
} // collinear — retry } // collinear — retry
Some((center, radius)) => { Some((center, radius)) => {
let a1 = angle_xy(center, p1); let a1 = angle_xy(center, p1, self.plane);
let a2 = angle_xy(center, p2); let a2 = angle_xy(center, p2, self.plane);
let a3 = angle_xy(center, p3); let a3 = angle_xy(center, p3, self.plane);
// Choose arc direction so that p2 lies on the arc from p1 to p3. // Choose arc direction so that p2 lies on the arc from p1 to p3.
let (sa, ea) = if ccw_contains(a2, a1, a3) { let (sa, ea) = if ccw_contains(a2, a1, a3) {
(a1, a3) (a1, a3)
} else { } else {
(a3, a1) (a3, a1)
}; };
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea)) CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane))
} }
} }
} }
@ -491,16 +564,16 @@ impl CadCommand for Arc3PCommand {
1 => Some(line_wire(self.pts[0], pt)), 1 => Some(line_wire(self.pts[0], pt)),
_ => { _ => {
let (p1, p2) = (self.pts[0], self.pts[1]); let (p1, p2) = (self.pts[0], self.pts[1]);
if let Some((center, radius)) = circumcircle(p1, p2, pt) { if let Some((center, radius)) = circumcircle(p1, p2, pt, self.plane) {
let a1 = angle_xy(center, p1); let a1 = angle_xy(center, p1, self.plane);
let a2 = angle_xy(center, p2); let a2 = angle_xy(center, p2, self.plane);
let a3 = angle_xy(center, pt); let a3 = angle_xy(center, pt, self.plane);
let (sa, ea) = if ccw_contains(a2, a1, a3) { let (sa, ea) = if ccw_contains(a2, a1, a3) {
(a1, a3) (a1, a3)
} else { } else {
(a3, a1) (a3, a1)
}; };
Some(arc_preview(center, radius, sa, ea)) Some(arc_preview(center, radius, sa, ea, self.plane))
} else { } else {
Some(WireModel::solid_f64( Some(WireModel::solid_f64(
"rubber_band".into(), "rubber_band".into(),
@ -524,6 +597,7 @@ pub struct ArcSCECommand {
sa: f64, sa: f64,
prev_pt: Option<DVec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
plane: WorkingPlane,
} }
impl ArcSCECommand { impl ArcSCECommand {
@ -536,11 +610,16 @@ impl ArcSCECommand {
sa: 0.0, sa: 0.0,
prev_pt: None, prev_pt: None,
cw: false, cw: false,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcSCECommand { impl CadCommand for ArcSCECommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_SCE" "ARC_SCE"
} }
@ -564,16 +643,16 @@ impl CadCommand for ArcSCECommand {
1 => { 1 => {
self.c = pt; self.c = pt;
self.r = pt.distance(self.s); self.r = pt.distance(self.s);
self.sa = angle_xy(pt, self.s); self.sa = angle_xy(pt, self.s, self.plane);
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let ea = angle_xy(self.c, pt); let ea = angle_xy(self.c, pt, self.plane);
let e = if self.cw { let e = if self.cw {
make_arc(self.c, self.r, ea, self.sa) make_arc(self.c, self.r, ea, self.sa, self.plane)
} else { } else {
make_arc(self.c, self.r, self.sa, ea) make_arc(self.c, self.r, self.sa, ea, self.plane)
}; };
CmdResult::CommitAndExit(e) CmdResult::CommitAndExit(e)
} }
@ -596,7 +675,7 @@ impl CadCommand for ArcSCECommand {
// Only flip the sweep once the cursor has moved a clear // Only flip the sweep once the cursor has moved a clear
// angular step; keep the reference point until then so slow // angular step; keep the reference point until then so slow
// moves accumulate and jitter is ignored. // moves accumulate and jitter is ignored.
let d = rot_delta(self.c, prev, pt); let d = rot_delta(self.c, prev, pt, self.plane);
if d.abs() > DIR_TOL { if d.abs() > DIR_TOL {
self.cw = d < 0.0; self.cw = d < 0.0;
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
@ -604,11 +683,11 @@ impl CadCommand for ArcSCECommand {
} else { } else {
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
} }
let ea = angle_xy(self.c, pt); let ea = angle_xy(self.c, pt, self.plane);
Some(if self.cw { Some(if self.cw {
arc_preview(self.c, self.r, ea, self.sa) arc_preview(self.c, self.r, ea, self.sa, self.plane)
} else { } else {
arc_preview(self.c, self.r, self.sa, ea) arc_preview(self.c, self.r, self.sa, ea, self.plane)
}) })
} }
_ => None, _ => None,
@ -627,6 +706,7 @@ pub struct ArcSCACommand {
sa: f64, sa: f64,
prev_pt: Option<DVec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
plane: WorkingPlane,
} }
impl ArcSCACommand { impl ArcSCACommand {
@ -639,11 +719,16 @@ impl ArcSCACommand {
sa: 0.0, sa: 0.0,
prev_pt: None, prev_pt: None,
cw: false, cw: false,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcSCACommand { impl CadCommand for ArcSCACommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_SCA" "ARC_SCA"
} }
@ -671,16 +756,16 @@ impl CadCommand for ArcSCACommand {
1 => { 1 => {
self.c = pt; self.c = pt;
self.r = pt.distance(self.s); self.r = pt.distance(self.s);
self.sa = angle_xy(pt, self.s); self.sa = angle_xy(pt, self.s, self.plane);
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let ea = angle_xy(self.c, pt); let ea = angle_xy(self.c, pt, self.plane);
let e = if self.cw { let e = if self.cw {
make_arc(self.c, self.r, ea, self.sa) make_arc(self.c, self.r, ea, self.sa, self.plane)
} else { } else {
make_arc(self.c, self.r, self.sa, ea) make_arc(self.c, self.r, self.sa, ea, self.plane)
}; };
CmdResult::CommitAndExit(e) CmdResult::CommitAndExit(e)
} }
@ -701,7 +786,7 @@ impl CadCommand for ArcSCACommand {
// Negative span = CW; positive = CCW. // Negative span = CW; positive = CCW.
let ea = self.sa + span.to_radians(); let ea = self.sa + span.to_radians();
return Some(CmdResult::CommitAndExit(make_arc( return Some(CmdResult::CommitAndExit(make_arc(
self.c, self.r, self.sa, ea, self.c, self.r, self.sa, ea, self.plane,
))); )));
} }
None None
@ -714,7 +799,9 @@ impl CadCommand for ArcSCACommand {
anchor: DynAnchor::Point(self.c), anchor: DynAnchor::Point(self.c),
fields: vec![DynFieldSpec::new(DynRole::Angle)], fields: vec![DynFieldSpec::new(DynRole::Angle)],
guide: DynGuide::Polar, guide: DynGuide::Polar,
ref_point: Some(self.c + DVec3::new(self.sa.cos(), self.sa.sin(), 0.0)), ref_point: Some(
self.c + self.plane.x * self.sa.cos() + self.plane.y * self.sa.sin(),
),
}) })
} }
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
@ -722,7 +809,9 @@ impl CadCommand for ArcSCACommand {
} }
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
(self.step == 2).then(|| { (self.step == 2).then(|| {
crate::command::dyn_display_angle_deg((angle_xy(self.c, cursor) - self.sa) as f32) as f64 crate::command::dyn_display_angle_deg(
(angle_xy(self.c, cursor, self.plane) - self.sa) as f32,
) as f64
}) })
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
@ -733,7 +822,7 @@ impl CadCommand for ArcSCACommand {
// Only flip the sweep once the cursor has moved a clear // Only flip the sweep once the cursor has moved a clear
// angular step; keep the reference point until then so slow // angular step; keep the reference point until then so slow
// moves accumulate and jitter is ignored. // moves accumulate and jitter is ignored.
let d = rot_delta(self.c, prev, pt); let d = rot_delta(self.c, prev, pt, self.plane);
if d.abs() > DIR_TOL { if d.abs() > DIR_TOL {
self.cw = d < 0.0; self.cw = d < 0.0;
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
@ -741,11 +830,11 @@ impl CadCommand for ArcSCACommand {
} else { } else {
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
} }
let ea = angle_xy(self.c, pt); let ea = angle_xy(self.c, pt, self.plane);
Some(if self.cw { Some(if self.cw {
arc_preview(self.c, self.r, ea, self.sa) arc_preview(self.c, self.r, ea, self.sa, self.plane)
} else { } else {
arc_preview(self.c, self.r, self.sa, ea) arc_preview(self.c, self.r, self.sa, ea, self.plane)
}) })
} }
_ => None, _ => None,
@ -763,6 +852,7 @@ pub struct ArcSCLCommand {
c: DVec3, c: DVec3,
r: f64, r: f64,
sa: f64, sa: f64,
plane: WorkingPlane,
} }
impl ArcSCLCommand { impl ArcSCLCommand {
@ -773,11 +863,16 @@ impl ArcSCLCommand {
c: DVec3::ZERO, c: DVec3::ZERO,
r: 0.0, r: 0.0,
sa: 0.0, sa: 0.0,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcSCLCommand { impl CadCommand for ArcSCLCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_SCL" "ARC_SCL"
} }
@ -805,14 +900,20 @@ impl CadCommand for ArcSCLCommand {
1 => { 1 => {
self.c = pt; self.c = pt;
self.r = pt.distance(self.s); self.r = pt.distance(self.s);
self.sa = angle_xy(pt, self.s); self.sa = angle_xy(pt, self.s, self.plane);
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let chord = self.s.distance(pt); let chord = self.s.distance(pt);
let ea = end_angle_from_chord_len(self.sa, chord, self.r); let ea = end_angle_from_chord_len(self.sa, chord, self.r);
CmdResult::CommitAndExit(make_arc(self.c, self.r, self.sa, ea)) CmdResult::CommitAndExit(make_arc(
self.c,
self.r,
self.sa,
ea,
self.plane,
))
} }
} }
} }
@ -831,7 +932,7 @@ impl CadCommand for ArcSCLCommand {
if chord > 0.0 { if chord > 0.0 {
let ea = end_angle_from_chord_len(self.sa, chord, self.r); let ea = end_angle_from_chord_len(self.sa, chord, self.r);
return Some(CmdResult::CommitAndExit(make_arc( return Some(CmdResult::CommitAndExit(make_arc(
self.c, self.r, self.sa, ea, self.c, self.r, self.sa, ea, self.plane,
))); )));
} }
} }
@ -859,7 +960,7 @@ impl CadCommand for ArcSCLCommand {
2 => { 2 => {
let chord = self.s.distance(pt); let chord = self.s.distance(pt);
let ea = end_angle_from_chord_len(self.sa, chord, self.r); let ea = end_angle_from_chord_len(self.sa, chord, self.r);
Some(arc_preview(self.c, self.r, self.sa, ea)) Some(arc_preview(self.c, self.r, self.sa, ea, self.plane))
} }
_ => None, _ => None,
} }
@ -873,6 +974,7 @@ pub struct ArcSEACommand {
step: u8, step: u8,
s: DVec3, s: DVec3,
e: DVec3, e: DVec3,
plane: WorkingPlane,
} }
impl ArcSEACommand { impl ArcSEACommand {
@ -881,11 +983,16 @@ impl ArcSEACommand {
step: 0, step: 0,
s: DVec3::ZERO, s: DVec3::ZERO,
e: DVec3::ZERO, e: DVec3::ZERO,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcSEACommand { impl CadCommand for ArcSEACommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_SEA" "ARC_SEA"
} }
@ -908,11 +1015,11 @@ impl CadCommand for ArcSEACommand {
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => match arc_from_sagitta(self.s, self.e, pt) { _ => match arc_from_sagitta(self.s, self.e, pt, self.plane) {
Some((center, radius)) => { Some((center, radius)) => {
let sa = angle_xy(center, self.s); let sa = angle_xy(center, self.s, self.plane);
let ea = angle_xy(center, self.e); let ea = angle_xy(center, self.e, self.plane);
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea)) CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane))
} }
None => CmdResult::Cancel, None => CmdResult::Cancel,
}, },
@ -931,10 +1038,12 @@ impl CadCommand for ArcSEACommand {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
if let Some((center, radius)) = arc_from_sagitta(self.s, self.e, pt) { if let Some((center, radius)) =
let sa = angle_xy(center, self.s); arc_from_sagitta(self.s, self.e, pt, self.plane)
let ea = angle_xy(center, self.e); {
Some(arc_preview(center, radius, sa, ea)) let sa = angle_xy(center, self.s, self.plane);
let ea = angle_xy(center, self.e, self.plane);
Some(arc_preview(center, radius, sa, ea, self.plane))
} else { } else {
Some(line_wire(self.s, self.e)) Some(line_wire(self.s, self.e))
} }
@ -951,6 +1060,7 @@ pub struct ArcSERCommand {
step: u8, step: u8,
s: DVec3, s: DVec3,
e: DVec3, e: DVec3,
plane: WorkingPlane,
} }
impl ArcSERCommand { impl ArcSERCommand {
@ -959,11 +1069,16 @@ impl ArcSERCommand {
step: 0, step: 0,
s: DVec3::ZERO, s: DVec3::ZERO,
e: DVec3::ZERO, e: DVec3::ZERO,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcSERCommand { impl CadCommand for ArcSERCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_SER" "ARC_SER"
} }
@ -986,11 +1101,11 @@ impl CadCommand for ArcSERCommand {
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => match arc_from_se_radius(self.s, self.e, pt) { _ => match arc_from_se_radius(self.s, self.e, pt, self.plane) {
Some((center, radius)) => { Some((center, radius)) => {
let sa = angle_xy(center, self.s); let sa = angle_xy(center, self.s, self.plane);
let ea = angle_xy(center, self.e); let ea = angle_xy(center, self.e, self.plane);
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea)) CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane))
} }
None => CmdResult::Cancel, None => CmdResult::Cancel,
}, },
@ -1008,18 +1123,22 @@ impl CadCommand for ArcSERCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.step == 2 { if self.step == 2 {
let r: f64 = text.trim().replace(',', ".").parse().ok()?; let r: f64 = text.trim().replace(',', ".").parse().ok()?;
let chord = self.s.distance(self.e); let s = self.plane.to_local(self.s);
let e = self.plane.to_local(self.e);
let chord = s.distance(e);
if r > 0.0 && r >= chord / 2.0 { if r > 0.0 && r >= chord / 2.0 {
let mid = (self.s + self.e) * 0.5; let mid = (s + e) * 0.5;
let perp = { let perp = {
let cv = (self.e - self.s) / chord; let cv = (e - s) / chord;
DVec3::new(-cv.y, cv.x, 0.0) DVec3::new(-cv.y, cv.x, 0.0)
}; };
let d = (r * r - (chord / 2.0) * (chord / 2.0)).max(0.0).sqrt(); let d = (r * r - (chord / 2.0) * (chord / 2.0)).max(0.0).sqrt();
let center = mid - perp * d; let center = self.plane.to_world(mid - perp * d);
let sa = angle_xy(center, self.s); let sa = angle_xy(center, self.s, self.plane);
let ea = angle_xy(center, self.e); let ea = angle_xy(center, self.e, self.plane);
return Some(CmdResult::CommitAndExit(make_arc(center, r, sa, ea))); return Some(CmdResult::CommitAndExit(make_arc(
center, r, sa, ea, self.plane,
)));
} }
} }
None None
@ -1041,16 +1160,18 @@ impl CadCommand for ArcSERCommand {
if self.step != 2 { if self.step != 2 {
return None; return None;
} }
arc_from_se_radius(self.s, self.e, cursor).map(|(_, r)| r) arc_from_se_radius(self.s, self.e, cursor, self.plane).map(|(_, r)| r)
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
if let Some((center, radius)) = arc_from_se_radius(self.s, self.e, pt) { if let Some((center, radius)) =
let sa = angle_xy(center, self.s); arc_from_se_radius(self.s, self.e, pt, self.plane)
let ea = angle_xy(center, self.e); {
Some(arc_preview(center, radius, sa, ea)) let sa = angle_xy(center, self.s, self.plane);
let ea = angle_xy(center, self.e, self.plane);
Some(arc_preview(center, radius, sa, ea, self.plane))
} else { } else {
Some(line_wire(self.s, self.e)) Some(line_wire(self.s, self.e))
} }
@ -1067,6 +1188,7 @@ pub struct ArcSEDCommand {
step: u8, step: u8,
s: DVec3, s: DVec3,
e: DVec3, e: DVec3,
plane: WorkingPlane,
} }
impl ArcSEDCommand { impl ArcSEDCommand {
@ -1075,11 +1197,16 @@ impl ArcSEDCommand {
step: 0, step: 0,
s: DVec3::ZERO, s: DVec3::ZERO,
e: DVec3::ZERO, e: DVec3::ZERO,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcSEDCommand { impl CadCommand for ArcSEDCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_SED" "ARC_SED"
} }
@ -1102,11 +1229,11 @@ impl CadCommand for ArcSEDCommand {
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => match arc_from_direction(self.s, self.e, pt) { _ => match arc_from_direction(self.s, self.e, pt, self.plane) {
Some((center, radius)) => { Some((center, radius)) => {
let sa = angle_xy(center, self.s); let sa = angle_xy(center, self.s, self.plane);
let ea = angle_xy(center, self.e); let ea = angle_xy(center, self.e, self.plane);
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea)) CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane))
} }
None => CmdResult::Cancel, None => CmdResult::Cancel,
}, },
@ -1125,10 +1252,12 @@ impl CadCommand for ArcSEDCommand {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
if let Some((center, radius)) = arc_from_direction(self.s, self.e, pt) { if let Some((center, radius)) =
let sa = angle_xy(center, self.s); arc_from_direction(self.s, self.e, pt, self.plane)
let ea = angle_xy(center, self.e); {
Some(arc_preview(center, radius, sa, ea)) let sa = angle_xy(center, self.s, self.plane);
let ea = angle_xy(center, self.e, self.plane);
Some(arc_preview(center, radius, sa, ea, self.plane))
} else { } else {
Some(line_wire(self.s, self.e)) Some(line_wire(self.s, self.e))
} }
@ -1148,6 +1277,7 @@ pub struct ArcCSACommand {
sa: f64, sa: f64,
prev_pt: Option<DVec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
plane: WorkingPlane,
} }
impl ArcCSACommand { impl ArcCSACommand {
@ -1159,11 +1289,16 @@ impl ArcCSACommand {
sa: 0.0, sa: 0.0,
prev_pt: None, prev_pt: None,
cw: false, cw: false,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcCSACommand { impl CadCommand for ArcCSACommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_CSA" "ARC_CSA"
} }
@ -1190,16 +1325,16 @@ impl CadCommand for ArcCSACommand {
} }
1 => { 1 => {
self.r = self.c.distance(pt); self.r = self.c.distance(pt);
self.sa = angle_xy(self.c, pt); self.sa = angle_xy(self.c, pt, self.plane);
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let ea = angle_xy(self.c, pt); let ea = angle_xy(self.c, pt, self.plane);
let e = if self.cw { let e = if self.cw {
make_arc(self.c, self.r, ea, self.sa) make_arc(self.c, self.r, ea, self.sa, self.plane)
} else { } else {
make_arc(self.c, self.r, self.sa, ea) make_arc(self.c, self.r, self.sa, ea, self.plane)
}; };
CmdResult::CommitAndExit(e) CmdResult::CommitAndExit(e)
} }
@ -1219,7 +1354,7 @@ impl CadCommand for ArcCSACommand {
let span: f64 = text.trim().replace(',', ".").parse().ok()?; let span: f64 = text.trim().replace(',', ".").parse().ok()?;
let ea = self.sa + span.to_radians(); let ea = self.sa + span.to_radians();
return Some(CmdResult::CommitAndExit(make_arc( return Some(CmdResult::CommitAndExit(make_arc(
self.c, self.r, self.sa, ea, self.c, self.r, self.sa, ea, self.plane,
))); )));
} }
None None
@ -1230,7 +1365,9 @@ impl CadCommand for ArcCSACommand {
anchor: DynAnchor::Point(self.c), anchor: DynAnchor::Point(self.c),
fields: vec![DynFieldSpec::new(DynRole::Angle)], fields: vec![DynFieldSpec::new(DynRole::Angle)],
guide: DynGuide::Polar, guide: DynGuide::Polar,
ref_point: Some(self.c + DVec3::new(self.sa.cos(), self.sa.sin(), 0.0)), ref_point: Some(
self.c + self.plane.x * self.sa.cos() + self.plane.y * self.sa.sin(),
),
}) })
} }
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
@ -1238,7 +1375,9 @@ impl CadCommand for ArcCSACommand {
} }
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
(self.step == 2).then(|| { (self.step == 2).then(|| {
crate::command::dyn_display_angle_deg((angle_xy(self.c, cursor) - self.sa) as f32) as f64 crate::command::dyn_display_angle_deg(
(angle_xy(self.c, cursor, self.plane) - self.sa) as f32,
) as f64
}) })
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
@ -1249,7 +1388,7 @@ impl CadCommand for ArcCSACommand {
// Only flip the sweep once the cursor has moved a clear // Only flip the sweep once the cursor has moved a clear
// angular step; keep the reference point until then so slow // angular step; keep the reference point until then so slow
// moves accumulate and jitter is ignored. // moves accumulate and jitter is ignored.
let d = rot_delta(self.c, prev, pt); let d = rot_delta(self.c, prev, pt, self.plane);
if d.abs() > DIR_TOL { if d.abs() > DIR_TOL {
self.cw = d < 0.0; self.cw = d < 0.0;
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
@ -1257,11 +1396,11 @@ impl CadCommand for ArcCSACommand {
} else { } else {
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
} }
let ea = angle_xy(self.c, pt); let ea = angle_xy(self.c, pt, self.plane);
Some(if self.cw { Some(if self.cw {
arc_preview(self.c, self.r, ea, self.sa) arc_preview(self.c, self.r, ea, self.sa, self.plane)
} else { } else {
arc_preview(self.c, self.r, self.sa, ea) arc_preview(self.c, self.r, self.sa, ea, self.plane)
}) })
} }
_ => None, _ => None,
@ -1278,6 +1417,7 @@ pub struct ArcCSLCommand {
s: DVec3, s: DVec3,
r: f64, r: f64,
sa: f64, sa: f64,
plane: WorkingPlane,
} }
impl ArcCSLCommand { impl ArcCSLCommand {
@ -1288,11 +1428,16 @@ impl ArcCSLCommand {
s: DVec3::ZERO, s: DVec3::ZERO,
r: 0.0, r: 0.0,
sa: 0.0, sa: 0.0,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArcCSLCommand { impl CadCommand for ArcCSLCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_CSL" "ARC_CSL"
} }
@ -1320,14 +1465,20 @@ impl CadCommand for ArcCSLCommand {
1 => { 1 => {
self.s = pt; self.s = pt;
self.r = self.c.distance(pt); self.r = self.c.distance(pt);
self.sa = angle_xy(self.c, pt); self.sa = angle_xy(self.c, pt, self.plane);
self.step = 2; self.step = 2;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let chord = self.s.distance(pt); let chord = self.s.distance(pt);
let ea = end_angle_from_chord_len(self.sa, chord, self.r); let ea = end_angle_from_chord_len(self.sa, chord, self.r);
CmdResult::CommitAndExit(make_arc(self.c, self.r, self.sa, ea)) CmdResult::CommitAndExit(make_arc(
self.c,
self.r,
self.sa,
ea,
self.plane,
))
} }
} }
} }
@ -1346,7 +1497,7 @@ impl CadCommand for ArcCSLCommand {
if chord > 0.0 { if chord > 0.0 {
let ea = end_angle_from_chord_len(self.sa, chord, self.r); let ea = end_angle_from_chord_len(self.sa, chord, self.r);
return Some(CmdResult::CommitAndExit(make_arc( return Some(CmdResult::CommitAndExit(make_arc(
self.c, self.r, self.sa, ea, self.c, self.r, self.sa, ea, self.plane,
))); )));
} }
} }
@ -1374,7 +1525,7 @@ impl CadCommand for ArcCSLCommand {
2 => { 2 => {
let chord = self.s.distance(pt); let chord = self.s.distance(pt);
let ea = end_angle_from_chord_len(self.sa, chord, self.r); let ea = end_angle_from_chord_len(self.sa, chord, self.r);
Some(arc_preview(self.c, self.r, self.sa, ea)) Some(arc_preview(self.c, self.r, self.sa, ea, self.plane))
} }
_ => None, _ => None,
} }
@ -1396,6 +1547,7 @@ pub struct ArcContCommand {
/// committed arc was drawn from, so undoing a segment resumes the run /// committed arc was drawn from, so undoing a segment resumes the run
/// from the prior anchor instead of ending it. /// from the prior anchor instead of ending it.
history: Vec<(DVec3, DVec3)>, history: Vec<(DVec3, DVec3)>,
plane: WorkingPlane,
} }
impl ArcContCommand { impl ArcContCommand {
@ -1404,12 +1556,17 @@ impl ArcContCommand {
s, s,
tangent, tangent,
ctrl: false, ctrl: false,
plane: WorkingPlane::default(),
history: Vec::new(), history: Vec::new(),
} }
} }
} }
impl CadCommand for ArcContCommand { impl CadCommand for ArcContCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARC_CONT" "ARC_CONT"
} }
@ -1420,9 +1577,9 @@ impl CadCommand for ArcContCommand {
self.ctrl = ctrl; self.ctrl = ctrl;
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match arc_continue(self.s, self.tangent, pt, self.ctrl) { match arc_continue(self.s, self.tangent, pt, self.ctrl, self.plane) {
Some((center, radius, sa, ea)) => { Some((center, radius, sa, ea)) => {
let arc = make_arc(center, radius, sa, ea); let arc = make_arc(center, radius, sa, ea, self.plane);
// Remember the anchor this arc was drawn from — mid-command // Remember the anchor this arc was drawn from — mid-command
// Ctrl+Z reverts the commit and resumes from here. // Ctrl+Z reverts the commit and resumes from here.
self.history.push((self.s, self.tangent)); self.history.push((self.s, self.tangent));
@ -1456,8 +1613,10 @@ impl CadCommand for ArcContCommand {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match arc_continue(self.s, self.tangent, pt, self.ctrl) { match arc_continue(self.s, self.tangent, pt, self.ctrl, self.plane) {
Some((center, radius, sa, ea)) => Some(arc_preview(center, radius, sa, ea)), Some((center, radius, sa, ea)) => {
Some(arc_preview(center, radius, sa, ea, self.plane))
}
None => Some(line_wire(self.s, pt)), None => Some(line_wire(self.s, pt)),
} }
} }

View file

@ -12,7 +12,7 @@ use acadrust::EntityType;
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
enum Step { enum Step {
@ -37,6 +37,7 @@ pub struct AttdefCommand {
text_style: String, text_style: String,
width_factor: f64, width_factor: f64,
oblique_angle: f64, oblique_angle: f64,
plane: WorkingPlane,
} }
impl AttdefCommand { impl AttdefCommand {
@ -52,11 +53,16 @@ impl AttdefCommand {
text_style, text_style,
width_factor, width_factor,
oblique_angle, oblique_angle,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for AttdefCommand { impl CadCommand for AttdefCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ATTDEF" "ATTDEF"
} }
@ -134,11 +140,12 @@ impl CadCommand for AttdefCommand {
default, default,
} = &self.step } = &self.step
{ {
let point = self.plane.to_local(pt);
let mut attdef = AttributeDefinition { let mut attdef = AttributeDefinition {
tag: tag.clone(), tag: tag.clone(),
prompt: prompt.clone(), prompt: prompt.clone(),
default_value: default.clone(), default_value: default.clone(),
insertion_point: Vector3::new(pt.x, pt.y, pt.z), insertion_point: Vector3::new(point.x, point.y, point.z),
height: self.height, height: self.height,
text_style: self.text_style.clone(), text_style: self.text_style.clone(),
width_factor: self.width_factor, width_factor: self.width_factor,
@ -146,7 +153,10 @@ impl CadCommand for AttdefCommand {
..Default::default() ..Default::default()
}; };
attdef.common.layer = "0".into(); attdef.common.layer = "0".into();
CmdResult::CommitAndExit(EntityType::AttributeDefinition(attdef)) CmdResult::CommitAndExit(
self.plane
.place_entity(EntityType::AttributeDefinition(attdef)),
)
} else { } else {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -177,12 +187,19 @@ impl CadCommand for AttdefCommand {
} }
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3(); fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if !matches!(self.step, Step::Insertion { .. }) { if !matches!(self.step, Step::Insertion { .. }) {
return None; return None;
} }
// Show a small cross at the insertion point. // Show a small cross at the insertion point.
let d = 0.15_f32; let d = 0.15;
let points = [
pt - self.plane.x * d,
pt + self.plane.x * d,
DVec3::splat(f64::NAN),
pt - self.plane.y * d,
pt + self.plane.y * d,
];
Some(WireModel { Some(WireModel {
taper_widths: Vec::new(), taper_widths: Vec::new(),
world_width: 0.0, world_width: 0.0,
@ -195,13 +212,10 @@ impl CadCommand for AttdefCommand {
dash_align_end: None, dash_align_end: None,
text_verts: Vec::new(), text_verts: Vec::new(),
name: "attdef_preview".into(), name: "attdef_preview".into(),
points: vec![ points: points
[pt.x - d, pt.y, pt.z], .iter()
[pt.x + d, pt.y, pt.z], .map(|point| point.as_vec3().to_array())
[f32::NAN, 0.0, 0.0], .collect(),
[pt.x, pt.y, pt.z - d],
[pt.x, pt.y, pt.z + d],
],
points_low: Vec::new(), points_low: Vec::new(),
color: WireModel::CYAN, color: WireModel::CYAN,
selected: false, selected: false,

View file

@ -19,7 +19,7 @@ use acadrust::{EntityType, Handle, Line};
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
// ── Ribbon definition ───────────────────────────────────────────────────── // ── Ribbon definition ─────────────────────────────────────────────────────
@ -191,6 +191,7 @@ pub struct CenterLineCommand {
/// The entity injected by the host before `on_entity_pick` runs; `None` /// The entity injected by the host before `on_entity_pick` runs; `None`
/// until the host injects it. /// until the host injects it.
picked: Option<EntityType>, picked: Option<EntityType>,
plane: WorkingPlane,
} }
impl CenterLineCommand { impl CenterLineCommand {
@ -198,19 +199,29 @@ impl CenterLineCommand {
Self { Self {
first: None, first: None,
picked: None, picked: None,
plane: WorkingPlane::default(),
} }
} }
/// Extract a `LineGeom` from a picked entity; `None` for anything else. /// Extract a `LineGeom` from a picked entity; `None` for anything else.
fn as_line(entity: &EntityType) -> Option<LineGeom> { fn as_line(&self, entity: &EntityType) -> Option<LineGeom> {
match entity { match entity {
EntityType::Line(l) => Some(LineGeom::from_line(l)), EntityType::Line(line) => {
let mut geom = LineGeom::from_line(line);
geom.start = self.plane.to_local(geom.start);
geom.end = self.plane.to_local(geom.end);
Some(geom)
}
_ => None, _ => None,
} }
} }
} }
impl CadCommand for CenterLineCommand { impl CadCommand for CenterLineCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"CENTERLINE" "CENTERLINE"
} }
@ -241,7 +252,7 @@ impl CadCommand for CenterLineCommand {
} }
// Resolve the just-picked entity; ignore the pick if it isn't a Line. // Resolve the just-picked entity; ignore the pick if it isn't a Line.
let picked = self.picked.take(); let picked = self.picked.take();
let geom = match picked.as_ref().and_then(Self::as_line) { let geom = match picked.as_ref().and_then(|entity| self.as_line(entity)) {
Some(g) => g, Some(g) => g,
None => return CmdResult::NeedPoint, None => return CmdResult::NeedPoint,
}; };
@ -257,7 +268,9 @@ impl CadCommand for CenterLineCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Some(first) => match compute_center_line(first, geom) { Some(first) => match compute_center_line(first, geom) {
Some(line) => CmdResult::CommitAndExit(EntityType::Line(line)), Some(line) => CmdResult::CommitAndExit(
self.plane.place_entity(EntityType::Line(line)),
),
// Degenerate combination (coincident / no usable result): // Degenerate combination (coincident / no usable result):
// keep prompting for a usable second line. // keep prompting for a usable second line.
None => CmdResult::NeedPoint, None => CmdResult::NeedPoint,

View file

@ -12,7 +12,7 @@ use acadrust::types::Vector3;
use acadrust::{Circle, EntityType}; use acadrust::{Circle, EntityType};
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult, DynField, TangentObject}; use crate::command::{CadCommand, CmdResult, DynField, TangentObject, WorkingPlane};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -59,16 +59,18 @@ pub const ICON: IconKind = ICON_CR;
// ── Shared geometry ──────────────────────────────────────────────────────── // ── Shared geometry ────────────────────────────────────────────────────────
fn circle_wire(center: DVec3, radius: f64) -> WireModel { fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel {
let segs = 64u32; let segs = 64u32;
let mut pts: Vec<[f64; 3]> = (0..=segs) let mut pts: Vec<[f64; 3]> = (0..=segs)
.map(|i| { .map(|i| {
let a = (i as f64) * TAU / segs as f64; let a = (i as f64) * TAU / segs as f64;
[ let point = center
center.x + radius * a.cos(), + plane.vector_to_world(DVec3::new(
center.y + radius * a.sin(), radius * a.cos(),
center.z, radius * a.sin(),
] 0.0,
));
[point.x, point.y, point.z]
}) })
.collect(); .collect();
if let Some(first) = pts.first().cloned() { if let Some(first) = pts.first().cloned() {
@ -77,17 +79,24 @@ fn circle_wire(center: DVec3, radius: f64) -> WireModel {
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false) WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
} }
fn make_circle(center: DVec3, radius: f64) -> EntityType { fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType {
EntityType::Circle(Circle { let center = plane.to_local(center);
plane.place_entity(EntityType::Circle(Circle {
center: Vector3::new(center.x, center.y, center.z), center: Vector3::new(center.x, center.y, center.z),
radius, radius,
..Default::default() ..Default::default()
}) }))
} }
/// Circumscribed circle through three points. /// Circumscribed circle through three points.
/// Returns `None` if the points are collinear. /// Returns `None` if the points are collinear.
fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> { fn circumcircle(
a: DVec3,
b: DVec3,
c: DVec3,
plane: WorkingPlane,
) -> Option<(DVec3, f64)> {
let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c));
let ax = a.x; let ax = a.x;
let ay = a.y; let ay = a.y;
let bx = b.x; let bx = b.x;
@ -107,7 +116,25 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
+ (cx * cx + cy * cy) * (bx - ax)) + (cx * cx + cy * cy) * (bx - ax))
/ d; / d;
let center = DVec3::new(ux, uy, a.z); let center = DVec3::new(ux, uy, a.z);
Some((center, center.distance(a))) Some((plane.to_world(center), center.distance(a)))
}
fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 {
let d = plane.vector_to_local(b - a);
d.x.hypot(d.y)
}
fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject {
match object {
TangentObject::Line { p1, p2 } => TangentObject::Line {
p1: plane.to_local(p1),
p2: plane.to_local(p2),
},
TangentObject::Circle { center, radius } => TangentObject::Circle {
center: plane.to_local(center),
radius,
},
}
} }
// ── Command: Center, Radius ──────────────────────────────────────────────── // ── Command: Center, Radius ────────────────────────────────────────────────
@ -115,6 +142,7 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
pub struct CircleCommand { pub struct CircleCommand {
step: StepCR, step: StepCR,
default_r: f64, default_r: f64,
plane: WorkingPlane,
} }
enum StepCR { enum StepCR {
Center, Center,
@ -126,11 +154,16 @@ impl CircleCommand {
Self { Self {
step: StepCR::Center, step: StepCR::Center,
default_r: defaults::get_circle_radius(), default_r: defaults::get_circle_radius(),
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for CircleCommand { impl CadCommand for CircleCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"CIRCLE" "CIRCLE"
} }
@ -176,9 +209,9 @@ impl CadCommand for CircleCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
StepCR::Radius(c) => { StepCR::Radius(c) => {
let r = c.distance(pt); let r = plane_distance(*c, pt, self.plane);
defaults::set_circle_radius(r); defaults::set_circle_radius(r);
CmdResult::CommitAndExit(make_circle(*c, r)) CmdResult::CommitAndExit(make_circle(*c, r, self.plane))
} }
} }
} }
@ -186,7 +219,7 @@ impl CadCommand for CircleCommand {
if let StepCR::Radius(c) = &self.step { if let StepCR::Radius(c) = &self.step {
let c = *c; let c = *c;
let r = self.default_r; let r = self.default_r;
return CmdResult::CommitAndExit(make_circle(c, r)); return CmdResult::CommitAndExit(make_circle(c, r, self.plane));
} }
CmdResult::Cancel CmdResult::Cancel
} }
@ -195,7 +228,7 @@ impl CadCommand for CircleCommand {
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if let StepCR::Radius(c) = &self.step { if let StepCR::Radius(c) = &self.step {
Some(circle_wire(*c, c.distance(pt))) Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane))
} else { } else {
None None
} }
@ -217,7 +250,7 @@ impl CadCommand for CircleCommand {
let r: f64 = text.trim().replace(',', ".").parse().ok()?; let r: f64 = text.trim().replace(',', ".").parse().ok()?;
if r > 0.0 { if r > 0.0 {
defaults::set_circle_radius(r); defaults::set_circle_radius(r);
return Some(CmdResult::CommitAndExit(make_circle(*c, r))); return Some(CmdResult::CommitAndExit(make_circle(*c, r, self.plane)));
} }
} }
None None
@ -251,6 +284,7 @@ impl CadCommand for CircleCommand {
pub struct CircleCDCommand { pub struct CircleCDCommand {
step: StepCR, step: StepCR,
default_d: f64, default_d: f64,
plane: WorkingPlane,
} }
impl CircleCDCommand { impl CircleCDCommand {
@ -258,11 +292,16 @@ impl CircleCDCommand {
Self { Self {
step: StepCR::Center, step: StepCR::Center,
default_d: defaults::get_circle_diam(), default_d: defaults::get_circle_diam(),
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for CircleCDCommand { impl CadCommand for CircleCDCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"CIRCLE_CD" "CIRCLE_CD"
} }
@ -283,9 +322,9 @@ impl CadCommand for CircleCDCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
StepCR::Radius(c) => { StepCR::Radius(c) => {
let d = c.distance(pt) * 2.0; let d = plane_distance(*c, pt, self.plane) * 2.0;
defaults::set_circle_diam(d); defaults::set_circle_diam(d);
CmdResult::CommitAndExit(make_circle(*c, d / 2.0)) CmdResult::CommitAndExit(make_circle(*c, d / 2.0, self.plane))
} }
} }
} }
@ -293,7 +332,7 @@ impl CadCommand for CircleCDCommand {
if let StepCR::Radius(c) = &self.step { if let StepCR::Radius(c) = &self.step {
let c = *c; let c = *c;
let d = self.default_d; let d = self.default_d;
return CmdResult::CommitAndExit(make_circle(c, (d / 2.0) as f64)); return CmdResult::CommitAndExit(make_circle(c, d / 2.0, self.plane));
} }
CmdResult::Cancel CmdResult::Cancel
} }
@ -303,7 +342,7 @@ impl CadCommand for CircleCDCommand {
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
// Preview radius = distance to cursor; on commit that distance becomes the radius (diameter = 2x). // Preview radius = distance to cursor; on commit that distance becomes the radius (diameter = 2x).
if let StepCR::Radius(c) = &self.step { if let StepCR::Radius(c) = &self.step {
Some(circle_wire(*c, c.distance(pt))) Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane))
} else { } else {
None None
} }
@ -313,7 +352,7 @@ impl CadCommand for CircleCDCommand {
let d: f64 = text.trim().replace(',', ".").parse().ok()?; let d: f64 = text.trim().replace(',', ".").parse().ok()?;
if d > 0.0 { if d > 0.0 {
defaults::set_circle_diam(d); defaults::set_circle_diam(d);
return Some(CmdResult::CommitAndExit(make_circle(*c, d / 2.0))); return Some(CmdResult::CommitAndExit(make_circle(*c, d / 2.0, self.plane)));
} }
} }
None None
@ -339,15 +378,23 @@ impl CadCommand for CircleCDCommand {
pub struct Circle2PCommand { pub struct Circle2PCommand {
p1: Option<DVec3>, p1: Option<DVec3>,
plane: WorkingPlane,
} }
impl Circle2PCommand { impl Circle2PCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { p1: None } Self {
p1: None,
plane: WorkingPlane::default(),
}
} }
} }
impl CadCommand for Circle2PCommand { impl CadCommand for Circle2PCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"CIRCLE_2P" "CIRCLE_2P"
} }
@ -366,8 +413,8 @@ impl CadCommand for Circle2PCommand {
} }
Some(p1) => { Some(p1) => {
let center = (p1 + pt) * 0.5; let center = (p1 + pt) * 0.5;
let radius = p1.distance(pt) / 2.0; let radius = plane_distance(p1, pt, self.plane) / 2.0;
CmdResult::CommitAndExit(make_circle(center, radius)) CmdResult::CommitAndExit(make_circle(center, radius, self.plane))
} }
} }
} }
@ -380,8 +427,8 @@ impl CadCommand for Circle2PCommand {
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let p1 = self.p1?; let p1 = self.p1?;
let center = (p1 + pt) * 0.5; let center = (p1 + pt) * 0.5;
let radius = p1.distance(pt) / 2.0; let radius = plane_distance(p1, pt, self.plane) / 2.0;
Some(circle_wire(center, radius)) Some(circle_wire(center, radius, self.plane))
} }
} }
@ -389,15 +436,23 @@ impl CadCommand for Circle2PCommand {
pub struct Circle3PCommand { pub struct Circle3PCommand {
pts: Vec<DVec3>, pts: Vec<DVec3>,
plane: WorkingPlane,
} }
impl Circle3PCommand { impl Circle3PCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { pts: Vec::new() } Self {
pts: Vec::new(),
plane: WorkingPlane::default(),
}
} }
} }
impl CadCommand for Circle3PCommand { impl CadCommand for Circle3PCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"CIRCLE_3P" "CIRCLE_3P"
} }
@ -414,8 +469,10 @@ impl CadCommand for Circle3PCommand {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]); let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]);
match circumcircle(a, b, c) { match circumcircle(a, b, c, self.plane) {
Some((center, radius)) => CmdResult::CommitAndExit(make_circle(center, radius)), Some((center, radius)) => {
CmdResult::CommitAndExit(make_circle(center, radius, self.plane))
}
None => { None => {
self.pts.pop(); self.pts.pop();
CmdResult::NeedPoint CmdResult::NeedPoint
@ -435,14 +492,14 @@ impl CadCommand for Circle3PCommand {
// Show circle preview with p1→cursor as diameter (same as 2P). // Show circle preview with p1→cursor as diameter (same as 2P).
let p1 = self.pts[0]; let p1 = self.pts[0];
let center = (p1 + pt) * 0.5; let center = (p1 + pt) * 0.5;
let radius = p1.distance(pt) / 2.0; let radius = plane_distance(p1, pt, self.plane) / 2.0;
Some(circle_wire(center, radius)) Some(circle_wire(center, radius, self.plane))
} }
_ => { _ => {
// Show circumcircle preview if non-collinear, else polyline. // Show circumcircle preview if non-collinear, else polyline.
let (a, b) = (self.pts[0], self.pts[1]); let (a, b) = (self.pts[0], self.pts[1]);
if let Some((center, radius)) = circumcircle(a, b, pt) { if let Some((center, radius)) = circumcircle(a, b, pt, self.plane) {
Some(circle_wire(center, radius)) Some(circle_wire(center, radius, self.plane))
} else { } else {
Some(WireModel::solid_f64( Some(WireModel::solid_f64(
"rubber_band".into(), "rubber_band".into(),
@ -860,6 +917,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64)
pub struct CircleTTRCommand { pub struct CircleTTRCommand {
step: StepTTR, step: StepTTR,
plane: WorkingPlane,
} }
enum StepTTR { enum StepTTR {
@ -880,11 +938,16 @@ impl CircleTTRCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: StepTTR::First, step: StepTTR::First,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for CircleTTRCommand { impl CadCommand for CircleTTRCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"CIRCLE_TTR" "CIRCLE_TTR"
} }
@ -918,6 +981,8 @@ impl CadCommand for CircleTTRCommand {
} }
fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult { fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult {
let obj = tangent_object_local(obj, self.plane);
let hit = self.plane.to_local(hit);
match &self.step { match &self.step {
StepTTR::First => { StepTTR::First => {
self.step = StepTTR::Second { self.step = StepTTR::Second {
@ -955,7 +1020,11 @@ impl CadCommand for CircleTTRCommand {
let hint = (*hit1 + *hit2) * 0.5; let hint = (*hit1 + *hit2) * 0.5;
let candidates = ttr_candidates(*obj1, *obj2, r); let candidates = ttr_candidates(*obj1, *obj2, r);
if let Some(center) = best_of(&candidates, hint) { if let Some(center) = best_of(&candidates, hint) {
Some(CmdResult::CommitAndExit(make_circle(center, r))) Some(CmdResult::CommitAndExit(make_circle(
self.plane.to_world(center),
r,
self.plane,
)))
} else { } else {
Some(CmdResult::Cancel) Some(CmdResult::Cancel)
} }
@ -980,6 +1049,7 @@ impl CadCommand for CircleTTRCommand {
pub struct CircleTTTCommand { pub struct CircleTTTCommand {
objs: Vec<TangentObject>, objs: Vec<TangentObject>,
hits: Vec<DVec3>, hits: Vec<DVec3>,
plane: WorkingPlane,
} }
impl CircleTTTCommand { impl CircleTTTCommand {
@ -987,11 +1057,16 @@ impl CircleTTTCommand {
Self { Self {
objs: Vec::new(), objs: Vec::new(),
hits: Vec::new(), hits: Vec::new(),
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for CircleTTTCommand { impl CadCommand for CircleTTTCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"CIRCLE_TTT" "CIRCLE_TTT"
} }
@ -1013,15 +1088,19 @@ impl CadCommand for CircleTTTCommand {
} }
fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult { fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult {
self.objs.push(obj); self.objs.push(tangent_object_local(obj, self.plane));
self.hits.push(hit); self.hits.push(self.plane.to_local(hit));
if self.objs.len() < 3 { if self.objs.len() < 3 {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
let hint = self.hits.iter().fold(DVec3::ZERO, |a, &b| a + b) / 3.0; let hint = self.hits.iter().fold(DVec3::ZERO, |a, &b| a + b) / 3.0;
let candidates = ttt_candidates(self.objs[0], self.objs[1], self.objs[2]); let candidates = ttt_candidates(self.objs[0], self.objs[1], self.objs[2]);
match best_circle_of(&candidates, hint) { match best_circle_of(&candidates, hint) {
Some((center, r)) => CmdResult::CommitAndExit(make_circle(center, r)), Some((center, r)) => CmdResult::CommitAndExit(make_circle(
self.plane.to_world(center),
r,
self.plane,
)),
None => { None => {
self.objs.pop(); self.objs.pop();
self.hits.pop(); self.hits.pop();

View file

@ -41,28 +41,46 @@ impl DimCenterCommand {
} }
/// Extract (center, radius) from a Circle or Arc; `None` for anything else. /// Extract (center, radius) from a Circle or Arc; `None` for anything else.
fn center_radius(entity: &EntityType) -> Option<(Vector3, f64)> { fn center_radius(entity: &EntityType) -> Option<(DVec3, f64, DVec3, DVec3)> {
match entity { match entity {
EntityType::Circle(c) => Some((c.center, c.radius)), EntityType::Circle(circle) => {
EntityType::Arc(a) => Some((a.center, a.radius)), let normal = (circle.normal.x, circle.normal.y, circle.normal.z);
let center = crate::scene::view::transform::ocs_point_to_wcs(
(circle.center.x, circle.center.y, circle.center.z),
normal,
);
let (x, y) = crate::scene::view::transform::ocs_axes(normal);
Some((
DVec3::new(center.0, center.1, center.2),
circle.radius,
DVec3::new(x.0, x.1, x.2),
DVec3::new(y.0, y.1, y.2),
))
}
EntityType::Arc(arc) => {
let normal = (arc.normal.x, arc.normal.y, arc.normal.z);
let center = crate::scene::view::transform::ocs_point_to_wcs(
(arc.center.x, arc.center.y, arc.center.z),
normal,
);
let (x, y) = crate::scene::view::transform::ocs_axes(normal);
Some((
DVec3::new(center.0, center.1, center.2),
arc.radius,
DVec3::new(x.0, x.1, x.2),
DVec3::new(y.0, y.1, y.2),
))
}
_ => None, _ => None,
} }
} }
/// Build the two cross lines from a center and radius. /// Build the two cross lines from a center and radius.
fn build_cross(center: Vector3, radius: f64) -> Vec<EntityType> { fn build_cross(center: DVec3, radius: f64, x: DVec3, y: DVec3) -> Vec<EntityType> {
let m = radius * 0.2; let m = radius * 0.2;
let cx = center.x; let to_vector = |point: DVec3| Vector3::new(point.x, point.y, point.z);
let cy = center.y; let horizontal = Line::from_points(to_vector(center - x * m), to_vector(center + x * m));
let cz = center.z; let vertical = Line::from_points(to_vector(center - y * m), to_vector(center + y * m));
let horizontal = Line::from_points(
Vector3::new(cx - m, cy, cz),
Vector3::new(cx + m, cy, cz),
);
let vertical = Line::from_points(
Vector3::new(cx, cy - m, cz),
Vector3::new(cx, cy + m, cz),
);
vec![EntityType::Line(horizontal), EntityType::Line(vertical)] vec![EntityType::Line(horizontal), EntityType::Line(vertical)]
} }
} }
@ -93,8 +111,8 @@ impl CadCommand for DimCenterCommand {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
match self.picked.as_ref().and_then(Self::center_radius) { match self.picked.as_ref().and_then(Self::center_radius) {
Some((center, radius)) => { Some((center, radius, x, y)) => {
let lines = Self::build_cross(center, radius); let lines = Self::build_cross(center, radius, x, y);
CmdResult::ReplaceMany(vec![], lines) CmdResult::ReplaceMany(vec![], lines)
} }
// Picked something that is not a circle or arc — keep prompting. // Picked something that is not a circle or arc — keep prompting.

View file

@ -15,12 +15,13 @@ use acadrust::EntityType;
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
pub struct DonutCommand { pub struct DonutCommand {
state: DonutState, state: DonutState,
inner_r: f64, inner_r: f64,
outer_r: f64, outer_r: f64,
plane: WorkingPlane,
} }
enum DonutState { enum DonutState {
@ -35,11 +36,16 @@ impl DonutCommand {
state: DonutState::AskInner, state: DonutState::AskInner,
inner_r: 0.0, inner_r: 0.0,
outer_r: 1.0, outer_r: 1.0,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for DonutCommand { impl CadCommand for DonutCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"DONUT" "DONUT"
} }
@ -97,9 +103,10 @@ impl CadCommand for DonutCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.state { match &self.state {
DonutState::PlaceCenter => { DonutState::PlaceCenter => {
let entity = make_donut(pt.x as f64, pt.y as f64, self.inner_r, self.outer_r); let center = self.plane.to_local(pt);
let entity = make_donut(center.x, center.y, center.z, self.inner_r, self.outer_r);
// Keep command active so user can place more donuts. // Keep command active so user can place more donuts.
CmdResult::CommitEntity(entity) CmdResult::CommitEntity(self.plane.place_entity(entity))
} }
_ => CmdResult::NeedPoint, _ => CmdResult::NeedPoint,
} }
@ -119,7 +126,7 @@ impl CadCommand for DonutCommand {
} }
} }
fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType { fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType {
use acadrust::types::Vector2; use acadrust::types::Vector2;
let r_avg = (inner_r + outer_r) / 2.0; let r_avg = (inner_r + outer_r) / 2.0;
let width = outer_r - inner_r; let width = outer_r - inner_r;
@ -127,6 +134,7 @@ fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType {
let mut p = LwPolyline::new(); let mut p = LwPolyline::new();
p.is_closed = true; p.is_closed = true;
p.constant_width = width; p.constant_width = width;
p.elevation = elevation;
// Vertex at (cx - r, cy) with bulge 1.0 (180° CCW arc to next vertex) // Vertex at (cx - r, cy) with bulge 1.0 (180° CCW arc to next vertex)
let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy)); let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy));

View file

@ -9,7 +9,7 @@ use acadrust::types::Vector3;
use acadrust::{Ellipse, EntityType}; use acadrust::{Ellipse, EntityType};
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::DVec3; use glam::DVec3;
@ -57,13 +57,14 @@ fn ellipse_wire(
ratio: f64, // minor/major ratio: f64, // minor/major
t_start: f64, t_start: f64,
t_end: f64, t_end: f64,
plane: WorkingPlane,
) -> WireModel { ) -> WireModel {
let r_major = major.length(); let r_major = major.length();
if r_major < 1e-9 { if r_major < 1e-9 {
return WireModel::solid("rubber_band".into(), vec![], WireModel::CYAN, false); return WireModel::solid("rubber_band".into(), vec![], WireModel::CYAN, false);
} }
let major_dir = major / r_major; let major_dir = major / r_major;
let v = DVec3::Z.cross(major_dir).normalize(); let v = plane.z.cross(major_dir).normalize_or(plane.y);
let segs = 64u32; let segs = 64u32;
// Unwrap t_end so the arc goes counter-clockwise. // Unwrap t_end so the arc goes counter-clockwise.
let t_e = if t_end <= t_start { t_end + TAU } else { t_end }; let t_e = if t_end <= t_start { t_end + TAU } else { t_end };
@ -88,13 +89,21 @@ fn param_angle(center: DVec3, major_dir: DVec3, v: DVec3, pt: DVec3, ratio: f64)
} }
/// Build the final Ellipse entity. /// Build the final Ellipse entity.
fn make_ellipse(center: DVec3, major: DVec3, ratio: f64, t_start: f64, t_end: f64) -> Ellipse { fn make_ellipse(
center: DVec3,
major: DVec3,
ratio: f64,
t_start: f64,
t_end: f64,
plane: WorkingPlane,
) -> Ellipse {
Ellipse { Ellipse {
center: Vector3::new(center.x, center.y, center.z), center: Vector3::new(center.x, center.y, center.z),
major_axis: Vector3::new(major.x, major.y, major.z), major_axis: Vector3::new(major.x, major.y, major.z),
minor_axis_ratio: ratio, minor_axis_ratio: ratio,
start_parameter: t_start, start_parameter: t_start,
end_parameter: t_end, end_parameter: t_end,
normal: Vector3::new(plane.z.x, plane.z.y, plane.z.z),
..Default::default() ..Default::default()
} }
} }
@ -110,17 +119,23 @@ enum CtrStep {
pub struct EllipseCommand { pub struct EllipseCommand {
step: CtrStep, step: CtrStep,
plane: WorkingPlane,
} }
impl EllipseCommand { impl EllipseCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: CtrStep::Center, step: CtrStep::Center,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for EllipseCommand { impl CadCommand for EllipseCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ELLIPSE" "ELLIPSE"
} }
@ -170,7 +185,7 @@ impl CadCommand for EllipseCommand {
let (center, major) = (*center, *major); let (center, major) = (*center, *major);
let ratio = minor_ratio(center, major, pt); let ratio = minor_ratio(center, major, pt);
CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
center, major, ratio, 0.0, TAU, center, major, ratio, 0.0, TAU, self.plane,
))) )))
} }
} }
@ -195,7 +210,7 @@ impl CadCommand for EllipseCommand {
if r_minor > 0.0 { if r_minor > 0.0 {
let ratio = (r_minor / major.length()).clamp(1e-6, 1.0); let ratio = (r_minor / major.length()).clamp(1e-6, 1.0);
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
*center, *major, ratio, 0.0, TAU, *center, *major, ratio, 0.0, TAU, self.plane,
)))); ))));
} }
} }
@ -207,7 +222,7 @@ impl CadCommand for EllipseCommand {
CtrStep::MajorAxis { center } => Some(line_wire(*center, pt)), CtrStep::MajorAxis { center } => Some(line_wire(*center, pt)),
CtrStep::MinorRatio { center, major } => { CtrStep::MinorRatio { center, major } => {
let ratio = minor_ratio(*center, *major, pt).max(0.001); let ratio = minor_ratio(*center, *major, pt).max(0.001);
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU)) Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane))
} }
_ => None, _ => None,
} }
@ -250,17 +265,23 @@ enum AxisStep {
pub struct EllipseAxisCommand { pub struct EllipseAxisCommand {
step: AxisStep, step: AxisStep,
plane: WorkingPlane,
} }
impl EllipseAxisCommand { impl EllipseAxisCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: AxisStep::Pt1, step: AxisStep::Pt1,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for EllipseAxisCommand { impl CadCommand for EllipseAxisCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ELLIPSE_AXIS" "ELLIPSE_AXIS"
} }
@ -297,7 +318,7 @@ impl CadCommand for EllipseAxisCommand {
let (center, major) = (*center, *major); let (center, major) = (*center, *major);
let ratio = minor_ratio(center, major, pt); let ratio = minor_ratio(center, major, pt);
CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
center, major, ratio, 0.0, TAU, center, major, ratio, 0.0, TAU, self.plane,
))) )))
} }
} }
@ -313,7 +334,7 @@ impl CadCommand for EllipseAxisCommand {
if r_minor > 0.0 { if r_minor > 0.0 {
let ratio = (r_minor / major.length()).clamp(1e-6, 1.0); let ratio = (r_minor / major.length()).clamp(1e-6, 1.0);
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
*center, *major, ratio, 0.0, TAU, *center, *major, ratio, 0.0, TAU, self.plane,
)))); ))));
} }
} }
@ -326,7 +347,7 @@ impl CadCommand for EllipseAxisCommand {
AxisStep::Pt2 { p1 } => Some(line_wire(*p1, pt)), AxisStep::Pt2 { p1 } => Some(line_wire(*p1, pt)),
AxisStep::MinorRatio { center, major } => { AxisStep::MinorRatio { center, major } => {
let ratio = minor_ratio(*center, *major, pt).max(0.001); let ratio = minor_ratio(*center, *major, pt).max(0.001);
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU)) Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane))
} }
} }
} }
@ -385,6 +406,7 @@ pub struct EllipseArcCommand {
step: ArcStep, step: ArcStep,
prev_pt: Option<DVec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
plane: WorkingPlane,
} }
impl EllipseArcCommand { impl EllipseArcCommand {
@ -393,11 +415,16 @@ impl EllipseArcCommand {
step: ArcStep::Center, step: ArcStep::Center,
prev_pt: None, prev_pt: None,
cw: false, cw: false,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for EllipseArcCommand { impl CadCommand for EllipseArcCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ELLIPSE_ARC" "ELLIPSE_ARC"
} }
@ -458,7 +485,7 @@ impl CadCommand for EllipseArcCommand {
ratio, ratio,
} => { } => {
let (center, major, ratio) = (*center, *major, *ratio); let (center, major, ratio) = (*center, *major, *ratio);
let t_start = angle_from_point(center, major, ratio, pt); let t_start = angle_from_point(center, major, ratio, pt, self.plane);
self.prev_pt = None; // reset direction tracking for end-angle step self.prev_pt = None; // reset direction tracking for end-angle step
self.step = ArcStep::EndAngle { self.step = ArcStep::EndAngle {
center, center,
@ -475,11 +502,11 @@ impl CadCommand for EllipseArcCommand {
t_start, t_start,
} => { } => {
let (center, major, ratio, t_start) = (*center, *major, *ratio, *t_start); let (center, major, ratio, t_start) = (*center, *major, *ratio, *t_start);
let t_end = angle_from_point(center, major, ratio, pt); let t_end = angle_from_point(center, major, ratio, pt, self.plane);
let entity = if self.cw { let entity = if self.cw {
make_ellipse(center, major, ratio, t_end, t_start) make_ellipse(center, major, ratio, t_end, t_start, self.plane)
} else { } else {
make_ellipse(center, major, ratio, t_start, t_end) make_ellipse(center, major, ratio, t_start, t_end, self.plane)
}; };
CmdResult::CommitAndExit(EntityType::Ellipse(entity)) CmdResult::CommitAndExit(EntityType::Ellipse(entity))
} }
@ -530,7 +557,7 @@ impl CadCommand for EllipseArcCommand {
// Typed degrees: positive = CCW, negative = CW. // Typed degrees: positive = CCW, negative = CW.
let t_end = val.to_radians(); let t_end = val.to_radians();
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
*center, *major, *ratio, *t_start, t_end, *center, *major, *ratio, *t_start, t_end, self.plane,
)))); ))));
} }
_ => {} _ => {}
@ -543,7 +570,7 @@ impl CadCommand for EllipseArcCommand {
ArcStep::MajorAxis { center } => Some(line_wire(*center, pt)), ArcStep::MajorAxis { center } => Some(line_wire(*center, pt)),
ArcStep::MinorRatio { center, major } => { ArcStep::MinorRatio { center, major } => {
let ratio = minor_ratio(*center, *major, pt).max(0.001); let ratio = minor_ratio(*center, *major, pt).max(0.001);
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU)) Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane))
} }
ArcStep::StartAngle { center, .. } => { ArcStep::StartAngle { center, .. } => {
// Only the start angle is being chosen here — show a line from // Only the start angle is being chosen here — show a line from
@ -564,8 +591,8 @@ impl CadCommand for EllipseArcCommand {
// tolerance ignores jitter; the reference advances only on a // tolerance ignores jitter; the reference advances only on a
// clear move so slow sweeps accumulate. // clear move so slow sweeps accumulate.
if let Some(prev) = self.prev_pt { if let Some(prev) = self.prev_pt {
let t_prev = angle_from_point(*center, *major, *ratio, prev); let t_prev = angle_from_point(*center, *major, *ratio, prev, self.plane);
let t_cur = angle_from_point(*center, *major, *ratio, pt); let t_cur = angle_from_point(*center, *major, *ratio, pt, self.plane);
let mut d = t_cur - t_prev; let mut d = t_cur - t_prev;
while d > std::f64::consts::PI { while d > std::f64::consts::PI {
d -= TAU; d -= TAU;
@ -580,11 +607,11 @@ impl CadCommand for EllipseArcCommand {
} else { } else {
self.prev_pt = Some(pt); self.prev_pt = Some(pt);
} }
let t_end = angle_from_point(*center, *major, *ratio, pt); let t_end = angle_from_point(*center, *major, *ratio, pt, self.plane);
Some(if self.cw { Some(if self.cw {
ellipse_wire(*center, *major, *ratio, t_end, *t_start) ellipse_wire(*center, *major, *ratio, t_end, *t_start, self.plane)
} else { } else {
ellipse_wire(*center, *major, *ratio, *t_start, t_end) ellipse_wire(*center, *major, *ratio, *t_start, t_end, self.plane)
}) })
} }
_ => None, _ => None,
@ -638,13 +665,19 @@ fn minor_ratio(center: DVec3, major: DVec3, pt: DVec3) -> f64 {
(r_minor / r_major).clamp(1e-6, 1.0) (r_minor / r_major).clamp(1e-6, 1.0)
} }
fn angle_from_point(center: DVec3, major: DVec3, ratio: f64, pt: DVec3) -> f64 { fn angle_from_point(
center: DVec3,
major: DVec3,
ratio: f64,
pt: DVec3,
plane: WorkingPlane,
) -> f64 {
let r_major = major.length(); let r_major = major.length();
if r_major < 1e-9 { if r_major < 1e-9 {
return 0.0; return 0.0;
} }
let major_dir = major / r_major; let major_dir = major / r_major;
let v = DVec3::Z.cross(major_dir).normalize(); let v = plane.z.cross(major_dir).normalize_or(plane.y);
param_angle(center, major_dir, v, pt, ratio) param_angle(center, major_dir, v, pt, ratio)
} }

View file

@ -17,7 +17,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult, DynField}; use crate::command::{CadCommand, CmdResult, DynField, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::DVec3; use glam::DVec3;
@ -58,6 +58,7 @@ pub struct HelixCommand {
top_radius: f64, top_radius: f64,
height: f64, height: f64,
turns: f64, turns: f64,
plane: WorkingPlane,
} }
impl HelixCommand { impl HelixCommand {
@ -69,6 +70,7 @@ impl HelixCommand {
top_radius: 0.0, top_radius: 0.0,
height: 0.0, height: 0.0,
turns: DEFAULT_TURNS, turns: DEFAULT_TURNS,
plane: WorkingPlane::default(),
} }
} }
@ -82,11 +84,12 @@ impl HelixCommand {
let angle = turns * std::f64::consts::TAU * frac; let angle = turns * std::f64::consts::TAU * frac;
let radius = base_radius + (top_radius - base_radius) * frac; let radius = base_radius + (top_radius - base_radius) * frac;
let z = height * frac; let z = height * frac;
pts.push(DVec3::new( pts.push(
self.center.x + radius * angle.cos(), self.center
self.center.y + radius * angle.sin(), + self.plane.x * (radius * angle.cos())
self.center.z + z, + self.plane.y * (radius * angle.sin())
)); + self.plane.z * z,
);
} }
pts pts
} }
@ -110,6 +113,10 @@ impl HelixCommand {
} }
impl CadCommand for HelixCommand { impl CadCommand for HelixCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"HELIX" "HELIX"
} }
@ -134,7 +141,8 @@ impl CadCommand for HelixCommand {
Step::BaseRadius => { Step::BaseRadius => {
// A point click resolves the base radius as the planar distance // A point click resolves the base radius as the planar distance
// from the centre to the picked point. // from the centre to the picked point.
let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt(); let delta = self.plane.vector_to_local(pt - self.center);
let r = delta.x.hypot(delta.y);
if r > 0.0 { if r > 0.0 {
self.base_radius = r; self.base_radius = r;
self.step = Step::TopRadius; self.step = Step::TopRadius;
@ -142,14 +150,15 @@ impl CadCommand for HelixCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::TopRadius => { Step::TopRadius => {
let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt(); let delta = self.plane.vector_to_local(pt - self.center);
let r = delta.x.hypot(delta.y);
self.top_radius = if r > 0.0 { r } else { self.base_radius }; self.top_radius = if r > 0.0 { r } else { self.base_radius };
self.step = Step::Height; self.step = Step::Height;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::Height => { Step::Height => {
// Use the elevation of the picked point relative to the centre. // Use the elevation of the picked point relative to the centre.
let h = pt.z - self.center.z; let h = (pt - self.center).dot(self.plane.z);
if h.abs() > 0.0 { if h.abs() > 0.0 {
self.height = h; self.height = h;
self.step = Step::Turns; self.step = Step::Turns;
@ -250,7 +259,8 @@ impl CadCommand for HelixCommand {
match self.step { match self.step {
Step::Center => None, Step::Center => None,
Step::BaseRadius => { Step::BaseRadius => {
let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt(); let delta = self.plane.vector_to_local(pt - self.center);
let r = delta.x.hypot(delta.y);
if r <= 0.0 { if r <= 0.0 {
return None; return None;
} }
@ -267,12 +277,13 @@ impl CadCommand for HelixCommand {
// Use the parameters fixed so far; for the height step let the // Use the parameters fixed so far; for the height step let the
// cursor elevation drive the live height. // cursor elevation drive the live height.
let height = if self.step == Step::Height { let height = if self.step == Step::Height {
pt.z - self.center.z (pt - self.center).dot(self.plane.z)
} else { } else {
self.height self.height
}; };
let top = if self.step == Step::TopRadius { let top = if self.step == Step::TopRadius {
let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt(); let delta = self.plane.vector_to_local(pt - self.center);
let r = delta.x.hypot(delta.y);
if r > 0.0 { if r > 0.0 {
r r
} else { } else {

View file

@ -11,7 +11,7 @@ use acadrust::EntityType;
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
pub struct MlineCommand { pub struct MlineCommand {
@ -21,6 +21,7 @@ pub struct MlineCommand {
style_name: String, style_name: String,
style_handle: Option<acadrust::Handle>, style_handle: Option<acadrust::Handle>,
style_element_count: usize, style_element_count: usize,
plane: WorkingPlane,
} }
impl MlineCommand { impl MlineCommand {
@ -33,6 +34,7 @@ impl MlineCommand {
style_name: "Standard".into(), style_name: "Standard".into(),
style_handle: None, style_handle: None,
style_element_count: 2, style_element_count: 2,
plane: WorkingPlane::default(),
} }
} }
@ -48,11 +50,16 @@ impl MlineCommand {
style_name: style_name.into(), style_name: style_name.into(),
style_handle, style_handle,
style_element_count: style_element_count.max(1), style_element_count: style_element_count.max(1),
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for MlineCommand { impl CadCommand for MlineCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"MLINE" "MLINE"
} }
@ -105,14 +112,18 @@ impl CadCommand for MlineCommand {
// Close command // Close command
if (up == "C" || up == "CLOSE") && self.points.len() >= 3 { if (up == "C" || up == "CLOSE") && self.points.len() >= 3 {
let entity = build_mline( let entity = build_mline(
&self.points, &self
.points
.iter()
.map(|point| self.plane.to_local(*point))
.collect::<Vec<_>>(),
self.scale, self.scale,
true, true,
&self.style_name, &self.style_name,
self.style_handle, self.style_handle,
self.style_element_count, self.style_element_count,
); );
return Some(CmdResult::CommitAndExit(entity)); return Some(CmdResult::CommitAndExit(self.plane.place_entity(entity)));
} }
// Scale: "S" alone → prompt for value // Scale: "S" alone → prompt for value
@ -144,14 +155,18 @@ impl CadCommand for MlineCommand {
return CmdResult::Cancel; return CmdResult::Cancel;
} }
let entity = build_mline( let entity = build_mline(
&self.points, &self
.points
.iter()
.map(|point| self.plane.to_local(*point))
.collect::<Vec<_>>(),
self.scale, self.scale,
false, false,
&self.style_name, &self.style_name,
self.style_handle, self.style_handle,
self.style_element_count, self.style_element_count,
); );
CmdResult::CommitAndExit(entity) CmdResult::CommitAndExit(self.plane.place_entity(entity))
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {

View file

@ -15,7 +15,7 @@ use acadrust::{EntityType, Handle, LwPolyline};
use glam::{DVec2, DVec3, Vec2, Vec3}; use glam::{DVec2, DVec3, Vec2, Vec3};
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -52,6 +52,7 @@ pub struct PlineCommand {
/// document makes the partial polyline snappable while later vertices are /// document makes the partial polyline snappable while later vertices are
/// being placed. (#119) /// being placed. (#119)
live_handle: Option<Handle>, live_handle: Option<Handle>,
plane: WorkingPlane,
} }
impl PlineCommand { impl PlineCommand {
@ -62,6 +63,7 @@ impl PlineCommand {
mode: SegMode::Line, mode: SegMode::Line,
last_tangent: None, last_tangent: None,
live_handle: None, live_handle: None,
plane: WorkingPlane::default(),
} }
} }
@ -78,7 +80,11 @@ impl PlineCommand {
// following Arc segment stays tangent-continuous. // following Arc segment stays tangent-continuous.
let n = self.vertices.len(); let n = self.vertices.len();
self.last_tangent = if n >= 2 { self.last_tangent = if n >= 2 {
seg_exit_tangent(self.vertices[n - 2], self.vertices[n - 1], self.bulges[n - 2]) seg_exit_tangent(
self.plane.to_local(self.vertices[n - 2]),
self.plane.to_local(self.vertices[n - 1]),
self.bulges[n - 2],
)
} else { } else {
None None
}; };
@ -116,8 +122,12 @@ impl PlineCommand {
if self.vertices.len() < 2 { if self.vertices.len() < 2 {
return None; return None;
} }
let lw_verts: Vec<LwVertex> = self let local: Vec<DVec3> = self
.vertices .vertices
.iter()
.map(|vertex| self.plane.to_local(*vertex))
.collect();
let lw_verts: Vec<LwVertex> = local
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, v)| { .map(|(i, v)| {
@ -128,10 +138,11 @@ impl PlineCommand {
.collect(); .collect();
let pline = LwPolyline { let pline = LwPolyline {
vertices: lw_verts, vertices: lw_verts,
elevation: local.first().map_or(0.0, |point| point.z),
is_closed: closed, is_closed: closed,
..Default::default() ..Default::default()
}; };
Some(EntityType::LwPolyline(pline)) Some(self.plane.place_entity(EntityType::LwPolyline(pline)))
} }
} }
@ -250,6 +261,10 @@ pub(crate) fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[
// ── CadCommand impl ──────────────────────────────────────────────────────── // ── CadCommand impl ────────────────────────────────────────────────────────
impl CadCommand for PlineCommand { impl CadCommand for PlineCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"PLINE" "PLINE"
} }
@ -291,11 +306,13 @@ impl CadCommand for PlineCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
if !self.vertices.is_empty() { if !self.vertices.is_empty() {
let last = *self.vertices.last().unwrap(); let last = *self.vertices.last().unwrap();
let last_local = self.plane.to_local(last);
let pt_local = self.plane.to_local(pt);
let last_idx = self.vertices.len() - 1; let last_idx = self.vertices.len() - 1;
let bulge = match self.mode { let bulge = match self.mode {
SegMode::Line => { SegMode::Line => {
let d = DVec2::new(pt.x - last.x, pt.y - last.y); let d = DVec2::new(pt_local.x - last_local.x, pt_local.y - last_local.y);
if d.length_squared() > 1e-10 { if d.length_squared() > 1e-10 {
// Direction only — f32 is sufficient for tangent continuity. // Direction only — f32 is sufficient for tangent continuity.
self.last_tangent = Some(d.normalize().as_vec2()); self.last_tangent = Some(d.normalize().as_vec2());
@ -303,8 +320,8 @@ impl CadCommand for PlineCommand {
0.0 0.0
} }
SegMode::Arc => { SegMode::Arc => {
let a = DVec2::new(last.x, last.y); let a = DVec2::new(last_local.x, last_local.y);
let b = DVec2::new(pt.x, pt.y); let b = DVec2::new(pt_local.x, pt_local.y);
let tangent = self let tangent = self
.last_tangent .last_tangent
.map(|t| t.as_dvec2()) .map(|t| t.as_dvec2())
@ -417,11 +434,13 @@ impl CadCommand for PlineCommand {
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let pt = pt.as_vec3(); let pt_world = pt;
// The committed vertices already render as a real document entity, so // The committed vertices already render as a real document entity, so
// the preview is just the pending segment from the last vertex to the // the preview is just the pending segment from the last vertex to the
// cursor. (#119) Downcast to f32 for the pixel-space rubber band. // cursor. (#119) Downcast to f32 for the pixel-space rubber band.
let last = self.vertices.last()?.as_vec3(); let last_world = *self.vertices.last()?;
let last = self.plane.to_local(last_world).as_vec3();
let pt = self.plane.to_local(pt_world).as_vec3();
let mut pts: Vec<[f32; 3]> = Vec::new(); let mut pts: Vec<[f32; 3]> = Vec::new();
match self.mode { match self.mode {
@ -442,9 +461,13 @@ impl CadCommand for PlineCommand {
} }
} }
let world_points = pts
.into_iter()
.map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array())
.collect();
Some(WireModel::solid( Some(WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
pts, world_points,
WireModel::CYAN, WireModel::CYAN,
false, false,
)) ))

View file

@ -12,7 +12,7 @@ use acadrust::EntityType;
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
pub struct ImageCommand { pub struct ImageCommand {
@ -20,6 +20,7 @@ pub struct ImageCommand {
pixel_width: u32, pixel_width: u32,
pixel_height: u32, pixel_height: u32,
origin: Option<DVec3>, origin: Option<DVec3>,
plane: WorkingPlane,
} }
impl ImageCommand { impl ImageCommand {
@ -29,6 +30,7 @@ impl ImageCommand {
pixel_width, pixel_width,
pixel_height, pixel_height,
origin: None, origin: None,
plane: WorkingPlane::default(),
} }
} }
@ -41,6 +43,8 @@ impl ImageCommand {
} }
fn make_entity(&self, origin: DVec3, width_pt: DVec3) -> EntityType { fn make_entity(&self, origin: DVec3, width_pt: DVec3) -> EntityType {
let origin = self.plane.to_local(origin);
let width_pt = self.plane.to_local(width_pt);
let world_width = (width_pt.x - origin.x).abs().max(0.001); let world_width = (width_pt.x - origin.x).abs().max(0.001);
let world_height = world_width / self.aspect(); let world_height = world_width / self.aspect();
@ -56,11 +60,15 @@ impl ImageCommand {
); );
img.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE img.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE
| acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY; | acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
EntityType::RasterImage(img) self.plane.place_entity(EntityType::RasterImage(img))
} }
} }
impl CadCommand for ImageCommand { impl CadCommand for ImageCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"IMAGE" "IMAGE"
} }
@ -91,7 +99,7 @@ impl CadCommand for ImageCommand {
// If origin is set, place with a default width of 1 unit * pixel count / 100 // If origin is set, place with a default width of 1 unit * pixel count / 100
if let Some(origin) = self.origin { if let Some(origin) = self.origin {
let default_w = (self.pixel_width as f64 / 100.0).max(1.0); let default_w = (self.pixel_width as f64 / 100.0).max(1.0);
let width_pt = DVec3::new(origin.x + default_w, origin.y, origin.z); let width_pt = origin + self.plane.x * default_w;
let entity = self.make_entity(origin, width_pt); let entity = self.make_entity(origin, width_pt);
CmdResult::CommitAndExit(entity) CmdResult::CommitAndExit(entity)
} else { } else {
@ -99,15 +107,20 @@ impl CadCommand for ImageCommand {
} }
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3(); fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let origin = self.origin?.as_vec3(); let origin = self.origin?;
let world_width = (pt.x - origin.x).abs().max(0.001); let origin_local = self.plane.to_local(origin);
let world_height = world_width / self.aspect() as f32; let point_local = self.plane.to_local(pt);
let width = (point_local.x - origin_local.x).abs().max(0.001);
let p0 = [origin.x, origin.y, origin.z]; let height = width / self.aspect();
let p1 = [origin.x + world_width, origin.y, origin.z]; let corners = [
let p2 = [origin.x + world_width, origin.y + world_height, origin.z]; origin_local,
let p3 = [origin.x, origin.y + world_height, origin.z]; origin_local + DVec3::X * width,
origin_local + DVec3::new(width, height, 0.0),
origin_local + DVec3::Y * height,
]
.map(|point| self.plane.to_world(point).as_vec3().to_array());
let [p0, p1, p2, p3] = corners;
Some(WireModel { Some(WireModel {
taper_widths: Vec::new(), taper_widths: Vec::new(),

View file

@ -10,7 +10,7 @@ use acadrust::{entities::LwVertex, EntityType};
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -30,6 +30,7 @@ const DEFAULT_ARC_LEN: f64 = 1.0; // default arc bump length
pub struct RevCloudCommand { pub struct RevCloudCommand {
points: Vec<DVec3>, points: Vec<DVec3>,
arc_length: f64, arc_length: f64,
plane: WorkingPlane,
} }
impl RevCloudCommand { impl RevCloudCommand {
@ -37,11 +38,16 @@ impl RevCloudCommand {
Self { Self {
points: vec![], points: vec![],
arc_length: DEFAULT_ARC_LEN, arc_length: DEFAULT_ARC_LEN,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for RevCloudCommand { impl CadCommand for RevCloudCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"REVCLOUD" "REVCLOUD"
} }
@ -71,8 +77,13 @@ impl CadCommand for RevCloudCommand {
if self.points.len() < 3 { if self.points.len() < 3 {
return CmdResult::Cancel; return CmdResult::Cancel;
} }
let entity = make_revcloud(&self.points, self.arc_length); let local: Vec<DVec3> = self
CmdResult::CommitAndExit(entity) .points
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let entity = make_revcloud(&local, self.arc_length);
CmdResult::CommitAndExit(self.plane.place_entity(entity))
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
@ -153,6 +164,7 @@ fn make_revcloud(pts: &[DVec3], arc_len: f64) -> EntityType {
let mut p = LwPolyline::new(); let mut p = LwPolyline::new();
p.is_closed = true; p.is_closed = true;
p.elevation = pts.first().map_or(0.0, |point| point.z);
p.vertices = vertices; p.vertices = vertices;
EntityType::LwPolyline(p) EntityType::LwPolyline(p)
} }

View file

@ -15,35 +15,32 @@ use acadrust::types::Vector2;
use acadrust::{EntityType, LwPolyline}; use acadrust::{EntityType, LwPolyline};
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Mat4}; use glam::DVec3;
/// Build the four corners of an axis-aligned box between opposite corners `a` /// Build the four corners of an axis-aligned box between opposite corners `a`
/// and `b`, axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = /// and `b`, axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity =
/// world). The two given corners stay put; the other two are placed square to /// world). The two given corners stay put; the other two are placed square to
/// the UCS axes instead of the world axes. /// the UCS axes instead of the world axes.
fn ucs_box_corners(a: DVec3, b: DVec3, ucs: Mat4) -> [DVec3; 4] { fn ucs_box_corners(a: DVec3, b: DVec3, plane: WorkingPlane) -> [DVec3; 4] {
let ucs = ucs.as_dmat4(); let au = plane.to_local(a);
let inv = ucs.inverse(); let bu = plane.to_local(b);
let au = inv.transform_point3(a);
let bu = inv.transform_point3(b);
[ [
ucs.transform_point3(DVec3::new(au.x, au.y, au.z)), plane.to_world(DVec3::new(au.x, au.y, au.z)),
ucs.transform_point3(DVec3::new(bu.x, au.y, au.z)), plane.to_world(DVec3::new(bu.x, au.y, au.z)),
ucs.transform_point3(DVec3::new(bu.x, bu.y, au.z)), plane.to_world(DVec3::new(bu.x, bu.y, au.z)),
ucs.transform_point3(DVec3::new(au.x, bu.y, au.z)), plane.to_world(DVec3::new(au.x, bu.y, au.z)),
] ]
} }
/// Four corners of a box centred at `c` with half-extents taken from `corner`, /// Four corners of a box centred at `c` with half-extents taken from `corner`,
/// axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = world). /// axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = world).
fn ucs_box_around_center(c: DVec3, corner: DVec3, ucs: Mat4) -> [DVec3; 4] { fn ucs_box_around_center(c: DVec3, corner: DVec3, plane: WorkingPlane) -> [DVec3; 4] {
let ucs = ucs.as_dmat4(); let d = plane.vector_to_local(corner - c);
let d = ucs.inverse().transform_vector3(corner - c); let rx = plane.x * d.x.abs();
let rx = ucs.transform_vector3(DVec3::new(d.x.abs(), 0.0, 0.0)); let ry = plane.y * d.y.abs();
let ry = ucs.transform_vector3(DVec3::new(0.0, d.y.abs(), 0.0));
[c - rx - ry, c + rx - ry, c + rx + ry, c - rx + ry] [c - rx - ry, c + rx - ry, c + rx + ry, c - rx + ry]
} }
@ -84,15 +81,18 @@ pub const ICON: IconKind = ICON_RECT;
// ── Shared geometry helpers ──────────────────────────────────────────────── // ── Shared geometry helpers ────────────────────────────────────────────────
fn make_pline(xy_pairs: &[[f64; 2]]) -> EntityType { fn make_pline(points: &[DVec3], plane: WorkingPlane) -> EntityType {
EntityType::LwPolyline(LwPolyline { let local: Vec<DVec3> = points.iter().map(|point| plane.to_local(*point)).collect();
vertices: xy_pairs let elevation = local.first().map_or(0.0, |point| point.z);
plane.place_entity(EntityType::LwPolyline(LwPolyline {
vertices: local
.iter() .iter()
.map(|&[x, y]| LwVertex::new(Vector2::new(x, y))) .map(|point| LwVertex::new(Vector2::new(point.x, point.y)))
.collect(), .collect(),
elevation,
is_closed: true, is_closed: true,
..Default::default() ..Default::default()
}) }))
} }
fn wire_loop(pts: Vec<[f64; 3]>) -> WireModel { fn wire_loop(pts: Vec<[f64; 3]>) -> WireModel {
@ -114,46 +114,57 @@ fn wire_seg(a: DVec3, b: DVec3) -> WireModel {
// ── Polygon geometry ─────────────────────────────────────────────────────── // ── Polygon geometry ───────────────────────────────────────────────────────
fn poly_verts_xy(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> Vec<[f64; 2]> { fn poly_verts(
center: DVec3,
vertex_r: f64,
sides: u32,
start_angle: f64,
plane: WorkingPlane,
) -> Vec<DVec3> {
(0..sides) (0..sides)
.map(|i| { .map(|i| {
let a = start_angle + (i as f64) * TAU / sides as f64; let a = start_angle + (i as f64) * TAU / sides as f64;
[ center + plane.x * (vertex_r * a.cos()) + plane.y * (vertex_r * a.sin())
center.x + vertex_r * a.cos(),
center.y + vertex_r * a.sin(),
]
}) })
.collect() .collect()
} }
fn poly_wire(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> WireModel { fn poly_wire(
let pts: Vec<[f64; 3]> = (0..sides) center: DVec3,
.map(|i| { vertex_r: f64,
let a = start_angle + (i as f64) * TAU / sides as f64; sides: u32,
[ start_angle: f64,
center.x + vertex_r * a.cos(), plane: WorkingPlane,
center.y + vertex_r * a.sin(), ) -> WireModel {
center.z, let pts: Vec<[f64; 3]> = poly_verts(center, vertex_r, sides, start_angle, plane)
] .into_iter()
}) .map(|point| [point.x, point.y, point.z])
.collect(); .collect();
wire_loop(pts) wire_loop(pts)
} }
fn angle_xy(from: DVec3, to: DVec3) -> f64 { fn angle_xy(from: DVec3, to: DVec3, plane: WorkingPlane) -> f64 {
(to.y - from.y).atan2(to.x - from.x) plane.angle(from, to).unwrap_or(0.0)
}
fn plane_distance(from: DVec3, to: DVec3, plane: WorkingPlane) -> f64 {
let delta = plane.vector_to_local(to - from);
delta.x.hypot(delta.y)
} }
// ── Command: Rectangle — Two Corners (RECT) ────────────────────────────── // ── Command: Rectangle — Two Corners (RECT) ──────────────────────────────
pub struct RectCommand { pub struct RectCommand {
a: Option<DVec3>, a: Option<DVec3>,
ucs: Mat4, plane: WorkingPlane,
} }
impl RectCommand { impl RectCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { a: None, ucs: Mat4::IDENTITY } Self {
a: None,
plane: WorkingPlane::default(),
}
} }
} }
@ -161,8 +172,8 @@ impl CadCommand for RectCommand {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"RECT" "RECT"
} }
fn set_ucs(&mut self, ucs: Mat4) { fn set_working_plane(&mut self, plane: WorkingPlane) {
self.ucs = ucs; self.plane = plane;
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
if self.a.is_none() { if self.a.is_none() {
@ -209,13 +220,8 @@ impl CadCommand for RectCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Some(a) => { Some(a) => {
let c = ucs_box_corners(a, pt, self.ucs); let c = ucs_box_corners(a, pt, self.plane);
CmdResult::CommitAndExit(make_pline(&[ CmdResult::CommitAndExit(make_pline(&c, self.plane))
[c[0].x, c[0].y],
[c[1].x, c[1].y],
[c[2].x, c[2].y],
[c[3].x, c[3].y],
]))
} }
} }
} }
@ -227,7 +233,7 @@ impl CadCommand for RectCommand {
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let a = self.a?; let a = self.a?;
let c = ucs_box_corners(a, pt, self.ucs); let c = ucs_box_corners(a, pt, self.plane);
Some(wire_loop(vec![ Some(wire_loop(vec![
[c[0].x, c[0].y, c[0].z], [c[0].x, c[0].y, c[0].z],
[c[1].x, c[1].y, c[1].z], [c[1].x, c[1].y, c[1].z],
@ -261,6 +267,7 @@ pub struct RectRotCommand {
step: u8, step: u8,
a: DVec3, a: DVec3,
b: DVec3, b: DVec3,
plane: WorkingPlane,
} }
impl RectRotCommand { impl RectRotCommand {
@ -269,11 +276,16 @@ impl RectRotCommand {
step: 0, step: 0,
a: DVec3::ZERO, a: DVec3::ZERO,
b: DVec3::ZERO, b: DVec3::ZERO,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for RectRotCommand { impl CadCommand for RectRotCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"RECT_ROT" "RECT_ROT"
} }
@ -297,18 +309,18 @@ impl CadCommand for RectRotCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let (a, b) = (self.a, self.b); let (a, b, pt) = (
self.plane.to_local(self.a),
self.plane.to_local(self.b),
self.plane.to_local(pt),
);
let dir = (b - a).normalize_or_zero(); let dir = (b - a).normalize_or_zero();
let perp = DVec3::new(-dir.y, dir.x, 0.0); let perp = DVec3::new(-dir.y, dir.x, 0.0);
let h = (pt - b).dot(perp); // signed height let h = (pt - b).dot(perp); // signed height
let c = b + perp * h; let c = b + perp * h;
let d = a + perp * h; let d = a + perp * h;
CmdResult::CommitAndExit(make_pline(&[ let corners = [a, b, c, d].map(|point| self.plane.to_world(point));
[a.x, a.y], CmdResult::CommitAndExit(make_pline(&corners, self.plane))
[b.x, b.y],
[c.x, c.y],
[d.x, d.y],
]))
} }
} }
} }
@ -322,18 +334,20 @@ impl CadCommand for RectRotCommand {
match self.step { match self.step {
1 => Some(wire_seg(self.a, pt)), 1 => Some(wire_seg(self.a, pt)),
2 => { 2 => {
let (a, b) = (self.a, self.b); let (a, b, pt) = (
self.plane.to_local(self.a),
self.plane.to_local(self.b),
self.plane.to_local(pt),
);
let dir = (b - a).normalize_or_zero(); let dir = (b - a).normalize_or_zero();
let perp = DVec3::new(-dir.y, dir.x, 0.0); let perp = DVec3::new(-dir.y, dir.x, 0.0);
let h = (pt - b).dot(perp); let h = (pt - b).dot(perp);
let c = b + perp * h; let c = b + perp * h;
let d = a + perp * h; let d = a + perp * h;
Some(wire_loop(vec![ let points = [a, b, c, d].map(|point| self.plane.to_world(point));
[a.x, a.y, a.z], Some(wire_loop(
[b.x, b.y, b.z], points.into_iter().map(|p| [p.x, p.y, p.z]).collect(),
[c.x, c.y, c.z], ))
[d.x, d.y, d.z],
]))
} }
_ => None, _ => None,
} }
@ -356,9 +370,12 @@ impl CadCommand for RectRotCommand {
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
// Live height = perpendicular distance from the cursor to the base edge. // Live height = perpendicular distance from the cursor to the base edge.
(self.step == 2).then(|| { (self.step == 2).then(|| {
let dir = (self.b - self.a).normalize_or_zero(); let dir = self
.plane
.vector_to_local(self.b - self.a)
.normalize_or_zero();
let perp = DVec3::new(-dir.y, dir.x, 0.0); let perp = DVec3::new(-dir.y, dir.x, 0.0);
(cursor - self.b).dot(perp).abs() self.plane.vector_to_local(cursor - self.b).dot(perp).abs()
}) })
} }
} }
@ -369,12 +386,15 @@ impl CadCommand for RectRotCommand {
pub struct RectCenCommand { pub struct RectCenCommand {
center: Option<DVec3>, center: Option<DVec3>,
ucs: Mat4, plane: WorkingPlane,
} }
impl RectCenCommand { impl RectCenCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { center: None, ucs: Mat4::IDENTITY } Self {
center: None,
plane: WorkingPlane::default(),
}
} }
} }
@ -382,8 +402,8 @@ impl CadCommand for RectCenCommand {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"RECT_CEN" "RECT_CEN"
} }
fn set_ucs(&mut self, ucs: Mat4) { fn set_working_plane(&mut self, plane: WorkingPlane) {
self.ucs = ucs; self.plane = plane;
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
if self.center.is_none() { if self.center.is_none() {
@ -399,13 +419,8 @@ impl CadCommand for RectCenCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Some(c) => { Some(c) => {
let q = ucs_box_around_center(c, pt, self.ucs); let q = ucs_box_around_center(c, pt, self.plane);
CmdResult::CommitAndExit(make_pline(&[ CmdResult::CommitAndExit(make_pline(&q, self.plane))
[q[0].x, q[0].y],
[q[1].x, q[1].y],
[q[2].x, q[2].y],
[q[3].x, q[3].y],
]))
} }
} }
} }
@ -417,7 +432,7 @@ impl CadCommand for RectCenCommand {
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let c = self.center?; let c = self.center?;
let q = ucs_box_around_center(c, pt, self.ucs); let q = ucs_box_around_center(c, pt, self.plane);
Some(wire_loop(vec![ Some(wire_loop(vec![
[q[0].x, q[0].y, q[0].z], [q[0].x, q[0].y, q[0].z],
[q[1].x, q[1].y, q[1].z], [q[1].x, q[1].y, q[1].z],
@ -449,6 +464,7 @@ pub struct PolyCommand {
sides: u32, sides: u32,
step: u8, step: u8,
center: DVec3, center: DVec3,
plane: WorkingPlane,
} }
impl PolyCommand { impl PolyCommand {
@ -457,11 +473,16 @@ impl PolyCommand {
sides: 6, sides: 6,
step: 0, step: 0,
center: DVec3::ZERO, center: DVec3::ZERO,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for PolyCommand { impl CadCommand for PolyCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"POLY" "POLY"
} }
@ -555,9 +576,10 @@ impl CadCommand for PolyCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let r = self.center.distance(pt); let r = plane_distance(self.center, pt, self.plane);
let sa = angle_xy(self.center, pt); let sa = angle_xy(self.center, pt, self.plane);
CmdResult::CommitAndExit(make_pline(&poly_verts_xy(self.center, r, self.sides, sa))) let vertices = poly_verts(self.center, r, self.sides, sa, self.plane);
CmdResult::CommitAndExit(make_pline(&vertices, self.plane))
} }
} }
} }
@ -577,9 +599,9 @@ impl CadCommand for PolyCommand {
if self.step < 2 { if self.step < 2 {
return None; return None;
} }
let r = self.center.distance(pt); let r = plane_distance(self.center, pt, self.plane);
let sa = angle_xy(self.center, pt); let sa = angle_xy(self.center, pt, self.plane);
Some(poly_wire(self.center, r, self.sides, sa)) Some(poly_wire(self.center, r, self.sides, sa, self.plane))
} }
} }
@ -591,6 +613,7 @@ pub struct PolyCCommand {
sides: u32, sides: u32,
step: u8, step: u8,
center: DVec3, center: DVec3,
plane: WorkingPlane,
} }
impl PolyCCommand { impl PolyCCommand {
@ -599,11 +622,16 @@ impl PolyCCommand {
sides: 6, sides: 6,
step: 0, step: 0,
center: DVec3::ZERO, center: DVec3::ZERO,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for PolyCCommand { impl CadCommand for PolyCCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"POLY_C" "POLY_C"
} }
@ -669,18 +697,20 @@ impl CadCommand for PolyCCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
let inradius = self.center.distance(pt); let inradius = plane_distance(self.center, pt, self.plane);
let vr = inradius / (PI / self.sides as f64).cos(); let vr = inradius / (PI / self.sides as f64).cos();
// The picked pt is at the midpoint of an edge; the vertex is // The picked pt is at the midpoint of an edge; the vertex is
// offset by half a sector (π/N) from that direction. // offset by half a sector (π/N) from that direction.
let edge_angle = angle_xy(self.center, pt); let edge_angle = angle_xy(self.center, pt, self.plane);
let sa = edge_angle + PI / self.sides as f64; let sa = edge_angle + PI / self.sides as f64;
CmdResult::CommitAndExit(make_pline(&poly_verts_xy( let vertices = poly_verts(
self.center, self.center,
vr, vr,
self.sides, self.sides,
sa, sa,
))) self.plane,
);
CmdResult::CommitAndExit(make_pline(&vertices, self.plane))
} }
} }
} }
@ -700,10 +730,10 @@ impl CadCommand for PolyCCommand {
if self.step < 2 { if self.step < 2 {
return None; return None;
} }
let inradius = self.center.distance(pt); let inradius = plane_distance(self.center, pt, self.plane);
let vr = inradius / (PI / self.sides as f64).cos(); let vr = inradius / (PI / self.sides as f64).cos();
let sa = angle_xy(self.center, pt) + PI / self.sides as f64; let sa = angle_xy(self.center, pt, self.plane) + PI / self.sides as f64;
Some(poly_wire(self.center, vr, self.sides, sa)) Some(poly_wire(self.center, vr, self.sides, sa, self.plane))
} }
} }
@ -715,6 +745,7 @@ pub struct PolyECommand {
sides: u32, sides: u32,
step: u8, step: u8,
a: DVec3, a: DVec3,
plane: WorkingPlane,
} }
impl PolyECommand { impl PolyECommand {
@ -723,11 +754,16 @@ impl PolyECommand {
sides: 6, sides: 6,
step: 0, step: 0,
a: DVec3::ZERO, a: DVec3::ZERO,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for PolyECommand { impl CadCommand for PolyECommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"POLY_E" "POLY_E"
} }
@ -792,8 +828,11 @@ impl CadCommand for PolyECommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
_ => { _ => {
if let Some((center, vr, sa)) = edge_poly_params(self.a, pt, self.sides) { if let Some((center, vr, sa)) =
CmdResult::CommitAndExit(make_pline(&poly_verts_xy(center, vr, self.sides, sa))) edge_poly_params(self.a, pt, self.sides, self.plane)
{
let vertices = poly_verts(center, vr, self.sides, sa, self.plane);
CmdResult::CommitAndExit(make_pline(&vertices, self.plane))
} else { } else {
CmdResult::Cancel CmdResult::Cancel
} }
@ -816,8 +855,10 @@ impl CadCommand for PolyECommand {
if self.step < 2 { if self.step < 2 {
return None; return None;
} }
if let Some((center, vr, sa)) = edge_poly_params(self.a, pt, self.sides) { if let Some((center, vr, sa)) =
Some(poly_wire(center, vr, self.sides, sa)) edge_poly_params(self.a, pt, self.sides, self.plane)
{
Some(poly_wire(center, vr, self.sides, sa, self.plane))
} else { } else {
Some(wire_seg(self.a, pt)) Some(wire_seg(self.a, pt))
} }
@ -826,7 +867,13 @@ impl CadCommand for PolyECommand {
/// Compute polygon center, vertex-radius and start-angle from two edge endpoints. /// Compute polygon center, vertex-radius and start-angle from two edge endpoints.
/// The polygon is placed on the left side of A→B (CCW convention). /// The polygon is placed on the left side of A→B (CCW convention).
fn edge_poly_params(a: DVec3, b: DVec3, sides: u32) -> Option<(DVec3, f64, f64)> { fn edge_poly_params(
a: DVec3,
b: DVec3,
sides: u32,
plane: WorkingPlane,
) -> Option<(DVec3, f64, f64)> {
let (a, b) = (plane.to_local(a), plane.to_local(b));
let edge_len = a.distance(b); let edge_len = a.distance(b);
if edge_len < 1e-6 { if edge_len < 1e-6 {
return None; return None;
@ -841,8 +888,9 @@ fn edge_poly_params(a: DVec3, b: DVec3, sides: u32) -> Option<(DVec3, f64, f64)>
let mid = (a + b) * 0.5; let mid = (a + b) * 0.5;
let center = mid + perp * inradius; let center = mid + perp * inradius;
// First vertex = A // First vertex = A
let sa = angle_xy(center, a); let center_world = plane.to_world(center);
Some((center, vr, sa)) let sa = angle_xy(center_world, plane.to_world(a), plane);
Some((center_world, vr, sa))
} }

View file

@ -17,7 +17,7 @@ use acadrust::types::{Vector2, Vector3};
use acadrust::EntityType; use acadrust::EntityType;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::DVec3; use glam::DVec3;
@ -45,6 +45,7 @@ pub struct SketchCommand {
strokes: Vec<Vec<DVec3>>, strokes: Vec<Vec<DVec3>>,
/// True while the pen is down and samples accumulate into the current stroke. /// True while the pen is down and samples accumulate into the current stroke.
pen_down: bool, pen_down: bool,
plane: WorkingPlane,
} }
impl SketchCommand { impl SketchCommand {
@ -52,15 +53,20 @@ impl SketchCommand {
Self { Self {
strokes: Vec::new(), strokes: Vec::new(),
pen_down: false, pen_down: false,
plane: WorkingPlane::default(),
} }
} }
/// Build one lightweight polyline from a stroke's sampled points, or `None` /// Build one lightweight polyline from a stroke's sampled points, or `None`
/// when the stroke has too few points to form a segment. /// when the stroke has too few points to form a segment.
fn build_stroke(points: &[DVec3]) -> Option<EntityType> { fn build_stroke(&self, points: &[DVec3]) -> Option<EntityType> {
if points.len() < 2 { if points.len() < 2 {
return None; return None;
} }
let points: Vec<DVec3> = points
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let elevation = points[0].z; let elevation = points[0].z;
let mut pl = LwPolyline::new(); let mut pl = LwPolyline::new();
pl.is_closed = false; pl.is_closed = false;
@ -70,19 +76,23 @@ impl SketchCommand {
.map(|p| LwVertex::new(Vector2::new(p.x, p.y))) .map(|p| LwVertex::new(Vector2::new(p.x, p.y)))
.collect(); .collect();
pl.normal = Vector3::UNIT_Z; pl.normal = Vector3::UNIT_Z;
Some(EntityType::LwPolyline(pl)) Some(self.plane.place_entity(EntityType::LwPolyline(pl)))
} }
/// All strokes that have enough points, as committable entities. /// All strokes that have enough points, as committable entities.
fn build_all(&self) -> Vec<EntityType> { fn build_all(&self) -> Vec<EntityType> {
self.strokes self.strokes
.iter() .iter()
.filter_map(|s| Self::build_stroke(s)) .filter_map(|stroke| self.build_stroke(stroke))
.collect() .collect()
} }
} }
impl CadCommand for SketchCommand { impl CadCommand for SketchCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"SKETCH" "SKETCH"
} }

View file

@ -3,7 +3,7 @@
// Command: SOLID (reachable as SO / SOLID2D — the bare SOLID verb currently // Command: SOLID (reachable as SO / SOLID2D — the bare SOLID verb currently
// toggles the shaded display) — pick three or four corner points and commit a // toggles the shaded display) — pick three or four corner points and commit a
// filled triangle or quadrilateral. Four points may be picked either around // filled triangle or quadrilateral. Four points may be picked either around
// the perimeter (the natural order shown by the rubber band) or in AutoCAD's // the perimeter (the natural order shown by the rubber band) or in the
// traditional Z pattern. The command normalizes both forms to the DXF corner // traditional Z pattern. The command normalizes both forms to the DXF corner
// order before committing. Enter after the third point commits a triangle. // order before committing. Enter after the third point commits a triangle.
@ -12,7 +12,7 @@ use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::DVec3; use glam::DVec3;
@ -34,11 +34,15 @@ pub fn tool() -> ToolDef {
pub struct Solid2dCommand { pub struct Solid2dCommand {
/// Corner points picked so far (3 → triangle, 4 → quadrilateral). /// Corner points picked so far (3 → triangle, 4 → quadrilateral).
points: Vec<DVec3>, points: Vec<DVec3>,
plane: WorkingPlane,
} }
impl Solid2dCommand { impl Solid2dCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { points: Vec::new() } Self {
points: Vec::new(),
plane: WorkingPlane::default(),
}
} }
fn v3(p: DVec3) -> Vector3 { fn v3(p: DVec3) -> Vector3 {
@ -86,6 +90,10 @@ impl Solid2dCommand {
} }
impl CadCommand for Solid2dCommand { impl CadCommand for Solid2dCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"SOLID" "SOLID"
} }
@ -112,8 +120,13 @@ impl CadCommand for Solid2dCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.points.push(pt); self.points.push(pt);
if self.points.len() == 4 { if self.points.len() == 4 {
let solid = Self::solid_from_four_points(&self.points); let local: Vec<DVec3> = self
CmdResult::CommitAndExit(EntityType::Solid(solid)) .points
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let solid = Self::solid_from_four_points(&local);
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Solid(solid)))
} else { } else {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -121,9 +134,13 @@ impl CadCommand for Solid2dCommand {
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
if self.points.len() == 3 { if self.points.len() == 3 {
let p = &self.points; let p: Vec<DVec3> = self
.points
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let solid = Solid::triangle(Self::v3(p[0]), Self::v3(p[1]), Self::v3(p[2])); let solid = Solid::triangle(Self::v3(p[0]), Self::v3(p[1]), Self::v3(p[2]));
CmdResult::CommitAndExit(EntityType::Solid(solid)) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Solid(solid)))
} else { } else {
CmdResult::Cancel CmdResult::Cancel
} }

View file

@ -16,7 +16,7 @@ use acadrust::types::{Vector2, Vector3};
use acadrust::EntityType; use acadrust::EntityType;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::DVec3; use glam::DVec3;
@ -45,6 +45,7 @@ pub struct TraceCommand {
/// Every point picked so far, in world space. `points[0]` fixes the band's /// Every point picked so far, in world space. `points[0]` fixes the band's
/// elevation (its Z). /// elevation (its Z).
points: Vec<DVec3>, points: Vec<DVec3>,
plane: WorkingPlane,
} }
impl TraceCommand { impl TraceCommand {
@ -53,6 +54,7 @@ impl TraceCommand {
width: 1.0, width: 1.0,
width_set: false, width_set: false,
points: Vec::new(), points: Vec::new(),
plane: WorkingPlane::default(),
} }
} }
@ -62,13 +64,17 @@ impl TraceCommand {
if self.points.len() < 2 { if self.points.len() < 2 {
return None; return None;
} }
let elevation = self.points[0].z; let local: Vec<DVec3> = self
.points
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let elevation = local[0].z;
let mut pl = LwPolyline::new(); let mut pl = LwPolyline::new();
pl.is_closed = false; pl.is_closed = false;
pl.constant_width = self.width; pl.constant_width = self.width;
pl.elevation = elevation; pl.elevation = elevation;
pl.vertices = self pl.vertices = local
.points
.iter() .iter()
.map(|p| { .map(|p| {
let mut v = LwVertex::new(Vector2::new(p.x, p.y)); let mut v = LwVertex::new(Vector2::new(p.x, p.y));
@ -78,11 +84,15 @@ impl TraceCommand {
}) })
.collect(); .collect();
pl.normal = Vector3::UNIT_Z; pl.normal = Vector3::UNIT_Z;
Some(EntityType::LwPolyline(pl)) Some(self.plane.place_entity(EntityType::LwPolyline(pl)))
} }
} }
impl CadCommand for TraceCommand { impl CadCommand for TraceCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"TRACE" "TRACE"
} }

View file

@ -6,7 +6,7 @@ use acadrust::{EntityType, Handle};
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdOption, CmdResult}; use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -25,6 +25,7 @@ pub struct WipeoutCommand {
mode: WipeoutMode, mode: WipeoutMode,
first: Option<DVec3>, first: Option<DVec3>,
points: Vec<DVec3>, points: Vec<DVec3>,
plane: WorkingPlane,
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
@ -40,6 +41,7 @@ impl WipeoutCommand {
mode: WipeoutMode::Draw, mode: WipeoutMode::Draw,
first: None, first: None,
points: Vec::new(), points: Vec::new(),
plane: WorkingPlane::default(),
} }
} }
@ -48,6 +50,7 @@ impl WipeoutCommand {
mode: WipeoutMode::Polyline, mode: WipeoutMode::Polyline,
first: None, first: None,
points: Vec::new(), points: Vec::new(),
plane: WorkingPlane::default(),
} }
} }
@ -57,12 +60,18 @@ impl WipeoutCommand {
mode: WipeoutMode::Rectangular, mode: WipeoutMode::Rectangular,
first: None, first: None,
points: Vec::new(), points: Vec::new(),
plane: WorkingPlane::default(),
} }
} }
fn finish_draw(&self) -> CmdResult { fn finish_draw(&self) -> CmdResult {
match make_poly_wipeout(&self.points) { let local: Vec<DVec3> = self
Some(entity) => CmdResult::CommitAndExit(entity), .points
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
match make_poly_wipeout(&local) {
Some(entity) => CmdResult::CommitAndExit(self.plane.place_entity(entity)),
None => CmdResult::NeedPoint, None => CmdResult::NeedPoint,
} }
} }
@ -74,6 +83,10 @@ impl WipeoutCommand {
} }
impl CadCommand for WipeoutCommand { impl CadCommand for WipeoutCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"WIPEOUT" "WIPEOUT"
} }
@ -131,7 +144,11 @@ impl CadCommand for WipeoutCommand {
} }
WipeoutMode::Rectangular => { WipeoutMode::Rectangular => {
if let Some(first) = self.first { if let Some(first) = self.first {
CmdResult::CommitAndExit(make_rect_wipeout(first, point)) let first = self.plane.to_local(first);
let point = self.plane.to_local(point);
CmdResult::CommitAndExit(
self.plane.place_entity(make_rect_wipeout(first, point)),
)
} else { } else {
self.first = Some(point); self.first = Some(point);
CmdResult::NeedPoint CmdResult::NeedPoint
@ -228,15 +245,21 @@ impl CadCommand for WipeoutCommand {
} }
WipeoutMode::Rectangular => { WipeoutMode::Rectangular => {
let first = self.first?; let first = self.first?;
let corners = {
let first_local = self.plane.to_local(first);
let point_local = self.plane.to_local(point);
[
first_local,
DVec3::new(point_local.x, first_local.y, first_local.z),
DVec3::new(point_local.x, point_local.y, first_local.z),
DVec3::new(first_local.x, point_local.y, first_local.z),
first_local,
]
.map(|corner| self.plane.to_world(corner))
};
Some(WireModel::solid_f64( Some(WireModel::solid_f64(
"wipeout_preview".into(), "wipeout_preview".into(),
vec![ corners.iter().map(|p| [p.x, p.y, p.z]).collect(),
[first.x, first.y, first.z],
[point.x, first.y, first.z],
[point.x, point.y, first.z],
[first.x, point.y, first.z],
[first.x, first.y, first.z],
],
WireModel::CYAN, WireModel::CYAN,
false, false,
)) ))

View file

@ -3,7 +3,7 @@
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
pub struct DistCommand { pub struct DistCommand {
@ -11,15 +11,23 @@ pub struct DistCommand {
// loses ~0.03–0.06 units at survey-scale coordinates (e.g. eastings near // loses ~0.03–0.06 units at survey-scale coordinates (e.g. eastings near
// 5e5), which made snapped-endpoint measurements read off by that much. // 5e5), which made snapped-endpoint measurements read off by that much.
first: Option<DVec3>, first: Option<DVec3>,
plane: WorkingPlane,
} }
impl DistCommand { impl DistCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { first: None } Self {
first: None,
plane: WorkingPlane::default(),
}
} }
} }
impl CadCommand for DistCommand { impl CadCommand for DistCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"DIST" "DIST"
} }
@ -34,11 +42,11 @@ impl CadCommand for DistCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Some(p1) = self.first { if let Some(p1) = self.first {
let delta = pt - p1; let delta = self.plane.vector_to_local(pt - p1);
let dist = delta.length(); let dist = delta.length();
let dx = delta.x; let dx = delta.x;
let dy = delta.y; // drawing plane is world XY let dy = delta.y;
let dz = delta.z; // elevation let dz = delta.z;
// Angle in XY plane — degrees from +X // Angle in XY plane — degrees from +X
let angle_xy = dy.atan2(dx).to_degrees(); let angle_xy = dy.atan2(dx).to_degrees();

View file

@ -3,17 +3,25 @@
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
pub struct IdCommand; pub struct IdCommand {
plane: WorkingPlane,
}
impl IdCommand { impl IdCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self Self {
plane: WorkingPlane::default(),
}
} }
} }
impl CadCommand for IdCommand { impl CadCommand for IdCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ID" "ID"
} }
@ -23,10 +31,10 @@ impl CadCommand for IdCommand {
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
// Drawing plane is world XY (z = elevation). let local = self.plane.to_local(pt);
let x = pt.x; let x = local.x;
let y = pt.y; let y = local.y;
let z = pt.z; let z = local.z;
let x_s = format!("{x:.4}"); let x_s = format!("{x:.4}");
let y_s = format!("{y:.4}"); let y_s = format!("{y:.4}");
let z_s = format!("{z:.4}"); let z_s = format!("{z:.4}");

View file

@ -18,7 +18,7 @@ use acadrust::{EntityType, Handle};
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -54,6 +54,7 @@ pub struct MeasureGeomCommand {
points: Vec<DVec3>, points: Vec<DVec3>,
/// The picked entity for RADIUS, injected before `on_entity_pick`. /// The picked entity for RADIUS, injected before `on_entity_pick`.
picked: Option<EntityType>, picked: Option<EntityType>,
plane: WorkingPlane,
} }
impl MeasureGeomCommand { impl MeasureGeomCommand {
@ -62,6 +63,7 @@ impl MeasureGeomCommand {
mode: Mode::Choose, mode: Mode::Choose,
points: vec![], points: vec![],
picked: None, picked: None,
plane: WorkingPlane::default(),
} }
} }
@ -107,12 +109,14 @@ impl MeasureGeomCommand {
} }
/// DISTANCE readout for the two collected points. /// DISTANCE readout for the two collected points.
fn distance_msg(p1: DVec3, p2: DVec3) -> String { fn distance_msg(plane: WorkingPlane, p1: DVec3, p2: DVec3) -> String {
let p1 = plane.to_local(p1);
let p2 = plane.to_local(p2);
let delta = p2 - p1; let delta = p2 - p1;
let dist = delta.length(); let dist = delta.length();
let dx = delta.x; let dx = delta.x;
let dy = delta.y; // drawing plane is world XY let dy = delta.y;
let dz = delta.z; // elevation let dz = delta.z;
let angle_xy = dy.atan2(dx).to_degrees(); let angle_xy = dy.atan2(dx).to_degrees();
let dist_s = format!("{dist:.4}"); let dist_s = format!("{dist:.4}");
let angle_xy_s = format!("{angle_xy:.4}"); let angle_xy_s = format!("{angle_xy:.4}");
@ -131,7 +135,8 @@ impl MeasureGeomCommand {
} }
/// AREA readout: shoelace area (f64, relative to first vertex) + perimeter. /// AREA readout: shoelace area (f64, relative to first vertex) + perimeter.
fn area_msg(points: &[DVec3]) -> String { fn area_msg(plane: WorkingPlane, points: &[DVec3]) -> String {
let points = points.iter().map(|point| plane.to_local(*point)).collect::<Vec<_>>();
let n = points.len(); let n = points.len();
let origin = points[0]; let origin = points[0];
let mut area_sum = 0.0f64; let mut area_sum = 0.0f64;
@ -207,6 +212,10 @@ impl CadCommand for MeasureGeomCommand {
} }
} }
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn wants_text_input(&self) -> bool { fn wants_text_input(&self) -> bool {
// Only the opening mode-keyword step reads a typed token. // Only the opening mode-keyword step reads a typed token.
self.mode == Mode::Choose self.mode == Mode::Choose
@ -267,7 +276,11 @@ impl CadCommand for MeasureGeomCommand {
Mode::Distance => { Mode::Distance => {
self.points.push(pt); self.points.push(pt);
if self.points.len() == 2 { if self.points.len() == 2 {
CmdResult::Measurement(Self::distance_msg(self.points[0], self.points[1])) CmdResult::Measurement(Self::distance_msg(
self.plane,
self.points[0],
self.points[1],
))
} else { } else {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -299,7 +312,7 @@ impl CadCommand for MeasureGeomCommand {
if self.points.len() < 3 { if self.points.len() < 3 {
CmdResult::Cancel CmdResult::Cancel
} else { } else {
CmdResult::Measurement(Self::area_msg(&self.points)) CmdResult::Measurement(Self::area_msg(self.plane, &self.points))
} }
} }
_ => CmdResult::Cancel, _ => CmdResult::Cancel,

View file

@ -14,10 +14,10 @@
// 1. Center point → 2. Item count (text) → 3. Total angle in degrees (text) // 1. Center point → 2. Item count (text) → 3. Total angle in degrees (text)
use acadrust::Handle; use acadrust::Handle;
use glam::{DVec3, Mat4}; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult, EntityTransform}; use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -63,7 +63,7 @@ pub struct ArrayRectCommand {
default_cols: u32, default_cols: u32,
default_row_sp: f64, default_row_sp: f64,
default_col_sp: f64, default_col_sp: f64,
ucs: Mat4, plane: WorkingPlane,
} }
impl ArrayRectCommand { impl ArrayRectCommand {
@ -76,18 +76,17 @@ impl ArrayRectCommand {
default_cols: defaults::get_array_cols() as u32, default_cols: defaults::get_array_cols() as u32,
default_row_sp: defaults::get_array_row_sp(), default_row_sp: defaults::get_array_row_sp(),
default_col_sp: defaults::get_array_col_sp(), default_col_sp: defaults::get_array_col_sp(),
ucs: Mat4::IDENTITY, plane: WorkingPlane::default(),
} }
} }
/// Row/column offsets run along the active UCS axes (`ucs` rotates the /// Row/column offsets run along the active coordinate axes.
/// world-frame grid step), identity = world.
fn build_transforms( fn build_transforms(
rows: u32, rows: u32,
cols: u32, cols: u32,
row_sp: f64, row_sp: f64,
col_sp: f64, col_sp: f64,
ucs: Mat4, plane: WorkingPlane,
) -> Vec<EntityTransform> { ) -> Vec<EntityTransform> {
let mut t = Vec::new(); let mut t = Vec::new();
for r in 0..rows { for r in 0..rows {
@ -95,9 +94,11 @@ impl ArrayRectCommand {
if r == 0 && c == 0 { if r == 0 && c == 0 {
continue; continue;
} }
t.push(EntityTransform::Translate(ucs.as_dmat4().transform_vector3( t.push(EntityTransform::Translate(plane.vector_to_world(DVec3::new(
glam::DVec3::new(col_sp * c as f64, row_sp * r as f64, 0.0), col_sp * c as f64,
))); row_sp * r as f64,
0.0,
))));
} }
} }
t t
@ -109,8 +110,8 @@ impl CadCommand for ArrayRectCommand {
"ARRAYRECT" "ARRAYRECT"
} }
fn set_ucs(&mut self, ucs: Mat4) { fn set_working_plane(&mut self, plane: WorkingPlane) {
self.ucs = ucs; self.plane = plane;
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
@ -194,7 +195,7 @@ impl CadCommand for ArrayRectCommand {
}; };
Some(CmdResult::BatchCopy( Some(CmdResult::BatchCopy(
self.handles.clone(), self.handles.clone(),
Self::build_transforms(rows, cols, row_sp, col_sp, self.ucs), Self::build_transforms(rows, cols, row_sp, col_sp, self.plane),
)) ))
} }
} }
@ -219,7 +220,7 @@ impl CadCommand for ArrayRectCommand {
} }
RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp), RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp),
}; };
Self::build_transforms(rows, cols, row_sp, col_sp, self.ucs) Self::build_transforms(rows, cols, row_sp, col_sp, self.plane)
.iter() .iter()
.flat_map(|t| { .flat_map(|t| {
if let EntityTransform::Translate(delta) = t { if let EntityTransform::Translate(delta) = t {
@ -262,6 +263,7 @@ pub struct ArrayPolarCommand {
step: PolarStep, step: PolarStep,
default_count: u32, default_count: u32,
default_angle: f64, default_angle: f64,
plane: WorkingPlane,
} }
impl ArrayPolarCommand { impl ArrayPolarCommand {
@ -272,11 +274,16 @@ impl ArrayPolarCommand {
step: PolarStep::Center, step: PolarStep::Center,
default_count: defaults::get_array_p_count() as u32, default_count: defaults::get_array_p_count() as u32,
default_angle: defaults::get_array_p_angle(), default_angle: defaults::get_array_p_angle(),
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for ArrayPolarCommand { impl CadCommand for ArrayPolarCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ARRAYPOLAR" "ARRAYPOLAR"
} }
@ -342,6 +349,7 @@ impl CadCommand for ArrayPolarCommand {
let transforms = (1..count) let transforms = (1..count)
.map(|n| EntityTransform::Rotate { .map(|n| EntityTransform::Rotate {
center, center,
axis: self.plane.z,
angle_rad: step_rad * n as f64, angle_rad: step_rad * n as f64,
}) })
.collect(); .collect();
@ -363,12 +371,13 @@ impl CadCommand for ArrayPolarCommand {
} }
}; };
let step_rad = total_deg.to_radians() / count as f64; let step_rad = total_deg.to_radians() / count as f64;
let axis = self.plane.z.as_vec3();
let mut out: Vec<WireModel> = (1..count) let mut out: Vec<WireModel> = (1..count)
.flat_map(|n| { .flat_map(|n| {
let angle_rad = (step_rad * n as f64) as f32; let angle_rad = (step_rad * n as f64) as f32;
self.wire_models self.wire_models
.iter() .iter()
.map(move |w| w.rotated(center, angle_rad)) .map(move |wire| wire.rotated_about_axis(center, axis, angle_rad))
}) })
.collect(); .collect();
// Rubber-band from center to cursor while picking the center point. // Rubber-band from center to cursor while picking the center point.
@ -675,6 +684,7 @@ impl ArrayPathCommand {
let center = Self::rigid_center(p0, p, dth); let center = Self::rigid_center(p0, p, dth);
EntityTransform::Rotate { EntityTransform::Rotate {
center, center,
axis: DVec3::Z,
angle_rad: dth, angle_rad: dth,
} }
} }
@ -814,7 +824,7 @@ impl CadCommand for ArrayPathCommand {
.iter() .iter()
.map(|w| w.translated(delta.as_vec3())) .map(|w| w.translated(delta.as_vec3()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
EntityTransform::Rotate { center, angle_rad } => self EntityTransform::Rotate { center, angle_rad, .. } => self
.wire_models .wire_models
.iter() .iter()
.map(|w| w.rotated(center.as_vec3(), *angle_rad as f32)) .map(|w| w.rotated(center.as_vec3(), *angle_rad as f32))

View file

@ -12,7 +12,7 @@ use acadrust::Handle;
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult, EntityTransform}; use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -41,6 +41,7 @@ pub struct MirrorCommand {
text_ghosts: Vec<(WireModel, DVec3)>, text_ghosts: Vec<(WireModel, DVec3)>,
mirror_text: bool, mirror_text: bool,
step: Step, step: Step,
plane: WorkingPlane,
} }
impl MirrorCommand { impl MirrorCommand {
@ -56,11 +57,16 @@ impl MirrorCommand {
text_ghosts, text_ghosts,
mirror_text, mirror_text,
step: Step::P1, step: Step::P1,
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for MirrorCommand { impl CadCommand for MirrorCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"MIRROR" "MIRROR"
} }
@ -147,18 +153,32 @@ impl CadCommand for MirrorCommand {
let mut out: Vec<WireModel> = self let mut out: Vec<WireModel> = self
.wire_models .wire_models
.iter() .iter()
.map(|w| w.mirrored(p1.as_vec3(), p2.as_vec3())) .map(|w| {
w.mirrored_in_plane(
p1.as_vec3(),
p2.as_vec3(),
self.plane.z.as_vec3(),
)
})
.collect(); .collect();
// Text ghosts honour MIRRTEXT: on → true glyph mirror (full reflection); // Text ghosts honour MIRRTEXT: on → true glyph mirror (full reflection);
// off → keep glyphs readable, relocate to the mirror-symmetric position // off → keep glyphs readable, relocate to the mirror-symmetric position
// by reflecting the box centre and translating. // by reflecting the box centre and translating.
for (w, center) in &self.text_ghosts { for (w, center) in &self.text_ghosts {
if self.mirror_text { if self.mirror_text {
out.push(w.mirrored(p1.as_vec3(), p2.as_vec3())); out.push(w.mirrored_in_plane(
p1.as_vec3(),
p2.as_vec3(),
self.plane.z.as_vec3(),
));
} else { } else {
let (mut cx, mut cy) = (center.x, center.y); let reflected = crate::scene::view::transform::reflected_point(
crate::scene::view::transform::reflect_xy_point(&mut cx, &mut cy, p1, p2); *center,
let delta = glam::Vec3::new((cx - center.x) as f32, (cy - center.y) as f32, 0.0); p1,
p2,
self.plane.z,
);
let delta = (reflected - *center).as_vec3();
out.push(w.translated(delta)); out.push(w.translated(delta));
} }
} }
@ -180,7 +200,11 @@ impl MirrorCommand {
/// Commit the mirror. `erase` true flips the originals in place; false /// Commit the mirror. `erase` true flips the originals in place; false
/// keeps them and adds a mirrored copy. Either way the command ends. /// keeps them and adds a mirrored copy. Either way the command ends.
fn finish(&self, p1: DVec3, p2: DVec3, erase: bool) -> CmdResult { fn finish(&self, p1: DVec3, p2: DVec3, erase: bool) -> CmdResult {
let xform = EntityTransform::Mirror { p1, p2 }; let xform = EntityTransform::Mirror {
p1,
p2,
working_normal: self.plane.z,
};
if erase { if erase {
CmdResult::TransformSelected(self.handles.clone(), xform) CmdResult::TransformSelected(self.handles.clone(), xform)
} else { } else {

View file

@ -13,7 +13,7 @@ use acadrust::Handle;
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult, DynField, EntityTransform}; use crate::command::{CadCommand, CmdResult, DynField, EntityTransform, WorkingPlane};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -44,6 +44,7 @@ pub struct RotateCommand {
wire_models: Vec<WireModel>, wire_models: Vec<WireModel>,
step: Step, step: Step,
default_angle: f64, // degrees default_angle: f64, // degrees
plane: WorkingPlane,
} }
impl RotateCommand { impl RotateCommand {
@ -53,6 +54,7 @@ impl RotateCommand {
wire_models, wire_models,
step: Step::Center, step: Step::Center,
default_angle: defaults::get_rotate_angle(), default_angle: defaults::get_rotate_angle(),
plane: WorkingPlane::default(),
} }
} }
@ -60,12 +62,20 @@ impl RotateCommand {
defaults::set_rotate_angle(angle_rad.to_degrees()); defaults::set_rotate_angle(angle_rad.to_degrees());
CmdResult::TransformSelected( CmdResult::TransformSelected(
self.handles.clone(), self.handles.clone(),
EntityTransform::Rotate { center, angle_rad }, EntityTransform::Rotate {
center,
axis: self.plane.z,
angle_rad,
},
) )
} }
} }
impl CadCommand for RotateCommand { impl CadCommand for RotateCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"ROTATE" "ROTATE"
} }
@ -110,11 +120,9 @@ impl CadCommand for RotateCommand {
} }
Step::Angle { center } => { Step::Angle { center } => {
let center = *center; let center = *center;
let direction = pt - center; let Some(angle_rad) = self.plane.angle(center, pt) else {
if direction.x.hypot(direction.y) <= f64::EPSILON {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} };
let angle_rad = direction.y.atan2(direction.x);
self.commit(center, angle_rad) self.commit(center, angle_rad)
} }
Step::RefFirst { center } => { Step::RefFirst { center } => {
@ -123,22 +131,18 @@ impl CadCommand for RotateCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::RefSecond { center, first } => { Step::RefSecond { center, first } => {
let direction = pt - *first;
if direction.x.hypot(direction.y) <= f64::EPSILON {
return CmdResult::NeedPoint;
}
let center = *center; let center = *center;
let ref_angle = direction.y.atan2(direction.x); let Some(ref_angle) = self.plane.angle(*first, pt) else {
return CmdResult::NeedPoint;
};
self.step = Step::RefNew { center, ref_angle }; self.step = Step::RefNew { center, ref_angle };
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::RefNew { center, ref_angle } => { Step::RefNew { center, ref_angle } => {
let center = *center; let center = *center;
let direction = pt - center; let Some(new_angle) = self.plane.angle(center, pt) else {
if direction.x.hypot(direction.y) <= f64::EPSILON {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} };
let new_angle = direction.y.atan2(direction.x);
self.commit(center, new_angle - *ref_angle) self.commit(center, new_angle - *ref_angle)
} }
} }
@ -192,11 +196,10 @@ impl CadCommand for RotateCommand {
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> { fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let (center, angle_rad) = match &self.step { let (center, angle_rad) = match &self.step {
Step::Angle { center } => { Step::Angle { center } => {
let direction = pt - *center; let Some(angle) = self.plane.angle(*center, pt) else {
if direction.x.hypot(direction.y) <= f64::EPSILON {
return vec![]; return vec![];
} };
(*center, direction.y.atan2(direction.x)) (*center, angle)
} }
Step::RefSecond { first, .. } => { Step::RefSecond { first, .. } => {
return vec![WireModel::solid( return vec![WireModel::solid(
@ -210,11 +213,10 @@ impl CadCommand for RotateCommand {
)]; )];
} }
Step::RefNew { center, ref_angle } => { Step::RefNew { center, ref_angle } => {
let direction = pt - *center; let Some(angle) = self.plane.angle(*center, pt) else {
if direction.x.hypot(direction.y) <= f64::EPSILON {
return vec![]; return vec![];
} };
(*center, direction.y.atan2(direction.x) - *ref_angle) (*center, angle - *ref_angle)
} }
_ => return vec![], _ => return vec![],
}; };
@ -222,7 +224,13 @@ impl CadCommand for RotateCommand {
// drawn by the dynamic-input overlay (polar guide), not here. // drawn by the dynamic-input overlay (polar guide), not here.
self.wire_models self.wire_models
.iter() .iter()
.map(|w| w.rotated(center.as_vec3(), angle_rad as f32)) .map(|w| {
w.rotated_about_axis(
center.as_vec3(),
self.plane.z.as_vec3(),
angle_rad as f32,
)
})
.collect() .collect()
} }

View file

@ -4,7 +4,7 @@ use acadrust::EntityType;
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -59,6 +59,7 @@ pub struct InsertBlockCommand {
/// is measured from, so `on_preview_wires` can rubber-band it to the /// is measured from, so `on_preview_wires` can rubber-band it to the
/// cursor. Set by paste-as-block; empty for a plain INSERT. /// cursor. Set by paste-as-block; empty for a plain INSERT.
preview: Option<(Vec<WireModel>, Vec3)>, preview: Option<(Vec<WireModel>, Vec3)>,
plane: WorkingPlane,
} }
impl InsertBlockCommand { impl InsertBlockCommand {
@ -72,6 +73,7 @@ impl InsertBlockCommand {
awaiting: None, awaiting: None,
pending_insert: None, pending_insert: None,
preview: None, preview: None,
plane: WorkingPlane::default(),
} }
} }
@ -89,11 +91,16 @@ impl InsertBlockCommand {
awaiting: None, awaiting: None,
pending_insert: None, pending_insert: None,
preview: Some((preview_wires, base)), preview: Some((preview_wires, base)),
plane: WorkingPlane::default(),
} }
} }
} }
impl CadCommand for InsertBlockCommand { impl CadCommand for InsertBlockCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"INSERT" "INSERT"
} }
@ -146,10 +153,15 @@ impl CadCommand for InsertBlockCommand {
match &self.step { match &self.step {
Step::Name => CmdResult::NeedPoint, Step::Name => CmdResult::NeedPoint,
Step::Point { name } => { Step::Point { name } => {
let mut ins = Insert::new(name.clone(), Vector3::new(pt.x, pt.y, pt.z)); let point = self.plane.to_local(pt);
let mut ins = Insert::new(
name.clone(),
Vector3::new(point.x, point.y, point.z),
);
ins.set_x_scale(self.x_scale); ins.set_x_scale(self.x_scale);
ins.set_y_scale(self.y_scale); ins.set_y_scale(self.y_scale);
ins.rotation = self.rotation_rad; ins.rotation = self.rotation_rad;
ins.apply_transform(&self.plane.to_world_transform());
let block_name = name.clone(); let block_name = name.clone();
self.pending_insert = Some(ins); self.pending_insert = Some(ins);
// Signal the host to check for attdefs. // Signal the host to check for attdefs.

View file

@ -12,7 +12,7 @@ use acadrust::EntityType;
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
#[allow(dead_code)] #[allow(dead_code)]
@ -67,6 +67,7 @@ pub struct MinsertCommand {
columns: u16, columns: u16,
row_spacing: f64, row_spacing: f64,
column_spacing: f64, column_spacing: f64,
plane: WorkingPlane,
} }
impl MinsertCommand { impl MinsertCommand {
@ -78,6 +79,7 @@ impl MinsertCommand {
columns: 1, columns: 1,
row_spacing: 0.0, row_spacing: 0.0,
column_spacing: 0.0, column_spacing: 0.0,
plane: WorkingPlane::default(),
} }
} }
@ -88,7 +90,7 @@ impl MinsertCommand {
ins.column_count = self.columns.max(1); ins.column_count = self.columns.max(1);
ins.row_spacing = self.row_spacing; ins.row_spacing = self.row_spacing;
ins.column_spacing = self.column_spacing; ins.column_spacing = self.column_spacing;
CmdResult::CommitAndExit(EntityType::Insert(ins)) CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Insert(ins)))
} }
/// Advance from the parameter currently at `idx` to the next, or build the /// Advance from the parameter currently at `idx` to the next, or build the
@ -109,6 +111,10 @@ impl MinsertCommand {
} }
impl CadCommand for MinsertCommand { impl CadCommand for MinsertCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"MINSERT" "MINSERT"
} }
@ -160,7 +166,8 @@ impl CadCommand for MinsertCommand {
Step::Name => CmdResult::NeedPoint, Step::Name => CmdResult::NeedPoint,
Step::Point { name } => { Step::Point { name } => {
let name = name.clone(); let name = name.clone();
let point = Vector3::new(pt.x, pt.y, pt.z); let point = self.plane.to_local(pt);
let point = Vector3::new(point.x, point.y, point.z);
self.step = Step::Params { self.step = Step::Params {
name, name,
point, point,

View file

@ -16,7 +16,7 @@ use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::scene::Scene; use crate::scene::Scene;
@ -33,17 +33,26 @@ pub fn tool() -> ToolDef {
pub struct XAttachCommand { pub struct XAttachCommand {
path: String, path: String,
block_name: String, block_name: String,
plane: WorkingPlane,
} }
impl XAttachCommand { impl XAttachCommand {
/// Create an XATTACH command with a path already filled in (from file-picker). /// Create an XATTACH command with a path already filled in (from file-picker).
pub fn with_path(path: String) -> Self { pub fn with_path(path: String) -> Self {
let block_name = path_to_block_name(&path); let block_name = path_to_block_name(&path);
Self { path, block_name } Self {
path,
block_name,
plane: WorkingPlane::default(),
}
} }
} }
impl CadCommand for XAttachCommand { impl CadCommand for XAttachCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"XATTACH" "XATTACH"
} }
@ -59,10 +68,11 @@ impl CadCommand for XAttachCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
// We return the INSERT entity; the command handler in commands.rs // We return the INSERT entity; the command handler in commands.rs
// calls `prepare_xref_block` on the scene before committing. // calls `prepare_xref_block` on the scene before committing.
CmdResult::CommitAndExit(EntityType::Insert(Insert::new( let point = self.plane.to_local(pt);
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Insert(Insert::new(
self.block_name.clone(), self.block_name.clone(),
Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64), Vector3::new(point.x, point.y, point.z),
))) ))))
} }
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {

View file

@ -13,7 +13,7 @@ use glam::DVec3;
use crate::t; use crate::t;
use truck_modeling::Solid; use truck_modeling::Solid;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::scene::model::solid_model; use crate::scene::model::solid_model;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -67,6 +67,7 @@ pub struct PrimitiveCommand {
pts: Vec<DVec3>, pts: Vec<DVec3>,
/// True once the footprint is set and we are collecting the height. /// True once the footprint is set and we are collecting the height.
height_step: bool, height_step: bool,
plane: WorkingPlane,
} }
impl PrimitiveCommand { impl PrimitiveCommand {
@ -75,6 +76,7 @@ impl PrimitiveCommand {
shape: Shape::from_id(id).unwrap_or(Shape::Box), shape: Shape::from_id(id).unwrap_or(Shape::Box),
pts: Vec::new(), pts: Vec::new(),
height_step: false, height_step: false,
plane: WorkingPlane::default(),
} }
} }
@ -110,13 +112,17 @@ impl PrimitiveCommand {
return None; return None;
} }
if self.shape == Shape::Box { if self.shape == Shape::Box {
let center = [(a.x + b.x) / 2.0, (a.y + b.y) / 2.0, height / 2.0]; let center = [
(a.x + b.x) / 2.0,
(a.y + b.y) / 2.0,
a.z + height / 2.0,
];
( (
primitives::build_box(center, length, width, height), primitives::build_box(center, length, width, height),
solid_model::box_solid(center, length, width, height), solid_model::box_solid(center, length, width, height),
) )
} else { } else {
let origin = [a.x.min(b.x), a.y.min(b.y), 0.0]; let origin = [a.x.min(b.x), a.y.min(b.y), a.z];
( (
primitives::build_wedge(origin, length, width, height), primitives::build_wedge(origin, length, width, height),
solid_model::wedge_solid(origin, length, width, height), solid_model::wedge_solid(origin, length, width, height),
@ -129,7 +135,7 @@ impl PrimitiveCommand {
if r < 1e-6 || height < 1e-6 { if r < 1e-6 || height < 1e-6 {
return None; return None;
} }
let center = [c.x, c.y, 0.0]; let center = [c.x, c.y, c.z];
if self.shape == Shape::Cylinder { if self.shape == Shape::Cylinder {
( (
primitives::build_cylinder(center, r, height), primitives::build_cylinder(center, r, height),
@ -148,7 +154,7 @@ impl PrimitiveCommand {
if r < 1e-6 { if r < 1e-6 {
return None; return None;
} }
let center = [c.x, c.y, 0.0]; let center = [c.x, c.y, c.z];
( (
primitives::build_sphere(center, r), primitives::build_sphere(center, r),
solid_model::sphere_solid(center, r), solid_model::sphere_solid(center, r),
@ -161,7 +167,7 @@ impl PrimitiveCommand {
if major < 1e-6 || minor < 1e-6 { if major < 1e-6 || minor < 1e-6 {
return None; return None;
} }
let center = [c.x, c.y, 0.0]; let center = [c.x, c.y, c.z];
( (
primitives::build_torus(center, major, minor), primitives::build_torus(center, major, minor),
solid_model::torus_solid(center, major, minor), solid_model::torus_solid(center, major, minor),
@ -178,16 +184,40 @@ impl PrimitiveCommand {
fn commit(&self, height: f64) -> CmdResult { fn commit(&self, height: f64) -> CmdResult {
match self.build(height) { match self.build(height) {
Some((entity, solid)) => CmdResult::CommitSolid { Some((entity, solid)) => {
entity, let matrix = truck_modeling::Matrix4::new(
solid: Box::new(solid), self.plane.x.x,
}, self.plane.x.y,
self.plane.x.z,
0.0,
self.plane.y.x,
self.plane.y.y,
self.plane.y.z,
0.0,
self.plane.z.x,
self.plane.z.y,
self.plane.z.z,
0.0,
self.plane.origin.x,
self.plane.origin.y,
self.plane.origin.z,
1.0,
);
CmdResult::CommitSolid {
entity: self.plane.place_entity(entity),
solid: Box::new(truck_modeling::builder::transformed(&solid, matrix)),
}
}
None => CmdResult::Cancel, None => CmdResult::Cancel,
} }
} }
} }
impl CadCommand for PrimitiveCommand { impl CadCommand for PrimitiveCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
self.shape.name() self.shape.name()
} }
@ -210,10 +240,10 @@ impl CadCommand for PrimitiveCommand {
if self.height_step { if self.height_step {
// A click in the ground plane has no Z; use its distance from the // A click in the ground plane has no Z; use its distance from the
// footprint centre as the height magnitude. // footprint centre as the height magnitude.
let h = (pt - self.pts[0]).length(); let h = (self.plane.to_local(pt) - self.pts[0]).length();
return self.commit(h.max(1e-6)); return self.commit(h.max(1e-6));
} }
self.pts.push(pt); self.pts.push(self.plane.to_local(pt));
if self.pts.len() < self.footprint_pts() { if self.pts.len() < self.footprint_pts() {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
@ -255,8 +285,23 @@ impl CadCommand for PrimitiveCommand {
return None; return None;
} }
let mut foot = self.pts.clone(); let mut foot = self.pts.clone();
foot.push(pt); foot.push(self.plane.to_local(pt));
Some(footprint_wire(self.shape, &foot)) let mut preview = footprint_wire(self.shape, &foot);
preview.points = preview
.points
.iter()
.map(|point| {
if point[0].is_nan() {
*point
} else {
self.plane
.to_world(glam::Vec3::from_array(*point).as_dvec3())
.as_vec3()
.to_array()
}
})
.collect();
Some(preview)
} }
} }
@ -277,11 +322,11 @@ fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel {
} else { } else {
let (a, b) = (pts[0], pts[1]); let (a, b) = (pts[0], pts[1]);
points.extend_from_slice(&[ points.extend_from_slice(&[
[a.x as f32, a.y as f32, 0.0], [a.x as f32, a.y as f32, a.z as f32],
[b.x as f32, a.y as f32, 0.0], [b.x as f32, a.y as f32, a.z as f32],
[b.x as f32, b.y as f32, 0.0], [b.x as f32, b.y as f32, a.z as f32],
[a.x as f32, b.y as f32, 0.0], [a.x as f32, b.y as f32, a.z as f32],
[a.x as f32, a.y as f32, 0.0], [a.x as f32, a.y as f32, a.z as f32],
]); ]);
} }
wire("primitive_preview", points) wire("primitive_preview", points)
@ -294,7 +339,7 @@ fn circle_points(out: &mut Vec<[f32; 3]>, c: DVec3, r: f64) {
out.push([ out.push([
(c.x + r * t.cos()) as f32, (c.x + r * t.cos()) as f32,
(c.y + r * t.sin()) as f32, (c.y + r * t.sin()) as f32,
0.0, c.z as f32,
]); ]);
} }
} }

View file

@ -16,6 +16,7 @@ use crate::t;
pub struct UcsCommand { pub struct UcsCommand {
/// The chosen option keyword (uppercased), once entered; `None` until then. /// The chosen option keyword (uppercased), once entered; `None` until then.
option: Option<String>, option: Option<String>,
points: Vec<DVec3>,
} }
impl UcsCommand { impl UcsCommand {
@ -25,12 +26,12 @@ impl UcsCommand {
/// Options that take no further argument and execute immediately. /// Options that take no further argument and execute immediately.
fn is_zero_arg(opt: &str) -> bool { fn is_zero_arg(opt: &str) -> bool {
matches!(opt, "W" | "WORLD" | "LIST" | "?") matches!(opt, "W" | "WORLD" | "VIEW" | "V" | "LIST" | "?")
} }
/// Options whose argument is a coordinate (so a click is accepted too). /// Options whose argument is a coordinate (so a click is accepted too).
fn takes_point(opt: &str) -> bool { fn takes_point(opt: &str) -> bool {
matches!(opt, "ORIGIN" | "O") matches!(opt, "ORIGIN" | "O" | "3POINT" | "3P")
} }
/// Options that expect one more typed argument (angle or name). /// Options that expect one more typed argument (angle or name).
@ -49,11 +50,16 @@ impl CadCommand for UcsCommand {
fn prompt(&self) -> String { fn prompt(&self) -> String {
match self.option.as_deref() { match self.option.as_deref() {
None => t!("UCS option [World/Z/X/Y/Origin/Save/Delete] or name:").into_owned(), None => t!("UCS option [World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:").into_owned(),
Some("Z") => t!("UCS rotation angle about Z (degrees):").into_owned(), Some("Z") => t!("UCS rotation angle about Z (degrees):").into_owned(),
Some("X") => t!("UCS rotation angle about X (degrees):").into_owned(), Some("X") => t!("UCS rotation angle about X (degrees):").into_owned(),
Some("Y") => t!("UCS rotation angle about Y (degrees):").into_owned(), Some("Y") => t!("UCS rotation angle about Y (degrees):").into_owned(),
Some("ORIGIN") | Some("O") => t!("UCS new origin point:").into_owned(), Some("ORIGIN") | Some("O") => t!("UCS new origin point:").into_owned(),
Some("3POINT") | Some("3P") => match self.points.len() {
0 => t!("UCS specify new origin:").into_owned(),
1 => t!("UCS specify point on positive X axis:").into_owned(),
_ => t!("UCS specify point in positive XY plane:").into_owned(),
},
Some("SAVE") | Some("S") => t!("UCS name to save current UCS as:").into_owned(), Some("SAVE") | Some("S") => t!("UCS name to save current UCS as:").into_owned(),
Some("DELETE") | Some("DEL") | Some("D") => { Some("DELETE") | Some("DEL") | Some("D") => {
t!("UCS name of UCS to delete:").into_owned() t!("UCS name of UCS to delete:").into_owned()
@ -68,6 +74,11 @@ impl CadCommand for UcsCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim(); let t = text.trim();
if matches!(self.option.as_deref(), Some("3POINT") | Some("3P"))
&& crate::app::helpers::parse_coord(t).is_some()
{
return None;
}
match self.option.take() { match self.option.take() {
// First token: the option keyword (or a named UCS to restore). // First token: the option keyword (or a named UCS to restore).
None => { None => {
@ -78,7 +89,7 @@ impl CadCommand for UcsCommand {
let up = t.to_uppercase(); let up = t.to_uppercase();
if Self::is_zero_arg(&up) { if Self::is_zero_arg(&up) {
Some(CmdResult::Dispatch(format!("UCS {up}"))) Some(CmdResult::Dispatch(format!("UCS {up}")))
} else if Self::takes_value(&up) { } else if Self::takes_value(&up) || Self::takes_point(&up) {
// Needs a value next; keep the command active and re-prompt. // Needs a value next; keep the command active and re-prompt.
self.option = Some(up); self.option = Some(up);
Some(CmdResult::NeedPoint) Some(CmdResult::NeedPoint)
@ -93,11 +104,31 @@ impl CadCommand for UcsCommand {
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
if matches!(self.option.as_deref(), Some("3POINT") | Some("3P")) {
self.points.push(pt);
if self.points.len() < 3 {
return CmdResult::NeedPoint;
}
let points = std::mem::take(&mut self.points);
self.option = None;
return CmdResult::Dispatch(format!(
"UCS 3POINTW {},{},{}|{},{},{}|{},{},{}",
points[0].x,
points[0].y,
points[0].z,
points[1].x,
points[1].y,
points[1].z,
points[2].x,
points[2].y,
points[2].z,
));
}
// A clicked point only makes sense for the Origin option; otherwise a // A clicked point only makes sense for the Origin option; otherwise a
// stray click is ignored (keep waiting for the typed keyword / value). // stray click is ignored (keep waiting for the typed keyword / value).
if matches!(self.option.as_deref(), Some(o) if Self::takes_point(o)) { if matches!(self.option.as_deref(), Some(o) if Self::takes_point(o)) {
let opt = self.option.take().unwrap_or_default(); self.option.take();
return CmdResult::Dispatch(format!("UCS {opt} {},{},{}", pt.x, pt.y, pt.z)); return CmdResult::Dispatch(format!("UCS ORIGINW {},{},{}", pt.x, pt.y, pt.z));
} }
CmdResult::NeedPoint CmdResult::NeedPoint
} }

View file

@ -245,23 +245,40 @@ impl WireModel {
/// Return a clone with every point rotated around `center` by `angle_rad`. /// Return a clone with every point rotated around `center` by `angle_rad`.
pub fn rotated(&self, center: glam::Vec3, angle_rad: f32) -> Self { pub fn rotated(&self, center: glam::Vec3, angle_rad: f32) -> Self {
self.rotated_about_axis(center, glam::Vec3::Z, angle_rad)
}
/// Return a clone rotated about an arbitrary world-space axis.
pub fn rotated_about_axis(
&self,
center: glam::Vec3,
axis: glam::Vec3,
angle_rad: f32,
) -> Self {
let rotation = glam::Quat::from_axis_angle(axis.normalize_or_zero(), angle_rad);
let (s, c) = angle_rad.sin_cos(); let (s, c) = angle_rad.sin_cos();
let mut out = self.clone(); let mut out = self.clone();
out.name = format!("preview_{}", self.name); out.name = format!("preview_{}", self.name);
out.color = Self::CYAN; out.color = Self::CYAN;
out.selected = false; out.selected = false;
for p in &mut out.points { for p in &mut out.points {
let dx = p[0] - center.x; let mapped = center + rotation * (glam::Vec3::from_array(*p) - center);
let dy = p[1] - center.y; *p = mapped.to_array();
p[0] = center.x + dx * c - dy * s; }
p[1] = center.y + dx * s + dy * c; for p in &mut out.points_low {
*p = (rotation * glam::Vec3::from_array(*p)).to_array();
} }
if !out.text_verts.is_empty() { if !out.text_verts.is_empty() {
let (cx, cy) = (center.x as f64, center.y as f64); let center = center.as_dvec3();
let axis = axis.as_dvec3().normalize_or_zero();
let (s, c) = (s as f64, c as f64); let (s, c) = (s as f64, c as f64);
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| { out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
let (dx, dy) = (x - cx, y - cy); let v = glam::DVec3::new(x, y, z) - center;
(cx + dx * c - dy * s, cy + dx * s + dy * c, z) let mapped = center
+ v * c
+ axis.cross(v) * s
+ axis * axis.dot(v) * (1.0 - c);
(mapped.x, mapped.y, mapped.z)
}); });
} }
out out
@ -322,41 +339,48 @@ impl WireModel {
/// Return a clone mirrored across the line through `p1`→`p2`. /// Return a clone mirrored across the line through `p1`→`p2`.
pub fn mirrored(&self, p1: glam::Vec3, p2: glam::Vec3) -> Self { pub fn mirrored(&self, p1: glam::Vec3, p2: glam::Vec3) -> Self {
let ax = p2.x - p1.x; self.mirrored_in_plane(p1, p2, glam::Vec3::Z)
let ay = p2.y - p1.y; }
let len2 = ax * ax + ay * ay;
/// Return a clone reflected through the plane containing the picked line
/// and the supplied working-plane normal.
pub fn mirrored_in_plane(
&self,
p1: glam::Vec3,
p2: glam::Vec3,
working_normal: glam::Vec3,
) -> Self {
let plane_normal = (p2 - p1)
.cross(working_normal.normalize_or_zero())
.normalize_or_zero();
let mut out = self.clone(); let mut out = self.clone();
out.name = format!("preview_{}", self.name); out.name = format!("preview_{}", self.name);
out.color = Self::CYAN; out.color = Self::CYAN;
out.selected = false; out.selected = false;
if len2 < 1e-12 { if plane_normal.length_squared() < 1e-12 {
return out; return out;
} }
for p in &mut out.points { for p in &mut out.points {
let dx = p[0] - p1.x; let point = glam::Vec3::from_array(*p);
let dy = p[1] - p1.y; *p = (point - 2.0 * plane_normal.dot(point - p1) * plane_normal).to_array();
let t = (dx * ax + dy * ay) / len2;
p[0] = p1.x + 2.0 * t * ax - dx;
p[1] = p1.y + 2.0 * t * ay - dy;
} }
// World position is the double-single sum `points + points_low` (text / // World position is the double-single sum `points + points_low` (text /
// UTM wires split it), so the residual must reflect too — as a direction // UTM wires split it), so the residual must reflect too — as a direction
// (linear reflection about the axis, no `p1` offset). // (linear reflection about the axis, no `p1` offset).
for p in &mut out.points_low { for p in &mut out.points_low {
let t = (p[0] * ax + p[1] * ay) / len2; let point = glam::Vec3::from_array(*p);
p[0] = 2.0 * t * ax - p[0]; *p = (point - 2.0 * plane_normal.dot(point) * plane_normal).to_array();
p[1] = 2.0 * t * ay - p[1];
} }
// Glyph quads reflect wholesale (true mirror) — the caller only routes // Glyph quads reflect wholesale (true mirror) — the caller only routes
// text through here for MIRRTEXT-on; MIRRTEXT-off relocates via // text through here for MIRRTEXT-on; MIRRTEXT-off relocates via
// `translated` so glyphs stay readable. // `translated` so glyphs stay readable.
if !out.text_verts.is_empty() { if !out.text_verts.is_empty() {
let (ax, ay, len2) = (ax as f64, ay as f64, len2 as f64); let p1 = p1.as_dvec3();
let (p1x, p1y) = (p1.x as f64, p1.y as f64); let normal = plane_normal.as_dvec3();
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| { out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
let (dx, dy) = (x - p1x, y - p1y); let point = glam::DVec3::new(x, y, z);
let t = (dx * ax + dy * ay) / len2; let mapped = point - 2.0 * normal.dot(point - p1) * normal;
(p1x + 2.0 * t * ax - dx, p1y + 2.0 * t * ay - dy, z) (mapped.x, mapped.y, mapped.z)
}); });
} }
out out

View file

@ -433,6 +433,15 @@ impl Scene {
self.camera.borrow().rotation * glam::Vec3::Z self.camera.borrow().rotation * glam::Vec3::Z
} }
pub fn active_camera_rotation(&self) -> glam::Quat {
if let Some(handle) = self.active_viewport {
if let Some(camera) = self.camera_for_viewport(handle) {
return camera.rotation;
}
}
self.camera.borrow().rotation
}
/// View-rotation matrix for the active viewport (MSPACE), or the /// View-rotation matrix for the active viewport (MSPACE), or the
/// paper-space camera's matrix when not in MSPACE. /// paper-space camera's matrix when not in MSPACE.
/// Used by ViewCube hit-testing so clicks map to the correct camera. /// Used by ViewCube hit-testing so clicks map to the correct camera.

View file

@ -83,6 +83,39 @@ pub fn properties_sectioned(
sections sections
} }
pub fn entity_in_working_plane(
entity: &EntityType,
plane: crate::command::WorkingPlane,
) -> EntityType {
let mut local = entity.clone();
if !plane.is_identity() {
apply_transform(
&mut local,
&EntityTransform::Affine(plane.to_local_transform()),
);
}
local
}
pub fn apply_geom_prop_in_working_plane(
entity: &mut EntityType,
field: &str,
value: &str,
plane: crate::command::WorkingPlane,
) {
if plane.is_identity() {
apply_geom_prop(entity, field, value);
return;
}
let mut local = entity_in_working_plane(entity, plane);
apply_geom_prop(&mut local, field, value);
apply_transform(
&mut local,
&EntityTransform::Affine(plane.to_world_transform()),
);
*entity = local;
}
pub fn apply_common_prop(entity: &mut EntityType, field: &str, value: &str) { pub fn apply_common_prop(entity: &mut EntityType, field: &str, value: &str) {
let e = entity.as_entity_mut(); let e = entity.as_entity_mut();
match field { match field {

View file

@ -8,13 +8,12 @@ fn to_v3(v: DVec3) -> Vector3 {
Vector3::new(v.x, v.y, v.z) Vector3::new(v.x, v.y, v.z)
} }
pub fn apply_standard_transform<T>(entity: &mut T, center: DVec3, angle_rad: f64) pub fn apply_standard_transform<T>(entity: &mut T, center: DVec3, axis: DVec3, angle_rad: f64)
where where
T: acadrust::Entity, T: acadrust::Entity,
{ {
let z_axis = Vector3::new(0.0, 0.0, 1.0);
let t = Transform::from_translation(to_v3(-center)) let t = Transform::from_translation(to_v3(-center))
.then(&Transform::from_rotation(z_axis, angle_rad)) .then(&Transform::from_rotation(to_v3(axis.normalize_or(DVec3::Z)), angle_rad))
.then(&Transform::from_translation(to_v3(center))); .then(&Transform::from_translation(to_v3(center)));
entity.apply_transform(&t); entity.apply_transform(&t);
} }
@ -35,11 +34,25 @@ where
{ {
match t { match t {
EntityTransform::Translate(d) => entity.translate(to_v3(*d)), EntityTransform::Translate(d) => entity.translate(to_v3(*d)),
EntityTransform::Rotate { center, angle_rad } => { EntityTransform::Rotate { center, axis, angle_rad } => {
apply_standard_transform(entity, *center, *angle_rad) apply_standard_transform(entity, *center, *axis, *angle_rad)
} }
EntityTransform::Scale { center, factor } => apply_standard_scale(entity, *center, *factor), EntityTransform::Scale { center, factor } => apply_standard_scale(entity, *center, *factor),
EntityTransform::Mirror { p1, p2 } => mirror(entity, *p1, *p2), EntityTransform::Mirror {
p1,
p2,
working_normal,
} => {
if working_normal.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) {
mirror(entity, *p1, *p2)
} else {
entity.apply_transform(&reflection_about_working_line(
*p1,
*p2,
*working_normal,
));
}
}
EntityTransform::Affine(transform) => entity.apply_transform(transform), EntityTransform::Affine(transform) => entity.apply_transform(transform),
} }
} }
@ -49,19 +62,46 @@ where
/// `apply_transform` paths (which handle direction flags, stored-angle /// `apply_transform` paths (which handle direction flags, stored-angle
/// conventions and bulges themselves). Degenerate line → identity. /// conventions and bulges themselves). Degenerate line → identity.
pub fn reflection_about_xy_line(p1: DVec3, p2: DVec3) -> acadrust::types::Transform { pub fn reflection_about_xy_line(p1: DVec3, p2: DVec3) -> acadrust::types::Transform {
reflection_about_working_line(p1, p2, DVec3::Z)
}
/// Reflection through the plane that contains the picked line and is
/// perpendicular to the active working plane.
pub fn reflection_about_working_line(
p1: DVec3,
p2: DVec3,
working_normal: DVec3,
) -> acadrust::types::Transform {
use acadrust::types::{Matrix4, Transform}; use acadrust::types::{Matrix4, Transform};
let dx = (p2.x - p1.x) as f64; let line = p2 - p1;
let dy = (p2.y - p1.y) as f64; let plane_normal = line
if dx * dx + dy * dy < 1e-12 { .cross(working_normal.normalize_or(DVec3::Z))
.normalize_or(DVec3::ZERO);
if plane_normal.length_squared() < 1e-12 {
return Transform::identity(); return Transform::identity();
} }
let ang = dy.atan2(dx); let n = plane_normal;
let m = Matrix4::translation(p1.x as f64, p1.y as f64, 0.0) let d = 2.0 * n.dot(p1);
* Matrix4::rotation_z(ang) Transform::from_matrix(Matrix4 {
* Matrix4::scaling(1.0, -1.0, 1.0) m: [
* Matrix4::rotation_z(-ang) [1.0 - 2.0 * n.x * n.x, -2.0 * n.x * n.y, -2.0 * n.x * n.z, d * n.x],
* Matrix4::translation(-(p1.x as f64), -(p1.y as f64), 0.0); [-2.0 * n.y * n.x, 1.0 - 2.0 * n.y * n.y, -2.0 * n.y * n.z, d * n.y],
Transform::from_matrix(m) [-2.0 * n.z * n.x, -2.0 * n.z * n.y, 1.0 - 2.0 * n.z * n.z, d * n.z],
[0.0, 0.0, 0.0, 1.0],
],
})
}
pub fn reflected_point(p: DVec3, p1: DVec3, p2: DVec3, working_normal: DVec3) -> DVec3 {
let line = p2 - p1;
let normal = line
.cross(working_normal.normalize_or(DVec3::Z))
.normalize_or(DVec3::ZERO);
if normal.length_squared() < 1e-12 {
p
} else {
p - 2.0 * normal.dot(p - p1) * normal
}
} }
pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) { pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) {
@ -80,11 +120,6 @@ pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) {
*y = p1.y as f64 + my; *y = p1.y as f64 + my;
} }
pub fn mirror_xy_line(line: &mut acadrust::entities::Line, p1: DVec3, p2: DVec3) {
reflect_xy_point(&mut line.start.x, &mut line.start.y, p1, p2);
reflect_xy_point(&mut line.end.x, &mut line.end.y, p1, p2);
}
/// DXF arbitrary-axis algorithm — returns the OCS X and Y basis vectors in WCS /// DXF arbitrary-axis algorithm — returns the OCS X and Y basis vectors in WCS
/// for a given entity normal vector. /// for a given entity normal vector.
/// ///
@ -190,6 +225,7 @@ mod mirror_delegation_tests {
h.apply_transform(&EntityTransform::Mirror { h.apply_transform(&EntityTransform::Mirror {
p1: DVec3::new(5.0, 0.0, 0.0), p1: DVec3::new(5.0, 0.0, 0.0),
p2: DVec3::new(5.0, 1.0, 0.0), p2: DVec3::new(5.0, 1.0, 0.0),
working_normal: DVec3::Z,
}); });
let edges = &h.paths[0].edges; let edges = &h.paths[0].edges;