fix(precision): keep typed coordinates exact in f64 (#311)

Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).

Two-layer fix:

1. Coordinate backbone -> f64. last_point, last_cursor_world,
   dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
   the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
   paths, command-line coordinate entry and the coordinate readout all
   compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
   geometry, projection, UCS-icon/viewcube drawing) with casts only at
   those boundaries.

2. Per-command committed storage -> DVec3. on_point already received a
   DVec3, but many command impls narrowed it back to f32 (let pt =
   pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
   the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
   INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
   wipeout, attdef, table, tolerance, array center, mview, plot_window,
   plus the QDIM_PLACE handler and qdim_collect_points. Preview,
   hit-test and rubber-band paths keep f32 at the boundary.

Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-11 21:26:40 +03:00
commit 57710eec2b
47 changed files with 699 additions and 606 deletions

View file

@ -137,7 +137,7 @@ impl OpenCADStudio {
}; };
self.last_point = Some(wcs); self.last_point = Some(wcs);
self.push_ucs_to_cmd(i); self.push_ucs_to_cmd(i);
let _ = self.feed_command(StepInput::Point(wcs.as_dvec3())); let _ = self.feed_command(StepInput::Point(wcs));
} else { } else {
let _ = self.feed_command(StepInput::Text(token.to_string())); let _ = self.feed_command(StepInput::Text(token.to_string()));
} }
@ -1156,7 +1156,7 @@ impl OpenCADStudio {
.scene .scene
.document .document
.get_entity(handle) .get_entity(handle)
.and_then(|e| break_entity(e, p1.as_vec3(), p2.as_vec3())); .and_then(|e| break_entity(e, p1, p2));
match replacement { match replacement {
Some(frags) => { Some(frags) => {
let label = self.history_label_from_active_cmd(i, "BREAK"); let label = self.history_label_from_active_cmd(i, "BREAK");

View file

@ -468,7 +468,7 @@ impl OpenCADStudio {
// dimension-line point and the gathered geometry now selected. Build // dimension-line point and the gathered geometry now selected. Build
// a continuous chain of linear dimensions across the endpoints. // a continuous chain of linear dimensions across the endpoints.
cmd if cmd.starts_with("QDIM_PLACE ") => { cmd if cmd.starts_with("QDIM_PLACE ") => {
let nums: Vec<f32> = cmd let nums: Vec<f64> = cmd
.split_whitespace() .split_whitespace()
.skip(1) .skip(1)
.filter_map(|s| s.parse().ok()) .filter_map(|s| s.parse().ok())
@ -476,14 +476,14 @@ impl OpenCADStudio {
if nums.len() < 3 { if nums.len() < 3 {
return Some(Task::none()); return Some(Task::none());
} }
let place = glam::Vec3::new(nums[0], nums[1], nums[2]); let place = 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()
.iter() .iter()
.map(|(h, _)| *h) .map(|(h, _)| *h)
.collect(); .collect();
let mut pts: Vec<glam::Vec3> = Vec::new(); let mut pts: Vec<glam::DVec3> = Vec::new();
for h in &handles { for h in &handles {
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) { if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
qdim_collect_points(e, &mut pts); qdim_collect_points(e, &mut pts);
@ -498,10 +498,10 @@ impl OpenCADStudio {
// span dimensions horizontally (ordered by X), else vertically. // span dimensions horizontally (ordered by X), else vertically.
let (minx, maxx) = pts let (minx, maxx) = pts
.iter() .iter()
.fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.x), b.max(p.x))); .fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.x), b.max(p.x)));
let (miny, maxy) = pts let (miny, maxy) = pts
.iter() .iter()
.fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.y), b.max(p.y))); .fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.y), b.max(p.y)));
let horizontal = (maxx - minx) >= (maxy - miny); let horizontal = (maxx - minx) >= (maxy - miny);
if horizontal { if horizontal {
pts.sort_by(|a, b| a.x.total_cmp(&b.x)); pts.sort_by(|a, b| a.x.total_cmp(&b.x));
@ -516,8 +516,8 @@ impl OpenCADStudio {
return Some(Task::none()); return Some(Task::none());
} }
self.push_undo_snapshot(i, "QDIM"); self.push_undo_snapshot(i, "QDIM");
let v = |p: glam::Vec3| { let v = |p: glam::DVec3| {
acadrust::types::Vector3::new(p.x as f64, p.y as f64, p.z as f64) acadrust::types::Vector3::new(p.x, p.y, p.z)
}; };
let mut made = 0usize; let mut made = 0usize;
for w in pts.windows(2) { for w in pts.windows(2) {
@ -530,9 +530,9 @@ impl OpenCADStudio {
}; };
// Dim line passes through the picked perpendicular position. // Dim line passes through the picked perpendicular position.
let def = if horizontal { let def = if horizontal {
glam::Vec3::new((p1.x + p2.x) * 0.5, place.y, 0.0) glam::DVec3::new((p1.x + p2.x) * 0.5, place.y, 0.0)
} else { } else {
glam::Vec3::new(place.x, (p1.y + p2.y) * 0.5, 0.0) glam::DVec3::new(place.x, (p1.y + p2.y) * 0.5, 0.0)
}; };
dim.definition_point = v(def); dim.definition_point = v(def);
dim.base.definition_point = v(def); dim.base.definition_point = v(def);
@ -1124,9 +1124,9 @@ fn find_last_linear_dim(
/// Collect candidate dimension endpoints from an entity for QDIM — line ends, /// Collect candidate dimension endpoints from an entity for QDIM — line ends,
/// polyline vertices, arc endpoints — in world space. /// polyline vertices, arc endpoints — in world space.
fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::Vec3>) { 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::Vec3::new(v.x as f32, v.y as f32, v.z as f32); let p = |v: &acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
match e { match e {
ET::Line(l) => { ET::Line(l) => {
out.push(p(&l.start)); out.push(p(&l.start));
@ -1134,10 +1134,10 @@ fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::Vec3>) {
} }
ET::LwPolyline(pl) => { ET::LwPolyline(pl) => {
for v in &pl.vertices { for v in &pl.vertices {
out.push(glam::Vec3::new( out.push(glam::DVec3::new(
v.location.x as f32, v.location.x,
v.location.y as f32, v.location.y,
pl.elevation as f32, pl.elevation,
)); ));
} }
} }
@ -1148,10 +1148,10 @@ fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::Vec3>) {
} }
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::Vec3::new( out.push(glam::DVec3::new(
(a.center.x + a.radius * ang.cos()) as f32, a.center.x + a.radius * ang.cos(),
(a.center.y + a.radius * ang.sin()) as f32, a.center.y + a.radius * ang.sin(),
a.center.z as f32, a.center.z,
)); ));
} }
} }

View file

@ -282,13 +282,9 @@ impl OpenCADStudio {
let current = self.tabs[i].active_ucs.as_ref(); let current = self.tabs[i].active_ucs.as_ref();
let origin = current let origin = current
.map(|u| { .map(|u| {
glam::Vec3::new( glam::DVec3::new(u.origin.x, u.origin.y, u.origin.z)
u.origin.x as f32,
u.origin.y as f32,
u.origin.z as f32,
)
}) })
.unwrap_or(glam::Vec3::ZERO); .unwrap_or(glam::DVec3::ZERO);
let mut new_ucs = ucs_rotated_z(origin, rad); let mut new_ucs = ucs_rotated_z(origin, rad);
// If already had axes, compose rotation on top // If already had axes, compose rotation on top
if let Some(ref ucs) = self.tabs[i].active_ucs { if let Some(ref ucs) = self.tabs[i].active_ucs {
@ -302,7 +298,7 @@ impl OpenCADStudio {
ucs.y_axis.y as f32, ucs.y_axis.y as f32,
ucs.y_axis.z as f32, ucs.y_axis.z as f32,
); );
let z_ax = ucs_z_axis(ucs); let z_ax = ucs_z_axis(ucs).as_vec3();
let rot = glam::Quat::from_axis_angle(z_ax, rad); let rot = glam::Quat::from_axis_angle(z_ax, rad);
let nx = rot * old_x; let nx = rot * old_x;
let ny = rot * old_y; let ny = rot * old_y;

View file

@ -169,7 +169,7 @@ pub(super) struct DocumentTab {
pub(super) wireframe: bool, pub(super) wireframe: bool,
pub(super) render_mode: acadrust::entities::ViewportRenderMode, pub(super) render_mode: acadrust::entities::ViewportRenderMode,
pub(super) visual_style: String, pub(super) visual_style: String,
pub(super) last_cursor_world: glam::Vec3, pub(super) last_cursor_world: glam::DVec3,
pub(super) last_cursor_screen: iced::Point, pub(super) last_cursor_screen: iced::Point,
/// Base point (`App::last_point`) projected to viewport pixels, refreshed /// Base point (`App::last_point`) projected to viewport pixels, refreshed
/// on cursor move. Lets the dynamic-input overlay place the distance label /// on cursor move. Lets the dynamic-input overlay place the distance label
@ -185,10 +185,10 @@ pub(super) struct DocumentTab {
pub(super) dyn_guide: crate::command::DynGuide, pub(super) dyn_guide: crate::command::DynGuide,
/// World-space anchor the current step's values are measured from. `None` /// World-space anchor the current step's values are measured from. `None`
/// falls back to `App::last_point`. /// falls back to `App::last_point`.
pub(super) dyn_anchor: Option<glam::Vec3>, pub(super) dyn_anchor: Option<glam::DVec3>,
/// Far end of a reference line through `dyn_anchor` (for the `Perp` guide: /// Far end of a reference line through `dyn_anchor` (for the `Perp` guide:
/// the base edge / major axis the offset is measured square to). /// the base edge / major axis the offset is measured square to).
pub(super) dyn_ref: Option<glam::Vec3>, pub(super) dyn_ref: Option<glam::DVec3>,
/// `dyn_ref` projected to viewport pixels. /// `dyn_ref` projected to viewport pixels.
pub(super) dyn_ref_screen: Option<iced::Point>, pub(super) dyn_ref_screen: Option<iced::Point>,
/// Index of the field that TAB has focused (the one keystrokes edit). /// Index of the field that TAB has focused (the one keystrokes edit).
@ -360,9 +360,9 @@ impl DocumentTab {
let (o, x, y, z) = self.ucs_xform().axes(); 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); let origin = glam::Vec3::new(o.x as f32, o.y as f32, o.z as f32);
glam::Mat4::from_cols( glam::Mat4::from_cols(
x.extend(0.0), x.as_vec3().extend(0.0),
y.extend(0.0), y.as_vec3().extend(0.0),
z.extend(0.0), z.as_vec3().extend(0.0),
origin.extend(1.0), origin.extend(1.0),
) )
} }
@ -421,7 +421,7 @@ impl DocumentTab {
wireframe: false, wireframe: false,
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D, render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
visual_style: "Wireframe 2D".into(), visual_style: "Wireframe 2D".into(),
last_cursor_world: glam::Vec3::ZERO, last_cursor_world: glam::DVec3::ZERO,
last_cursor_screen: iced::Point::ORIGIN, last_cursor_screen: iced::Point::ORIGIN,
last_point_screen: None, last_point_screen: None,
dyn_fields: Vec::new(), dyn_fields: Vec::new(),

View file

@ -20,7 +20,7 @@ pub(super) enum CoordKind {
/// "x,y,z" → Vec3(x, y, z) /// "x,y,z" → Vec3(x, y, z)
/// 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::Vec3, CoordKind)> { pub(super) 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)
@ -29,14 +29,14 @@ pub(super) fn parse_coord(text: &str) -> Option<(glam::Vec3, CoordKind)> {
} else { } else {
(CoordKind::Default, trimmed) (CoordKind::Default, trimmed)
}; };
let parts: Vec<f32> = rest let parts: Vec<f64> = rest
.split(|c| c == ',' || c == ';') .split(|c| c == ',' || c == ';')
.map(|s| s.trim()) .map(|s| s.trim())
.filter_map(|s| s.parse().ok()) .filter_map(|s| s.parse().ok())
.collect(); .collect();
match parts.as_slice() { match parts.as_slice() {
[x, y] => Some((glam::Vec3::new(*x, *y, 0.0), kind)), [x, y] => Some((glam::DVec3::new(*x, *y, 0.0), kind)),
[x, y, z] => Some((glam::Vec3::new(*x, *y, *z), kind)), [x, y, z] => Some((glam::DVec3::new(*x, *y, *z), kind)),
_ => None, _ => None,
} }
} }
@ -54,29 +54,28 @@ pub(super) fn parse_coord(text: &str) -> Option<(glam::Vec3, CoordKind)> {
/// is just the transpose (the dot products in `to_ucs`); no matrix inversion. /// is just the transpose (the dot products in `to_ucs`); no matrix inversion.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(super) struct UcsXform { pub(super) struct UcsXform {
origin: glam::Vec3, origin: glam::DVec3,
x: glam::Vec3, x: glam::DVec3,
y: glam::Vec3, y: glam::DVec3,
z: glam::Vec3, z: glam::DVec3,
} }
impl UcsXform { impl UcsXform {
/// Plain WCS — no active UCS. /// Plain WCS — no active UCS.
pub(super) fn identity() -> Self { pub(super) fn identity() -> Self {
Self { Self {
origin: glam::Vec3::ZERO, origin: glam::DVec3::ZERO,
x: glam::Vec3::X, x: glam::DVec3::X,
y: glam::Vec3::Y, y: glam::DVec3::Y,
z: glam::Vec3::Z, z: glam::DVec3::Z,
} }
} }
pub(super) fn from_ucs(ucs: &Ucs) -> Self { pub(super) fn from_ucs(ucs: &Ucs) -> Self {
let v = let v = |a: acadrust::types::Vector3| glam::DVec3::new(a.x, a.y, a.z);
|a: acadrust::types::Vector3| glam::Vec3::new(a.x as f32, a.y as f32, a.z as f32); let x = v(ucs.x_axis).normalize_or(glam::DVec3::X);
let x = v(ucs.x_axis).normalize_or(glam::Vec3::X); let y = v(ucs.y_axis).normalize_or(glam::DVec3::Y);
let y = v(ucs.y_axis).normalize_or(glam::Vec3::Y); let z = x.cross(y).normalize_or(glam::DVec3::Z);
let z = x.cross(y).normalize_or(glam::Vec3::Z);
Self { origin: v(ucs.origin), x, y, z } Self { origin: v(ucs.origin), x, y, z }
} }
@ -86,45 +85,46 @@ impl UcsXform {
/// True when this is plain WCS — lets callers skip the conversion. /// True when this is plain WCS — lets callers skip the conversion.
pub(super) fn is_identity(&self) -> bool { pub(super) fn is_identity(&self) -> bool {
self.origin == glam::Vec3::ZERO self.origin == glam::DVec3::ZERO
&& self.x == glam::Vec3::X && self.x == glam::DVec3::X
&& self.y == glam::Vec3::Y && self.y == glam::DVec3::Y
&& self.z == glam::Vec3::Z && self.z == glam::DVec3::Z
} }
/// UCS point → WCS. /// UCS point → WCS.
pub(super) fn to_wcs(&self, p: glam::Vec3) -> glam::Vec3 { pub(super) fn to_wcs(&self, p: glam::DVec3) -> glam::DVec3 {
self.origin + self.x * p.x + self.y * p.y + self.z * p.z self.origin + self.x * p.x + self.y * p.y + self.z * p.z
} }
/// WCS point → UCS. /// WCS point → UCS.
pub(super) fn to_ucs(&self, p: glam::Vec3) -> glam::Vec3 { pub(super) fn to_ucs(&self, p: glam::DVec3) -> glam::DVec3 {
let d = p - self.origin; let d = p - self.origin;
glam::Vec3::new(d.dot(self.x), d.dot(self.y), d.dot(self.z)) glam::DVec3::new(d.dot(self.x), d.dot(self.y), d.dot(self.z))
} }
/// UCS direction → WCS (rotation only, no origin shift). /// UCS direction → WCS (rotation only, no origin shift).
pub(super) fn vec_to_wcs(&self, v: glam::Vec3) -> glam::Vec3 { pub(super) fn vec_to_wcs(&self, v: glam::DVec3) -> glam::DVec3 {
self.x * v.x + self.y * v.y + self.z * v.z self.x * v.x + self.y * v.y + self.z * v.z
} }
/// WCS direction → UCS (rotation only, no origin shift). /// WCS direction → UCS (rotation only, no origin shift).
pub(super) fn vec_to_ucs(&self, v: glam::Vec3) -> glam::Vec3 { pub(super) fn vec_to_ucs(&self, v: glam::DVec3) -> glam::DVec3 {
glam::Vec3::new(v.dot(self.x), v.dot(self.y), v.dot(self.z)) glam::DVec3::new(v.dot(self.x), v.dot(self.y), v.dot(self.z))
} }
/// `(origin, x, y, z)` axes in WCS — for drawing the UCS icon. /// `(origin, x, y, z)` axes in WCS — for drawing the UCS icon.
pub(super) fn axes(&self) -> (glam::Vec3, glam::Vec3, glam::Vec3, glam::Vec3) { pub(super) fn axes(&self) -> (glam::DVec3, glam::DVec3, glam::DVec3, glam::DVec3) {
(self.origin, self.x, self.y, self.z) (self.origin, self.x, self.y, self.z)
} }
/// 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). /// a `Mat4` rotation directly (ViewCube, OTRACK ray directions). The GPU /
/// screen layer is f32, so the axes downcast here at the boundary.
pub(super) fn rotation_mat(&self) -> glam::Mat4 { pub(super) fn rotation_mat(&self) -> glam::Mat4 {
glam::Mat4::from_cols( glam::Mat4::from_cols(
self.x.extend(0.0), self.x.as_vec3().extend(0.0),
self.y.extend(0.0), self.y.as_vec3().extend(0.0),
self.z.extend(0.0), self.z.as_vec3().extend(0.0),
glam::Vec4::W, glam::Vec4::W,
) )
} }
@ -135,26 +135,26 @@ impl UcsXform {
/// Rotate a UCS-local offset into WCS without applying the origin /// Rotate a UCS-local offset into WCS without applying the origin
/// translation — used for relative coordinate entry, where only the /// translation — used for relative coordinate entry, where only the
/// axis orientation matters, not the UCS origin. /// axis orientation matters, not the UCS origin.
pub(super) fn ucs_rotate_vec(offset: glam::Vec3, ucs: &Ucs) -> glam::Vec3 { pub(super) fn ucs_rotate_vec(offset: glam::DVec3, ucs: &Ucs) -> glam::DVec3 {
UcsXform::from_ucs(ucs).vec_to_wcs(offset) UcsXform::from_ucs(ucs).vec_to_wcs(offset)
} }
/// Convert a point from UCS local coordinates to WCS. /// Convert a point from UCS local coordinates to WCS.
pub(super) fn ucs_to_wcs(pt: glam::Vec3, ucs: &Ucs) -> glam::Vec3 { pub(super) fn ucs_to_wcs(pt: glam::DVec3, ucs: &Ucs) -> glam::DVec3 {
UcsXform::from_ucs(ucs).to_wcs(pt) UcsXform::from_ucs(ucs).to_wcs(pt)
} }
/// Return the normalised Z axis of a UCS (cross product of X and Y axes). /// Return the normalised Z axis of a UCS (cross product of X and Y axes).
pub(super) fn ucs_z_axis(ucs: &Ucs) -> glam::Vec3 { pub(super) fn ucs_z_axis(ucs: &Ucs) -> glam::DVec3 {
UcsXform::from_ucs(ucs).axes().3 UcsXform::from_ucs(ucs).axes().3
} }
/// Build a UCS with `origin` and axes rotated by `angle_z_rad` around the Z axis. /// Build a UCS with `origin` and axes rotated by `angle_z_rad` around the Z axis.
pub(super) fn ucs_rotated_z(origin: glam::Vec3, angle_z: f32) -> Ucs { pub(super) fn ucs_rotated_z(origin: glam::DVec3, angle_z: f32) -> Ucs {
let cos = angle_z.cos() as f64; let cos = angle_z.cos() as f64;
let sin = angle_z.sin() as f64; let sin = angle_z.sin() as f64;
let mut ucs = Ucs::new("*ACTIVE*"); let mut ucs = Ucs::new("*ACTIVE*");
ucs.origin = acadrust::types::Vector3::new(origin.x as f64, origin.y as f64, origin.z as f64); ucs.origin = acadrust::types::Vector3::new(origin.x, origin.y, origin.z);
ucs.x_axis = acadrust::types::Vector3::new(cos, sin, 0.0); ucs.x_axis = acadrust::types::Vector3::new(cos, sin, 0.0);
ucs.y_axis = acadrust::types::Vector3::new(-sin, cos, 0.0); ucs.y_axis = acadrust::types::Vector3::new(-sin, cos, 0.0);
ucs ucs
@ -181,15 +181,15 @@ pub(super) fn grid_plane_from_camera(pitch: f32, yaw: f32) -> GridPlane {
/// Constrain `pt` to the nearest 90° direction from `base`, in the active UCS /// Constrain `pt` to the nearest 90° direction from `base`, in the active UCS
/// plane — ortho follows the user's coordinate system, not world axes. `xf` is /// plane — ortho follows the user's coordinate system, not world axes. `xf` is
/// identity for plain WCS, so the world-XY behaviour is unchanged there. /// identity for plain WCS, so the world-XY behaviour is unchanged there.
pub(super) fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3, xf: &UcsXform) -> glam::Vec3 { pub(super) fn ortho_constrain(pt: glam::DVec3, base: glam::DVec3, xf: &UcsXform) -> glam::DVec3 {
let p = xf.to_ucs(pt); let p = xf.to_ucs(pt);
let b = xf.to_ucs(base); let b = xf.to_ucs(base);
let dx = (p.x - b.x).abs(); let dx = (p.x - b.x).abs();
let dy = (p.y - b.y).abs(); let dy = (p.y - b.y).abs();
let c = if dx >= dy { let c = if dx >= dy {
glam::Vec3::new(p.x, b.y, p.z) glam::DVec3::new(p.x, b.y, p.z)
} else { } else {
glam::Vec3::new(b.x, p.y, p.z) glam::DVec3::new(b.x, p.y, p.z)
}; };
xf.to_wcs(c) xf.to_wcs(c)
} }
@ -197,11 +197,11 @@ pub(super) fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3, xf: &UcsXform) -
/// Constrain `pt` to the nearest polar angle multiple from `base`, measured in /// Constrain `pt` to the nearest polar angle multiple from `base`, measured in
/// the active UCS plane (identity `xf` = world XY, Z-up). /// the active UCS plane (identity `xf` = world XY, Z-up).
pub(super) fn polar_constrain( pub(super) fn polar_constrain(
pt: glam::Vec3, pt: glam::DVec3,
base: glam::Vec3, base: glam::DVec3,
step_deg: f32, step_deg: f32,
xf: &UcsXform, xf: &UcsXform,
) -> glam::Vec3 { ) -> glam::DVec3 {
let p = xf.to_ucs(pt); let p = xf.to_ucs(pt);
let b = xf.to_ucs(base); let b = xf.to_ucs(base);
let dx = p.x - b.x; let dx = p.x - b.x;
@ -210,10 +210,10 @@ pub(super) fn polar_constrain(
if dist < 1e-6 { if dist < 1e-6 {
return pt; return pt;
} }
let step = step_deg.to_radians(); let step = (step_deg as f64).to_radians();
let angle = dy.atan2(dx); let angle = dy.atan2(dx);
let snapped = (angle / step).round() * step; let snapped = (angle / step).round() * step;
xf.to_wcs(glam::Vec3::new( xf.to_wcs(glam::DVec3::new(
b.x + dist * snapped.cos(), b.x + dist * snapped.cos(),
b.y + dist * snapped.sin(), b.y + dist * snapped.sin(),
p.z, p.z,
@ -224,18 +224,18 @@ pub(super) fn polar_constrain(
/// screen pixels of the nearest polar ray; otherwise the cursor is left free /// screen pixels of the nearest polar ray; otherwise the cursor is left free
/// so POLAR behaves as if off when pointing away from every angle (issue #70). /// so POLAR behaves as if off when pointing away from every angle (issue #70).
pub(super) fn polar_constrain_near( pub(super) fn polar_constrain_near(
pt: glam::Vec3, pt: glam::DVec3,
base: glam::Vec3, base: glam::DVec3,
step_deg: f32, step_deg: f32,
view_rot: glam::Mat4, view_rot: glam::Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: iced::Rectangle, bounds: iced::Rectangle,
tol_px: f32, tol_px: f32,
xf: &UcsXform, xf: &UcsXform,
) -> glam::Vec3 { ) -> glam::DVec3 {
let snapped = polar_constrain(pt, base, step_deg, xf); let snapped = polar_constrain(pt, base, step_deg, xf);
let to_screen = |w: glam::Vec3| { let to_screen = |w: glam::DVec3| {
let ndc = view_rot.project_point3((w.as_dvec3() - eye).as_vec3()); let ndc = view_rot.project_point3((w - eye).as_vec3());
( (
(ndc.x + 1.0) * 0.5 * bounds.width, (ndc.x + 1.0) * 0.5 * bounds.width,
(1.0 - ndc.y) * 0.5 * bounds.height, (1.0 - ndc.y) * 0.5 * bounds.height,

View file

@ -416,7 +416,7 @@ pub(super) struct OpenCADStudio {
/// Whether the layout/paper-space tabs are shown at the bottom (LAYOUTTAB). /// Whether the layout/paper-space tabs are shown at the bottom (LAYOUTTAB).
show_layout_tabs: bool, show_layout_tabs: bool,
/// Last point committed by a drawing command — used as ortho/polar base. /// Last point committed by a drawing command — used as ortho/polar base.
last_point: Option<glam::Vec3>, last_point: Option<glam::DVec3>,
/// Endpoint + unit exit-tangent of the most recently drawn line/arc, so /// Endpoint + unit exit-tangent of the most recently drawn line/arc, so
/// `ARC_CONT` (Arc → Continue) can start tangentially from where drawing /// `ARC_CONT` (Arc → Continue) can start tangentially from where drawing
/// ended. `None` once a non-line/arc entity is committed. /// ended. `None` once a non-line/arc entity is committed.

View file

@ -283,13 +283,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
// (issue #69). // (issue #69).
if let Some((base, dir)) = self.otrack_active { if let Some((base, dir)) = self.otrack_active {
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) { if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
let pt = base + dir * dist as f32; // Typed distance along the tracking ray: keep the
// magnitude exact in f64 (ray origin/dir are the
// screen-space snapper's f32).
let pt = base.as_dvec3() + dir.as_dvec3() * dist;
self.last_point = Some(pt); self.last_point = Some(pt);
self.dyn_user_reshaped = false; self.dyn_user_reshaped = false;
self.sync_dyn_fields(); self.sync_dyn_fields();
self.reset_tracking_after_point(); self.reset_tracking_after_point();
self.push_ucs_to_cmd(i); self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt.as_dvec3())); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
if let Some(r) = result { if let Some(r) = result {
let task = self.apply_cmd_result(r); let task = self.apply_cmd_result(r);
self.refresh_active_cmd_preview(i); self.refresh_active_cmd_preview(i);
@ -331,7 +334,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.sync_dyn_fields(); self.sync_dyn_fields();
self.reset_tracking_after_point(); self.reset_tracking_after_point();
self.push_ucs_to_cmd(i); self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt.as_dvec3())); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt));
if let Some(r) = result { if let Some(r) = result {
let task = self.apply_cmd_result(r); let task = self.apply_cmd_result(r);
// The rubber-band preview that the command // The rubber-band preview that the command

View file

@ -171,9 +171,9 @@ impl OpenCADStudio {
self.tabs[i].dyn_guide = spec.guide; self.tabs[i].dyn_guide = spec.guide;
self.tabs[i].dyn_anchor = match spec.anchor { self.tabs[i].dyn_anchor = match spec.anchor {
crate::command::DynAnchor::LastPoint => self.last_point, crate::command::DynAnchor::LastPoint => self.last_point,
crate::command::DynAnchor::Point(p) => Some(p.as_vec3()), crate::command::DynAnchor::Point(p) => Some(p),
}; };
self.tabs[i].dyn_ref = spec.ref_point.map(|v| v.as_vec3()); self.tabs[i].dyn_ref = spec.ref_point;
} }
/// Track cursor dwell over a selected entity's grip. Sets /// Track cursor dwell over a selected entity's grip. Sets
@ -212,13 +212,11 @@ impl OpenCADStudio {
/// `None` for any other entity kind. Uses `last_point` (the final pick) to /// `None` for any other entity kind. Uses `last_point` (the final pick) to
/// pick the endpoint the pen actually finished on. /// pick the endpoint the pen actually finished on.
pub(in crate::app) fn update_cont_anchor(&mut self, entity: &acadrust::EntityType) { pub(in crate::app) fn update_cont_anchor(&mut self, entity: &acadrust::EntityType) {
let last = self let last = self.last_point;
.last_point
.map(|v| glam::DVec3::new(v.x as f64, v.y as f64, v.z as f64));
self.cont_anchor = crate::modules::draw::draw::arc::continue_anchor(entity, last); self.cont_anchor = crate::modules::draw::draw::arc::continue_anchor(entity, last);
} }
pub(in crate::app) fn dyn_resolve_point(&self) -> Option<glam::Vec3> { pub(in crate::app) fn dyn_resolve_point(&self) -> Option<glam::DVec3> {
use crate::app::document::DynComponent; use crate::app::document::DynComponent;
let i = self.active_tab; let i = self.active_tab;
let fields = &self.tabs[i].dyn_fields; let fields = &self.tabs[i].dyn_fields;
@ -229,7 +227,7 @@ impl OpenCADStudio {
let base = self.tabs[i] let base = self.tabs[i]
.dyn_anchor .dyn_anchor
.or(self.last_point) .or(self.last_point)
.unwrap_or(glam::Vec3::ZERO); .unwrap_or(glam::DVec3::ZERO);
// Buffer value parsed as f32 (de-scaled by the role so a typed diameter // Buffer value parsed as f32 (de-scaled by the role so a typed diameter
// becomes a radius), or the supplied geometric live value. Width/Height // becomes a radius), or the supplied geometric live value. Width/Height
// are shown unsigned, so a typed value normally takes the sign of the // are shown unsigned, so a typed value normally takes the sign of the
@ -241,13 +239,13 @@ impl OpenCADStudio {
// `dyn_user_reshaped`, so it cleanly distinguishes comma-cartesian entry // `dyn_user_reshaped`, so it cleanly distinguishes comma-cartesian entry
// from Tab-separated dimension entry (#269). // from Tab-separated dimension entry (#269).
let comma_cartesian = self.dyn_user_reshaped; let comma_cartesian = self.dyn_user_reshaped;
let val = |idx: usize, live: f32| -> f32 { let val = |idx: usize, live: f64| -> f64 {
match fields[idx] match fields[idx]
.buffer .buffer
.as_ref() .as_ref()
.map(|s| s.trim().replace(',', ".")) .map(|s| s.trim().replace(',', "."))
.and_then(|s| crate::app::expr_eval::eval_number(&s).map(|v| v as f32)) .and_then(|s| crate::app::expr_eval::eval_number(&s))
.map(|v| v / fields[idx].role.value_scale()) .map(|v| v / fields[idx].role.value_scale() as f64)
{ {
Some(v) => { Some(v) => {
if matches!( if matches!(
@ -277,12 +275,12 @@ impl OpenCADStudio {
// A typed angle is shown unsigned (0..180); give it the sign of the // A typed angle is shown unsigned (0..180); give it the sign of the
// cursor's current side so an entry made below the X axis sweeps // cursor's current side so an entry made below the X axis sweeps
// downward to match the arc instead of mirroring up. Untyped → live. // downward to match the arc instead of mirroring up. Untyped → live.
let angle_rad = |idx: usize| -> f32 { let angle_rad = |idx: usize| -> f64 {
match fields[idx] match fields[idx]
.buffer .buffer
.as_ref() .as_ref()
.map(|s| s.trim().replace(',', ".")) .map(|s| s.trim().replace(',', "."))
.and_then(|s| crate::app::expr_eval::eval_number(&s).map(|v| v as f32)) .and_then(|s| crate::app::expr_eval::eval_number(&s))
{ {
Some(mag) => mag.abs().to_radians().copysign(dy), Some(mag) => mag.abs().to_radians().copysign(dy),
None => live_a, None => live_a,
@ -298,13 +296,13 @@ impl OpenCADStudio {
(self.tabs[i].dyn_ref, comps.as_slice()) (self.tabs[i].dyn_ref, comps.as_slice())
{ {
let axis = (ref_pt - base).normalize_or_zero(); let axis = (ref_pt - base).normalize_or_zero();
let perp = glam::Vec3::new(-axis.y, axis.x, 0.0); let perp = glam::DVec3::new(-axis.y, axis.x, 0.0);
let signed = (w - base).dot(perp); let signed = (w - base).dot(perp);
let typed = fields[0] let typed = fields[0]
.buffer .buffer
.as_ref() .as_ref()
.map(|s| s.trim().replace(',', ".")) .map(|s| s.trim().replace(',', "."))
.and_then(|s| crate::app::expr_eval::eval_number(&s).map(|v| v as f32)); .and_then(|s| crate::app::expr_eval::eval_number(&s));
let h = match typed { let h = match typed {
Some(v) => v.abs().copysign(signed), Some(v) => v.abs().copysign(signed),
None => signed, None => signed,
@ -316,28 +314,28 @@ impl OpenCADStudio {
// position relative to base; typed values are relative deltas. // position relative to base; typed values are relative deltas.
let has_base = self.last_point.is_some(); let has_base = self.last_point.is_some();
// Relative result: base + the typed UCS-frame offset mapped to world. // Relative result: base + the typed UCS-frame offset mapped to world.
let rel = |off_ucs: glam::Vec3| base + xf.vec_to_wcs(off_ucs); let rel = |off_ucs: glam::DVec3| base + xf.vec_to_wcs(off_ucs);
match comps.as_slice() { match comps.as_slice() {
[DynComponent::X, DynComponent::Y] if has_base => { [DynComponent::X, DynComponent::Y] if has_base => {
Some(rel(glam::Vec3::new(val(0, dx), val(1, dy), 0.0))) Some(rel(glam::DVec3::new(val(0, dx), val(1, dy), 0.0)))
} }
[DynComponent::X, DynComponent::Y] => { [DynComponent::X, DynComponent::Y] => {
Some(glam::Vec3::new(val(0, w.x), val(1, w.y), base.z)) Some(glam::DVec3::new(val(0, w.x), val(1, w.y), base.z))
} }
[DynComponent::X, DynComponent::Y, DynComponent::Z] if has_base => { [DynComponent::X, DynComponent::Y, DynComponent::Z] if has_base => {
Some(rel(glam::Vec3::new(val(0, dx), val(1, dy), val(2, dz)))) Some(rel(glam::DVec3::new(val(0, dx), val(1, dy), val(2, dz))))
} }
[DynComponent::X, DynComponent::Y, DynComponent::Z] => { [DynComponent::X, DynComponent::Y, DynComponent::Z] => {
Some(glam::Vec3::new(val(0, w.x), val(1, w.y), val(2, base.z))) Some(glam::DVec3::new(val(0, w.x), val(1, w.y), val(2, base.z)))
} }
[DynComponent::Distance, DynComponent::Angle] => { [DynComponent::Distance, DynComponent::Angle] => {
let d = val(0, live_d); let d = val(0, live_d);
let a = angle_rad(1); let a = angle_rad(1);
Some(rel(glam::Vec3::new(d * a.cos(), d * a.sin(), 0.0))) Some(rel(glam::DVec3::new(d * a.cos(), d * a.sin(), 0.0)))
} }
[DynComponent::Distance] => { [DynComponent::Distance] => {
// Keep the cursor's direction (in UCS), override the magnitude. // Keep the cursor's direction (in UCS), override the magnitude.
let dir = glam::Vec3::new(dx, dy, 0.0).normalize_or(glam::Vec3::X); let dir = glam::DVec3::new(dx, dy, 0.0).normalize_or(glam::DVec3::X);
Some(rel(dir * val(0, live_d))) Some(rel(dir * val(0, live_d)))
} }
[DynComponent::Angle] => { [DynComponent::Angle] => {
@ -346,7 +344,7 @@ impl OpenCADStudio {
// magnitude — keep it literal. Only the polar Distance+Angle // magnitude — keep it literal. Only the polar Distance+Angle
// pair uses the cursor-signed `angle_rad`. // pair uses the cursor-signed `angle_rad`.
let a = val(0, live_a.to_degrees()).to_radians(); let a = val(0, live_a.to_degrees()).to_radians();
Some(rel(glam::Vec3::new(live_d * a.cos(), live_d * a.sin(), 0.0))) Some(rel(glam::DVec3::new(live_d * a.cos(), live_d * a.sin(), 0.0)))
} }
_ => None, _ => None,
} }
@ -433,7 +431,7 @@ impl OpenCADStudio {
let anchor = self.tabs[i] let anchor = self.tabs[i]
.dyn_anchor .dyn_anchor
.or(self.last_point) .or(self.last_point)
.unwrap_or(glam::Vec3::ZERO); .unwrap_or(glam::DVec3::ZERO);
let cur = self.tabs[i].last_cursor_world; let cur = self.tabs[i].last_cursor_world;
let a_cur = (cur.y - anchor.y).atan2(cur.x - anchor.x); let a_cur = (cur.y - anchor.y).atan2(cur.x - anchor.x);
let a_ref = self.tabs[i] let a_ref = self.tabs[i]
@ -441,11 +439,11 @@ impl OpenCADStudio {
.map(|r| (r.y - anchor.y).atan2(r.x - anchor.x)) .map(|r| (r.y - anchor.y).atan2(r.x - anchor.x))
.unwrap_or(0.0); .unwrap_or(0.0);
let mut d = a_cur - a_ref; let mut d = a_cur - a_ref;
while d > std::f32::consts::PI { while d > std::f64::consts::PI {
d -= std::f32::consts::TAU; d -= std::f64::consts::TAU;
} }
while d <= -std::f32::consts::PI { while d <= -std::f64::consts::PI {
d += std::f32::consts::TAU; d += std::f64::consts::TAU;
} }
if d < 0.0 { if d < 0.0 {
format!("-{t}") format!("-{t}")
@ -479,7 +477,10 @@ impl OpenCADStudio {
.find_map(|f| f.buffer.clone()) .find_map(|f| f.buffer.clone())
{ {
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) { if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
let pt = base + dir * dist as f32; // Distance is typed, so resolve in f64: the tracking ray
// origin/dir come from the screen-space snapper (f32) but
// the magnitude the user entered stays exact.
let pt = base.as_dvec3() + dir.as_dvec3() * dist;
self.last_point = Some(pt); self.last_point = Some(pt);
for f in self.tabs[i].dyn_fields.iter_mut() { for f in self.tabs[i].dyn_fields.iter_mut() {
f.buffer = None; f.buffer = None;
@ -489,7 +490,7 @@ impl OpenCADStudio {
self.sync_dyn_fields(); self.sync_dyn_fields();
self.reset_tracking_after_point(); self.reset_tracking_after_point();
self.push_ucs_to_cmd(i); self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt.as_dvec3())); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
let task = result.map(|r| self.apply_cmd_result(r))?; let task = result.map(|r| self.apply_cmd_result(r))?;
self.refresh_active_cmd_preview(i); self.refresh_active_cmd_preview(i);
return Some(task); return Some(task);
@ -548,7 +549,7 @@ impl OpenCADStudio {
self.sync_dyn_fields(); self.sync_dyn_fields();
self.reset_tracking_after_point(); self.reset_tracking_after_point();
self.push_ucs_to_cmd(i); self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt.as_dvec3())); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
for f in self.tabs[i].dyn_fields.iter_mut() { for f in self.tabs[i].dyn_fields.iter_mut() {
f.buffer = None; f.buffer = None;
} }
@ -578,7 +579,7 @@ impl OpenCADStudio {
let previews = self.tabs[i] let previews = self.tabs[i]
.active_cmd .active_cmd
.as_mut() .as_mut()
.map(|c| c.on_preview_wires(cur.as_dvec3())) .map(|c| c.on_preview_wires(cur))
.unwrap_or_default(); .unwrap_or_default();
self.tabs[i].scene.set_preview_wires(previews); self.tabs[i].scene.set_preview_wires(previews);
} }

View file

@ -190,7 +190,7 @@ impl OpenCADStudio {
) )
}); });
let pick = |cam: &crate::scene::view::camera::Camera| match plane { let pick = |cam: &crate::scene::view::camera::Camera| match plane {
Some((normal, origin)) => cam.pick_on_plane(p, bounds, normal, origin), Some((normal, origin)) => cam.pick_on_plane(p, bounds, normal.as_vec3(), origin),
None => cam.pick_on_target_plane(p, bounds), None => cam.pick_on_target_plane(p, bounds),
}; };
match edit_cam { match edit_cam {
@ -592,7 +592,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
sel.orbit_pivot = self.tabs[i] sel.orbit_pivot = self.tabs[i]
.scene .scene
.orbit_pivot() .orbit_pivot()
.or(Some(self.tabs[i].last_cursor_world.as_dvec3())); .or(Some(self.tabs[i].last_cursor_world));
} }
let pivot = sel.orbit_pivot; let pivot = sel.orbit_pivot;
drop(sel); drop(sel);
@ -768,13 +768,13 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
} }
if snap_hit.is_none() { if snap_hit.is_none() {
let base = grip.origin_world.as_vec3(); let base = grip.origin_world;
let ucs_xf = self.tabs[i].ucs_xform(); let ucs_xf = self.tabs[i].ucs_xform();
if self.ortho_mode { if self.ortho_mode {
snapped = ortho_constrain(snapped.as_vec3(), base, &ucs_xf).as_dvec3(); snapped = ortho_constrain(snapped, base, &ucs_xf);
} else if self.polar_mode { } else if self.polar_mode {
snapped = polar_constrain_near( snapped = polar_constrain_near(
snapped.as_vec3(), snapped,
base, base,
self.polar_increment_deg, self.polar_increment_deg,
view_rot, view_rot,
@ -782,8 +782,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
bounds, bounds,
self.snapper.osnap_radius_px, self.snapper.osnap_radius_px,
&ucs_xf, &ucs_xf,
) );
.as_dvec3();
} }
} }
@ -835,7 +834,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
height: vp_size.1, height: vp_size.1,
}; };
let world = self.cursor_model_point(i, &edit_cam, p, bounds); let world = self.cursor_model_point(i, &edit_cam, p, bounds);
self.tabs[i].last_cursor_world = world.as_vec3(); self.tabs[i].last_cursor_world = world;
} }
// Rollover highlight: when idle (no active command, no // Rollover highlight: when idle (no active command, no
@ -936,7 +935,10 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
) )
} else { } else {
let (go, gr) = self.tabs[i].ucs_grid_basis(); let (go, gr) = self.tabs[i].ucs_grid_basis();
self.snapper.from_point = self.last_point; // The snapper is a screen-space (f32) engine; the f64
// base only matters for typed-input precision, so hand it
// the downcast point here.
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
self.snapper self.snapper
.snap(snap_cursor, p, &all_wires[..], view_rot, eye, bounds, go, gr) .snap(snap_cursor, p, &all_wires[..], view_rot, eye, bounds, go, gr)
}; };
@ -980,7 +982,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
eye, eye,
bounds, bounds,
step, step,
self.last_point, self.last_point.map(|p| p.as_vec3()),
self.ortho_mode && !is_window_corner, self.ortho_mode && !is_window_corner,
ucs, ucs,
) )
@ -997,7 +999,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
if self.tabs[i].snap_result.is_none() && otrack_hit.is_none() { if self.tabs[i].snap_result.is_none() && otrack_hit.is_none() {
if let Some(par) = self.snapper.parallel_snap( if let Some(par) = self.snapper.parallel_snap(
cursor_world.as_vec3(), cursor_world.as_vec3(),
self.last_point, self.last_point.map(|p| p.as_vec3()),
view_rot, view_rot,
eye, eye,
bounds, bounds,
@ -1005,7 +1007,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
if let (Some(base), Some((dir, _))) = if let (Some(base), Some((dir, _))) =
(self.last_point, self.snapper.parallel_ref) (self.last_point, self.snapper.parallel_ref)
{ {
self.otrack_active = Some((base, dir)); self.otrack_active = Some((base.as_vec3(), dir));
} }
self.tabs[i].snap_result = Some(par); self.tabs[i].snap_result = Some(par);
} }
@ -1034,11 +1036,10 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
if let Some(base) = self.last_point { if let Some(base) = self.last_point {
let ucs_xf = self.tabs[i].ucs_xform(); let ucs_xf = self.tabs[i].ucs_xform();
if self.ortho_mode { if self.ortho_mode {
pt = ortho_constrain(pt.as_vec3(), base, &ucs_xf) pt = ortho_constrain(pt, base, &ucs_xf);
.as_dvec3();
} else if self.polar_mode { } else if self.polar_mode {
pt = polar_constrain_near( pt = polar_constrain_near(
pt.as_vec3(), pt,
base, base,
self.polar_increment_deg, self.polar_increment_deg,
view_rot, view_rot,
@ -1046,8 +1047,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
bounds, bounds,
self.snapper.osnap_radius_px, self.snapper.osnap_radius_px,
&ucs_xf, &ucs_xf,
) );
.as_dvec3();
} }
} }
} }
@ -1060,7 +1060,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
} }
pt pt
}; };
self.tabs[i].last_cursor_world = effective.as_vec3(); self.tabs[i].last_cursor_world = effective;
self.tabs[i].last_cursor_screen = p_full; self.tabs[i].last_cursor_screen = p_full;
// Project the step anchor (an explicit `dyn_anchor` or the // Project the step anchor (an explicit `dyn_anchor` or the
// last point) so the dynamic-input overlay can place its // last point) so the dynamic-input overlay can place its
@ -1069,8 +1069,8 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
// origin (`tile_b`, the viewport rect inside a viewport) so // origin (`tile_b`, the viewport rect inside a viewport) so
// the DYN guide/labels share the canvas frame with // the DYN guide/labels share the canvas frame with
// `last_cursor_screen` (= p_full, canvas space). // `last_cursor_screen` (= p_full, canvas space).
let proj = |bp: glam::Vec3| { let proj = |bp: glam::DVec3| {
let ndc = view_rot.project_point3((bp.as_dvec3() - eye).as_vec3()); let ndc = view_rot.project_point3((bp - eye).as_vec3());
iced::Point::new( iced::Point::new(
(ndc.x + 1.0) * 0.5 * bounds.width + tile_b.x, (ndc.x + 1.0) * 0.5 * bounds.width + tile_b.x,
(1.0 - ndc.y) * 0.5 * bounds.height + tile_b.y, (1.0 - ndc.y) * 0.5 * bounds.height + tile_b.y,
@ -1096,7 +1096,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
.and_then(|c| c.window_first_corner()); .and_then(|c| c.window_first_corner());
self.tabs[i].scene.selection.borrow_mut().preview_box = self.tabs[i].scene.selection.borrow_mut().preview_box =
if is_window_corner { if is_window_corner {
window_first.map(|c1| (proj(c1.as_vec3()), p_full, true)) window_first.map(|c1| (proj(c1), p_full, true))
} else { } else {
None None
}; };
@ -1217,6 +1217,8 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
// the snapped angle direction, extending across the drawing. // the snapped angle direction, extending across the drawing.
if self.polar_mode && !needs_entity { if self.polar_mode && !needs_entity {
if let Some(base) = self.last_point { if let Some(base) = self.last_point {
// Screen-space dotted guide (render geometry is f32).
let base = base.as_vec3();
let effective = effective.as_vec3(); let effective = effective.as_vec3();
let dx = effective.x - base.x; let dx = effective.x - base.x;
let dy = effective.y - base.y; let dy = effective.y - base.y;
@ -1367,7 +1369,12 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
} else { } else {
None None
}; };
crate::ui::overlay::ucs_icon_hit(cam.view_proj_rte(bounds), bounds, (ux, uy, uz), os) crate::ui::overlay::ucs_icon_hit(
cam.view_proj_rte(bounds),
bounds,
(ux.as_vec3(), uy.as_vec3(), uz.as_vec3()),
os,
)
} }
/// Apply one frame of a UCS-icon grip drag: map the cursor onto the active /// Apply one frame of a UCS-icon grip drag: map the cursor onto the active
@ -1896,7 +1903,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
.snap_tangent_only(snap_cursor.as_vec3(), p, &all_wires[..], view_rot, eye, bounds) .snap_tangent_only(snap_cursor.as_vec3(), p, &all_wires[..], view_rot, eye, bounds)
} else { } else {
let (go, gr) = self.tabs[i].ucs_grid_basis(); let (go, gr) = self.tabs[i].ucs_grid_basis();
self.snapper.from_point = self.last_point; self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
self.snapper.snap(snap_cursor, p, &all_wires[..], view_rot, eye, bounds, go, gr) self.snapper.snap(snap_cursor, p, &all_wires[..], view_rot, eye, bounds, go, gr)
}; };
// Snap runs in model space; the result is already model. // Snap runs in model space; the result is already model.
@ -1916,7 +1923,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
}; };
let ucs = self.tabs[i].scene.viewcube_ucs_mat(); let ucs = self.tabs[i].scene.viewcube_ucs_mat();
self.snapper self.snapper
.otrack_snap(raw.as_vec3(), view_rot, eye, bounds, step, self.last_point, self.ortho_mode && !is_window_corner, ucs) .otrack_snap(raw.as_vec3(), view_rot, eye, bounds, step, self.last_point.map(|p| p.as_vec3()), self.ortho_mode && !is_window_corner, ucs)
} else { } else {
None None
}; };
@ -1934,10 +1941,10 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
if let Some(base) = self.last_point { if let Some(base) = self.last_point {
let ucs_xf = self.tabs[i].ucs_xform(); let ucs_xf = self.tabs[i].ucs_xform();
if self.ortho_mode { if self.ortho_mode {
pt = ortho_constrain(pt.as_vec3(), base, &ucs_xf).as_dvec3(); pt = ortho_constrain(pt, base, &ucs_xf);
} else if self.polar_mode { } else if self.polar_mode {
pt = polar_constrain_near( pt = polar_constrain_near(
pt.as_vec3(), pt,
base, base,
self.polar_increment_deg, self.polar_increment_deg,
view_rot, view_rot,
@ -1945,8 +1952,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
bounds, bounds,
self.snapper.osnap_radius_px, self.snapper.osnap_radius_px,
&ucs_xf, &ucs_xf,
) );
.as_dvec3();
} }
} }
} }
@ -2128,7 +2134,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
self.tabs[i].dyn_active = 0; self.tabs[i].dyn_active = 0;
} }
} }
self.last_point = Some(world_pt.as_vec3()); self.last_point = Some(world_pt);
self.dyn_user_reshaped = false; self.dyn_user_reshaped = false;
self.sync_dyn_fields(); self.sync_dyn_fields();
self.reset_tracking_after_point(); self.reset_tracking_after_point();
@ -2147,7 +2153,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
.as_ref() .as_ref()
.and_then(|c| c.resolved_anchor()) .and_then(|c| c.resolved_anchor())
{ {
self.last_point = Some(a.as_vec3()); self.last_point = Some(a);
} }
handled handled
} else { } else {

View file

@ -191,15 +191,15 @@ pub(super) fn viewport_controls<'a>(
/// world position (and the base point for polar quantities). /// world position (and the base point for polar quantities).
pub(super) fn dyn_component_value( pub(super) fn dyn_component_value(
f: &DynFieldEntry, f: &DynFieldEntry,
w: glam::Vec3, w: glam::DVec3,
base: Option<glam::Vec3>, base: Option<glam::DVec3>,
xf: &super::super::helpers::UcsXform, xf: &super::super::helpers::UcsXform,
comma_cartesian: bool, comma_cartesian: bool,
) -> String { ) -> String {
if let Some(b) = &f.buffer { if let Some(b) = &f.buffer {
return b.clone(); return b.clone();
} }
let b = base.unwrap_or(glam::Vec3::ZERO); let b = base.unwrap_or(glam::DVec3::ZERO);
// Relative deltas and the polar angle read in the active UCS plane. The // Relative deltas and the polar angle read in the active UCS plane. The
// delta is offset-invariant, so only the axis rotation matters (identity // delta is offset-invariant, so only the axis rotation matters (identity
// xf reproduces the world-frame deltas). // xf reproduces the world-frame deltas).

View file

@ -200,7 +200,7 @@ impl OpenCADStudio {
let (vw, vh) = tab.scene.selection.borrow().vp_size; let (vw, vh) = tab.scene.selection.borrow().vp_size;
let model_basis = { let model_basis = {
let (o, ux, uy, uz) = tab.ucs_xform().axes(); let (o, ux, uy, uz) = tab.ucs_xform().axes();
(o.as_dvec3(), (ux, uy, uz)) (o, (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()))
}; };
let grid: Vec<crate::ui::overlay::GridParams> = tab let grid: Vec<crate::ui::overlay::GridParams> = tab
.scene .scene
@ -212,7 +212,7 @@ impl OpenCADStudio {
Some(u) => { Some(u) => {
let (o, ux, uy, uz) = let (o, ux, uy, uz) =
super::helpers::UcsXform::from_ucs(&u).axes(); super::helpers::UcsXform::from_ucs(&u).axes();
(o.as_dvec3(), (ux, uy, uz)) (o, (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()))
} }
None => ( None => (
glam::DVec3::ZERO, glam::DVec3::ZERO,
@ -345,7 +345,7 @@ impl OpenCADStudio {
vec![crate::ui::overlay::UcsIconParams { vec![crate::ui::overlay::UcsIconParams {
view_proj: vp_cam.view_proj_rte(full), view_proj: vp_cam.view_proj_rte(full),
bounds: full, bounds: full,
axes: (ux, uy, uz), axes: (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()),
origin_screen, origin_screen,
hover: self.ucs_icon_hover, hover: self.ucs_icon_hover,
selected: self.ucs_icon_selected, selected: self.ucs_icon_selected,
@ -378,7 +378,7 @@ impl OpenCADStudio {
crate::ui::overlay::UcsIconParams { crate::ui::overlay::UcsIconParams {
view_proj: cam.view_proj_rte(b), view_proj: cam.view_proj_rte(b),
bounds: b, bounds: b,
axes: (ux, uy, uz), axes: (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()),
origin_screen, origin_screen,
hover: i == active && self.ucs_icon_hover, hover: i == active && self.ucs_icon_hover,
selected: i == active && self.ucs_icon_selected, selected: i == active && self.ucs_icon_selected,
@ -440,7 +440,7 @@ impl OpenCADStudio {
match (otrack_proj, self.otrack_active) { match (otrack_proj, self.otrack_active) {
(Some((view_rot, eye, ob)), Some((base, _dir))) => { (Some((view_rot, eye, ob)), Some((base, _dir))) => {
let b = ost_project(base, view_rot, eye, ob); let b = ost_project(base, view_rot, eye, ob);
let a = ost_project(tab.last_cursor_world, view_rot, eye, ob); let a = ost_project(tab.last_cursor_world.as_vec3(), view_rot, eye, ob);
(b.x.is_finite() && a.x.is_finite()).then_some((b, a)) (b.x.is_finite() && a.x.is_finite()).then_some((b, a))
} }
_ => None, _ => None,
@ -573,7 +573,7 @@ impl OpenCADStudio {
// A command may drive a typed scalar by mouse (e.g. a // A command may drive a typed scalar by mouse (e.g. a
// perpendicular distance to a picked object); show that live // perpendicular distance to a picked object); show that live
// value in the box until the user types over it. // value in the box until the user types over it.
let live = tab.active_cmd.as_ref().and_then(|c| c.dyn_live_value(w.as_dvec3())); let live = tab.active_cmd.as_ref().and_then(|c| c.dyn_live_value(w));
let boxes: Vec<crate::ui::overlay::DynBox> = tab let boxes: Vec<crate::ui::overlay::DynBox> = tab
.dyn_fields .dyn_fields
.iter() .iter()
@ -1304,7 +1304,7 @@ impl OpenCADStudio {
// drawing coordinates (paper space carries no offset), then // drawing coordinates (paper space carries no offset), then
// report it in the active UCS — the readout follows the // report it in the active UCS — the readout follows the
// user's coordinate system, not raw WCS (no-op without UCS). // user's coordinate system, not raw WCS (no-op without UCS).
let to_readout = |p: glam::Vec3| { let to_readout = |p: glam::DVec3| {
// The readout follows the active pane's UCS — model space // The readout follows the active pane's UCS — model space
// or inside a floating viewport (no-op without a UCS). // or inside a floating viewport (no-op without a UCS).
if tab.editing_model_space() { if tab.editing_model_space() {

View file

@ -3,7 +3,7 @@
use acadrust::entities::{Dimension, DimensionAligned}; use acadrust::entities::{Dimension, DimensionAligned};
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
@ -22,8 +22,8 @@ pub fn tool() -> ToolDef {
enum Step { enum Step {
First, First,
Second(Vec3), Second(DVec3),
DimLine { p1: Vec3, p2: Vec3 }, DimLine { p1: DVec3, p2: DVec3 },
} }
pub struct AlignedDimensionCommand { pub struct AlignedDimensionCommand {
@ -73,7 +73,7 @@ impl CadCommand for AlignedDimensionCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
Step::First => { Step::First => {
self.step = Step::Second(pt); self.step = Step::Second(pt);
@ -174,7 +174,7 @@ impl CadCommand for AlignedDimensionCommand {
} }
} }
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 (p1, p2) = match self.step { let (p1, p2) = match self.step {
Step::First => return None, Step::First => return None,
Step::Second(p1) => (p1, pt), Step::Second(p1) => (p1, pt),
@ -182,6 +182,9 @@ impl CadCommand for AlignedDimensionCommand {
return Some(preview_aligned(p1, p2, pt)); return Some(preview_aligned(p1, p2, pt));
} }
}; };
// Preview WireModel points are screen/GPU-side: downcast to f32.
let p1 = p1.as_vec3();
let p2 = p2.as_vec3();
Some(WireModel { Some(WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "dimaligned_preview".into(), name: "dimaligned_preview".into(),
@ -205,8 +208,8 @@ impl CadCommand for AlignedDimensionCommand {
} }
} }
fn v3(p: Vec3) -> Vector3 { fn v3(p: DVec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, p.z as f64) Vector3::new(p.x, p.y, p.z)
} }
/// Dimension-line endpoints: the baseline `p1`–`p2` shifted to pass through /// Dimension-line endpoints: the baseline `p1`–`p2` shifted to pass through
@ -214,16 +217,21 @@ fn v3(p: Vec3) -> Vector3 {
/// it matches the committed entity's renderer (and DIMLINEAR). The old /// it matches the committed entity's renderer (and DIMLINEAR). The old
/// preview used an XZ-plane perpendicular, drawing the offset in the wrong /// preview used an XZ-plane perpendicular, drawing the offset in the wrong
/// spatial direction. (#150) /// spatial direction. (#150)
fn dim_line_endpoints(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> (Vec3, Vec3) { fn dim_line_endpoints(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> (DVec3, DVec3) {
let axis = (p2 - p1).normalize_or_zero(); let axis = (p2 - p1).normalize_or_zero();
let perp = Vec3::new(-axis.y, axis.x, 0.0); let perp = DVec3::new(-axis.y, axis.x, 0.0);
let offset = (dim_pt - p1).dot(perp); let offset = (dim_pt - p1).dot(perp);
(p1 + perp * offset, p2 + perp * offset) (p1 + perp * offset, p2 + perp * offset)
} }
fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel { fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel {
// Show ext lines + dim line. // Show ext lines + dim line.
let (d1, d2) = dim_line_endpoints(p1, p2, dim_pt); let (d1, d2) = dim_line_endpoints(p1, p2, dim_pt);
// Preview WireModel points are screen/GPU-side: downcast to f32.
let p1 = p1.as_vec3();
let p2 = p2.as_vec3();
let d1 = d1.as_vec3();
let d2 = d2.as_vec3();
WireModel { WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "dimaligned_preview".into(), name: "dimaligned_preview".into(),

View file

@ -20,15 +20,15 @@ pub fn tool() -> ToolDef {
enum Step { enum Step {
Vertex, Vertex,
FirstRay(Vec3), FirstRay(DVec3),
SecondRay { SecondRay {
vertex: Vec3, vertex: DVec3,
first: Vec3, first: DVec3,
}, },
ArcPoint { ArcPoint {
vertex: Vec3, vertex: DVec3,
first: Vec3, first: DVec3,
second: Vec3, second: DVec3,
}, },
} }
@ -56,7 +56,7 @@ impl CadCommand for AngularDimensionCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
Step::Vertex => { Step::Vertex => {
self.step = Step::FirstRay(pt); self.step = Step::FirstRay(pt);
@ -98,28 +98,34 @@ impl CadCommand for AngularDimensionCommand {
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> {
let pt = pt.as_vec3();
match self.step { match self.step {
Step::Vertex => None, Step::Vertex => None,
Step::FirstRay(vertex) => Some(preview_wire(vec![vertex, pt])), Step::FirstRay(vertex) => Some(preview_wire(vec![vertex.as_vec3(), pt])),
Step::SecondRay { vertex, first } => Some(preview_wire(vec![ Step::SecondRay { vertex, first } => Some(preview_wire(vec![
vertex, vertex.as_vec3(),
first, first.as_vec3(),
Vec3::new(f32::NAN, f32::NAN, f32::NAN), Vec3::new(f32::NAN, f32::NAN, f32::NAN),
vertex, vertex.as_vec3(),
pt, pt,
])), ])),
Step::ArcPoint { Step::ArcPoint {
vertex, vertex,
first, first,
second, second,
} => Some(preview_wire(angular_preview(vertex, first, second, pt))), } => Some(preview_wire(angular_preview(
vertex.as_vec3(),
first.as_vec3(),
second.as_vec3(),
pt,
))),
} }
} }
} }
fn v3(pt: Vec3) -> Vector3 { fn v3(pt: DVec3) -> Vector3 {
Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64) Vector3::new(pt.x, pt.y, pt.z)
} }
fn preview_wire(points: Vec<Vec3>) -> WireModel { fn preview_wire(points: Vec<Vec3>) -> WireModel {

View file

@ -22,8 +22,8 @@ pub fn tool() -> ToolDef {
enum Step { enum Step {
CenterPoint, CenterPoint,
ArcPoint(Vec3), ArcPoint(DVec3),
TextPoint { center: Vec3, arc_pt: Vec3 }, TextPoint { center: DVec3, arc_pt: DVec3 },
} }
pub struct DiameterDimensionCommand { pub struct DiameterDimensionCommand {
@ -71,7 +71,7 @@ impl CadCommand for DiameterDimensionCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
Step::CenterPoint => { Step::CenterPoint => {
self.step = Step::ArcPoint(pt); self.step = Step::ArcPoint(pt);
@ -86,7 +86,7 @@ impl CadCommand for DiameterDimensionCommand {
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);
dim.base.insertion_point = v3(pt); dim.base.insertion_point = v3(pt);
dim.leader_length = arc_pt.distance(pt) as f64; dim.leader_length = arc_pt.distance(pt);
dim.base.actual_measurement = dim.measurement(); dim.base.actual_measurement = dim.measurement();
dim.base.user_text = self.text_override.clone(); dim.base.user_text = self.text_override.clone();
// An explicit text angle overrides the default rotation. // An explicit text angle overrides the default rotation.
@ -161,20 +161,21 @@ impl CadCommand for DiameterDimensionCommand {
} }
} }
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 pt = pt.as_vec3();
match self.step { match self.step {
Step::CenterPoint => None, Step::CenterPoint => None,
Step::ArcPoint(center) => Some(preview_line(center, pt)), Step::ArcPoint(center) => Some(preview_line(center.as_vec3(), pt)),
Step::TextPoint { center, arc_pt } => { Step::TextPoint { center, arc_pt } => {
let far = center + (center - arc_pt); // opposite point on circle let far = center + (center - arc_pt); // opposite point on circle
Some(preview_line(far, pt)) Some(preview_line(far.as_vec3(), pt))
} }
} }
} }
} }
fn v3(p: Vec3) -> Vector3 { fn v3(p: DVec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, p.z as f64) Vector3::new(p.x, p.y, p.z)
} }
fn preview_line(a: Vec3, b: Vec3) -> WireModel { fn preview_line(a: Vec3, b: Vec3) -> WireModel {

View file

@ -27,22 +27,22 @@ pub fn tool() -> ToolDef {
} }
/// Fallback stacking increment (world units) when no DimStyle is available. /// Fallback stacking increment (world units) when no DimStyle is available.
const DEFAULT_DIMDLI: f32 = 1.5; const DEFAULT_DIMDLI: f64 = 1.5;
pub struct DimBaselineCommand { pub struct DimBaselineCommand {
/// Fixed first-extension-line origin (never changes). /// Fixed first-extension-line origin (never changes).
base_p1: Vec3, base_p1: DVec3,
/// Direction along the measurement direction (0.0 = horizontal, PI/2 = vertical). /// Direction along the measurement direction (0.0 = horizontal, PI/2 = vertical).
rotation: f64, rotation: f64,
/// Text reading rotation inherited from the base dim (keeps a UCS-aligned /// Text reading rotation inherited from the base dim (keeps a UCS-aligned
/// chain's text consistent with the originating DIMLINEAR). 0 = natural. /// chain's text consistent with the originating DIMLINEAR). 0 = natural.
text_rotation: f64, text_rotation: f64,
/// Unit vector perpendicular to the dimension axis, pointing toward the dim line side. /// Unit vector perpendicular to the dimension axis, pointing toward the dim line side.
perp: Vec3, perp: DVec3,
/// Perpendicular distance of the NEXT dimension line from the extension-line axis. /// Perpendicular distance of the NEXT dimension line from the extension-line axis.
next_offset: f32, next_offset: f64,
/// Stacking increment from the active DimStyle (DIMDLI). /// Stacking increment from the active DimStyle (DIMDLI).
dimdli: f32, dimdli: f64,
/// True once we have a base dimension loaded. /// True once we have a base dimension loaded.
ready: bool, ready: bool,
} }
@ -51,10 +51,10 @@ impl DimBaselineCommand {
/// No base dim found — cancel immediately. /// No base dim found — cancel immediately.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
base_p1: Vec3::ZERO, base_p1: DVec3::ZERO,
rotation: 0.0, rotation: 0.0,
text_rotation: 0.0, text_rotation: 0.0,
perp: Vec3::Y, perp: DVec3::Y,
next_offset: DEFAULT_DIMDLI, next_offset: DEFAULT_DIMDLI,
dimdli: DEFAULT_DIMDLI, dimdli: DEFAULT_DIMDLI,
ready: false, ready: false,
@ -76,11 +76,16 @@ impl DimBaselineCommand {
text_rotation: f64, text_rotation: f64,
dimdli: f32, dimdli: f32,
) -> Self { ) -> Self {
// The base dim's points enter as f32 from the caller; promote to f64 so
// all committed-coordinate math downstream stays exact.
let p1 = p1.as_dvec3();
let definition_point = definition_point.as_dvec3();
// Measurement axis = the base dim's rotation angle (any angle, incl. a // Measurement axis = the base dim's rotation angle (any angle, incl. a
// UCS-aligned one), not a world horizontal/vertical. // UCS-aligned one), not a world horizontal/vertical.
let axis = Vec3::new(rotation.cos() as f32, rotation.sin() as f32, 0.0); let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0);
let perp = Vec3::new(-axis.y, axis.x, 0.0); let perp = DVec3::new(-axis.y, axis.x, 0.0);
let base_offset = (definition_point - p1).dot(perp); let base_offset = (definition_point - p1).dot(perp);
let dimdli = dimdli as f64;
let dimdli = if dimdli.abs() < 1e-6 { let dimdli = if dimdli.abs() < 1e-6 {
DEFAULT_DIMDLI DEFAULT_DIMDLI
} else { } else {
@ -110,7 +115,7 @@ impl DimBaselineCommand {
/// base origin and `p2` each projected INDEPENDENTLY onto the dim line (at /// base origin and `p2` each projected INDEPENDENTLY onto the dim line (at
/// the current stacking offset). Projecting both keeps the line straight /// the current stacking offset). Projecting both keeps the line straight
/// even when the two origins aren't level — a shared offset tilts it. (#181) /// even when the two origins aren't level — a shared offset tilts it. (#181)
fn dim_line_pts(&self, p2: Vec3) -> (Vec3, Vec3) { fn dim_line_pts(&self, p2: DVec3) -> (DVec3, DVec3) {
let target = self.base_p1.dot(self.perp) + self.next_offset; let target = self.base_p1.dot(self.perp) + self.next_offset;
let d1 = self.base_p1 + self.perp * (target - self.base_p1.dot(self.perp)); let d1 = self.base_p1 + self.perp * (target - self.base_p1.dot(self.perp));
let d2 = p2 + self.perp * (target - p2.dot(self.perp)); let d2 = p2 + self.perp * (target - p2.dot(self.perp));
@ -131,7 +136,7 @@ impl CadCommand for DimBaselineCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if !self.ready { if !self.ready {
return CmdResult::Cancel; return CmdResult::Cancel;
} }
@ -162,12 +167,17 @@ impl CadCommand for DimBaselineCommand {
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 !self.ready { if !self.ready {
return None; return None;
} }
let p1 = self.base_p1; // dim_line_pts stays in f64; downcast only here, into the preview's
// [f32;3] GPU buffer — correct pixel-space, never touches committed geometry.
let (dim_line_pt, dim_line_pt2) = self.dim_line_pts(pt); let (dim_line_pt, dim_line_pt2) = self.dim_line_pts(pt);
let p1 = self.base_p1.as_vec3();
let dim_line_pt = dim_line_pt.as_vec3();
let dim_line_pt2 = dim_line_pt2.as_vec3();
let pt = pt.as_vec3();
Some(WireModel { Some(WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "dimbase_preview".into(), name: "dimbase_preview".into(),
@ -200,8 +210,8 @@ impl CadCommand for DimBaselineCommand {
} }
} }
fn v3(p: Vec3) -> Vector3 { fn v3(p: DVec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, p.z as f64) Vector3::new(p.x, p.y, p.z)
} }

View file

@ -28,7 +28,7 @@ pub fn tool() -> ToolDef {
pub struct DimContinueCommand { pub struct DimContinueCommand {
/// Fixed first-extension-line origin for the current step (moves each iteration). /// Fixed first-extension-line origin for the current step (moves each iteration).
chain_p1: Vec3, chain_p1: DVec3,
/// Direction along the dimension axis (0.0 = horizontal, PI/2 = vertical). /// Direction along the dimension axis (0.0 = horizontal, PI/2 = vertical).
rotation: f64, rotation: f64,
/// Text reading rotation inherited from the base dim so a UCS-aligned chain /// Text reading rotation inherited from the base dim so a UCS-aligned chain
@ -39,9 +39,9 @@ pub struct DimContinueCommand {
/// coordinate so the whole chain stays collinear — even when the extension /// coordinate so the whole chain stays collinear — even when the extension
/// origins sit at different perpendicular positions (extension lines of /// origins sit at different perpendicular positions (extension lines of
/// different lengths). (#151) /// different lengths). (#151)
dim_line_perp: f32, dim_line_perp: f64,
/// Direction of "up" perpendicular to the dim axis (points toward the dim line). /// Direction of "up" perpendicular to the dim axis (points toward the dim line).
perp: Vec3, perp: DVec3,
/// True once we have a base dimension loaded. /// True once we have a base dimension loaded.
ready: bool, ready: bool,
} }
@ -50,11 +50,11 @@ impl DimContinueCommand {
/// No base dim found — will show an error prompt and cancel immediately. /// No base dim found — will show an error prompt and cancel immediately.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
chain_p1: Vec3::ZERO, chain_p1: DVec3::ZERO,
rotation: 0.0, rotation: 0.0,
text_rotation: 0.0, text_rotation: 0.0,
dim_line_perp: 0.0, dim_line_perp: 0.0,
perp: Vec3::Y, perp: DVec3::Y,
ready: false, ready: false,
} }
} }
@ -75,11 +75,16 @@ impl DimContinueCommand {
rotation: f64, rotation: f64,
text_rotation: f64, text_rotation: f64,
) -> Self { ) -> Self {
// Widen the base-dim points to f64 so the committed chain math runs in
// full precision. (The base points arrive as f32 from the entity, but
// every derived committed coordinate must stay f64 from here on.)
let p2 = p2.as_dvec3();
let definition_point = definition_point.as_dvec3();
// Axis unit vector along the measurement direction — the base dim's // Axis unit vector along the measurement direction — the base dim's
// rotation angle (any angle, incl. a UCS-aligned one), not a world H/V. // rotation angle (any angle, incl. a UCS-aligned one), not a world H/V.
let axis = Vec3::new(rotation.cos() as f32, rotation.sin() as f32, 0.0); let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0);
// Perpendicular unit vector toward the dim line. // Perpendicular unit vector toward the dim line.
let perp = Vec3::new(-axis.y, axis.x, 0.0); let perp = DVec3::new(-axis.y, axis.x, 0.0);
let dim_line_perp = definition_point.dot(perp); let dim_line_perp = definition_point.dot(perp);
Self { Self {
chain_p1: p2, chain_p1: p2,
@ -105,7 +110,7 @@ impl CadCommand for DimContinueCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if !self.ready { if !self.ready {
return CmdResult::Cancel; return CmdResult::Cancel;
} }
@ -141,13 +146,18 @@ impl CadCommand for DimContinueCommand {
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 !self.ready { if !self.ready {
return None; return None;
} }
let p1 = self.chain_p1; // Preview / rubber-band geometry feeds an f32 GPU buffer, so drop to f32
let dim_line_pt = p1 + self.perp * (self.dim_line_perp - p1.dot(self.perp)); // at this screen-only boundary.
let dim_line_pt2 = pt + self.perp * (self.dim_line_perp - pt.dot(self.perp)); let pt = pt.as_vec3();
let p1 = self.chain_p1.as_vec3();
let perp = self.perp.as_vec3();
let dim_line_perp = self.dim_line_perp as f32;
let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp));
let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp));
Some(WireModel { Some(WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "dimcont_preview".into(), name: "dimcont_preview".into(),
@ -180,8 +190,8 @@ impl CadCommand for DimContinueCommand {
} }
} }
fn v3(p: Vec3) -> Vector3 { fn v3(p: DVec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, p.z as f64) Vector3::new(p.x, p.y, p.z)
} }

View file

@ -63,7 +63,7 @@ impl CadCommand for DimJogLineCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::PickJogPos { handle } = &self.step { if let Step::PickJogPos { handle } = &self.step {
let h = *handle; let h = *handle;
// Emit sentinel for commands.rs to store the jog position // Emit sentinel for commands.rs to store the jog position

View file

@ -69,13 +69,12 @@ impl CadCommand for DimTeditCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::PickTextPos { handle, entity } = &mut self.step { if let Step::PickTextPos { handle, entity } = &mut self.step {
let h = *handle; let h = *handle;
if let Some(mut ent) = entity.take() { if let Some(mut ent) = entity.take() {
if let EntityType::Dimension(ref mut d) = ent { if let EntityType::Dimension(ref mut d) = ent {
let new_pt = let new_pt = acadrust::types::Vector3::new(pt.x, pt.y, pt.z);
acadrust::types::Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64);
d.base_mut().text_middle_point = new_pt; d.base_mut().text_middle_point = new_pt;
d.base_mut().insertion_point = new_pt; d.base_mut().insertion_point = new_pt;
// Pin the text to this location, else the renderer recomputes // Pin the text to this location, else the renderer recomputes

View file

@ -31,7 +31,7 @@ pub fn tool() -> ToolDef {
} }
pub struct LeaderCommand { pub struct LeaderCommand {
verts: Vec<Vec3>, verts: Vec<DVec3>,
ucs: Mat4, ucs: Mat4,
} }
@ -61,7 +61,7 @@ impl CadCommand for LeaderCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.verts.push(pt); self.verts.push(pt);
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -85,24 +85,24 @@ impl CadCommand for LeaderCommand {
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 self.verts.is_empty() { if self.verts.is_empty() {
return None; return None;
} }
let mut pts = self.verts.clone(); let mut pts: Vec<Vec3> = self.verts.iter().map(|p| p.as_vec3()).collect();
pts.push(pt); pts.push(pt.as_vec3());
Some(preview_wire(&pts)) Some(preview_wire(&pts))
} }
} }
// ── Helpers ───────────────────────────────────────────────────────────────── // ── Helpers ─────────────────────────────────────────────────────────────────
fn v3(p: Vec3) -> Vector3 { fn dv3(p: DVec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, p.z as f64) Vector3::new(p.x, p.y, p.z)
} }
fn build_leader(verts: &[Vec3], ucs: Mat4) -> Leader { fn build_leader(verts: &[DVec3], ucs: Mat4) -> Leader {
let mut l = Leader::from_vertices(verts.iter().map(|p| v3(*p)).collect()); let mut l = Leader::from_vertices(verts.iter().map(|p| dv3(*p)).collect());
l.creation_type = LeaderCreationType::WithText; l.creation_type = LeaderCreationType::WithText;
l.hookline_enabled = true; l.hookline_enabled = true;
// The hookline / text read along the UCS X axis (DXF "horizontal direction // The hookline / text read along the UCS X axis (DXF "horizontal direction
@ -116,17 +116,17 @@ fn build_leader(verts: &[Vec3], ucs: Mat4) -> Leader {
/// keeps the text reading away from the leader (text to the right of a /// keeps the text reading away from the leader (text to the right of a
/// left-pointing landing, to the left of a right-pointing one). /// left-pointing landing, to the left of a right-pointing one).
fn annotation_anchor( fn annotation_anchor(
verts: &[Vec3], verts: &[DVec3],
text_height: f64, text_height: f64,
ucs: Mat4, ucs: Mat4,
) -> (Vec3, AttachmentPoint) { ) -> (DVec3, AttachmentPoint) {
let last = *verts.last().unwrap(); let last = *verts.last().unwrap();
let prev = verts[verts.len() - 2]; let prev = verts[verts.len() - 2];
// Side + landing run along the UCS X axis (identity = world). // Side + landing run along the UCS X axis (identity = world).
let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X); let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X).as_dvec3();
let to_right = (last - prev).dot(ux) >= 0.0; let to_right = (last - prev).dot(ux) >= 0.0;
let sign = if to_right { 1.0_f32 } else { -1.0_f32 }; let sign = if to_right { 1.0_f64 } else { -1.0_f64 };
let anchor = last + ux * (sign * text_height as f32 * 1.5); let anchor = last + ux * (sign * text_height * 1.5);
let attach = if to_right { let attach = if to_right {
AttachmentPoint::MiddleLeft AttachmentPoint::MiddleLeft
} else { } else {
@ -137,14 +137,14 @@ fn annotation_anchor(
fn build_mtext( fn build_mtext(
text: &str, text: &str,
pos: Vec3, pos: DVec3,
height: f64, height: f64,
attach: AttachmentPoint, attach: AttachmentPoint,
ucs: Mat4, ucs: Mat4,
) -> MText { ) -> MText {
let mut m = MText::new(); let mut m = MText::new();
m.value = text.to_string(); m.value = text.to_string();
m.insertion_point = v3(pos); m.insertion_point = dv3(pos);
m.height = height; m.height = height;
m.attachment_point = attach; m.attachment_point = attach;
// Text reads along the UCS X axis. // Text reads along the UCS X axis.

View file

@ -10,10 +10,10 @@ use glam::{DVec3, Mat4, Vec3};
/// World-space measurement axis for a linear dimension between `first` and /// World-space measurement axis for a linear dimension between `first` and
/// `second`, chosen as the UCS X or Y axis (whichever the span is closer to). /// `second`, chosen as the UCS X or Y axis (whichever the span is closer to).
/// `ucs` is the UCS→wire affine; identity gives the world X/Y behaviour. /// `ucs` is the UCS→wire affine; identity gives the world X/Y behaviour.
fn measure_axis(first: Vec3, second: Vec3, ucs: Mat4) -> Vec3 { fn measure_axis(first: DVec3, second: DVec3, ucs: Mat4) -> DVec3 {
let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::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); let uy = ucs.transform_vector3(Vec3::Y).normalize_or(Vec3::Y).as_dvec3();
let du = ucs.inverse().transform_vector3(second - first); let du = ucs.inverse().transform_vector3((second - first).as_vec3());
if du.x.abs() >= du.y.abs() { if du.x.abs() >= du.y.abs() {
ux ux
} else { } else {
@ -34,8 +34,8 @@ pub fn tool() -> ToolDef {
enum Step { enum Step {
FirstPoint, FirstPoint,
SecondPoint(Vec3), SecondPoint(DVec3),
DimensionLine { first: Vec3, second: Vec3 }, DimensionLine { first: DVec3, second: DVec3 },
} }
pub struct LinearDimensionCommand { pub struct LinearDimensionCommand {
@ -91,7 +91,7 @@ impl CadCommand for LinearDimensionCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
Step::FirstPoint => { Step::FirstPoint => {
self.step = Step::SecondPoint(pt); self.step = Step::SecondPoint(pt);
@ -106,7 +106,7 @@ impl CadCommand for LinearDimensionCommand {
// Measure along the UCS axis the span is closest to; the DXF // Measure along the UCS axis the span is closest to; the DXF
// rotation is that axis's angle in world space. // rotation is that axis's angle in world space.
let axis = measure_axis(first, second, self.ucs); let axis = measure_axis(first, second, self.ucs);
dim.rotation = (axis.y as f64).atan2(axis.x as f64); dim.rotation = axis.y.atan2(axis.x);
// Bake the UCS X-axis angle as the text rotation so the // Bake the UCS X-axis angle as the text rotation so the
// measurement text reads horizontally in the UCS (i.e. square on // measurement text reads horizontally in the UCS (i.e. square on
// screen) regardless of the style's force-horizontal flags, // screen) regardless of the style's force-horizontal flags,
@ -200,7 +200,7 @@ impl CadCommand for LinearDimensionCommand {
} }
} }
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> {
match self.step { match self.step {
Step::FirstPoint => None, Step::FirstPoint => None,
Step::SecondPoint(first) => Some(preview_wire(vec![first, pt])), Step::SecondPoint(first) => Some(preview_wire(vec![first, pt])),
@ -212,15 +212,18 @@ impl CadCommand for LinearDimensionCommand {
} }
} }
fn v3(pt: Vec3) -> Vector3 { fn v3(pt: DVec3) -> Vector3 {
Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64) Vector3::new(pt.x, pt.y, pt.z)
} }
fn preview_wire(points: Vec<Vec3>) -> WireModel { fn preview_wire(points: Vec<DVec3>) -> WireModel {
WireModel { WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "dimlinear_preview".to_string(), name: "dimlinear_preview".to_string(),
points: points.into_iter().map(|p| [p.x, p.y, p.z]).collect(), points: points
.into_iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect(),
points_low: Vec::new(), points_low: Vec::new(),
color: WireModel::CYAN, color: WireModel::CYAN,
selected: false, selected: false,
@ -244,23 +247,23 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
/// single shared offset only lands both on the line when they are level, and /// single shared offset only lands both on the line when they are level, and
/// tilts the dimension line when they are not (e.g. measuring across sloped /// tilts the dimension line when they are not (e.g. measuring across sloped
/// points). See #181. /// points). See #181.
fn dim_line_endpoints(first: Vec3, second: Vec3, def: Vec3, axis: Vec3) -> (Vec3, Vec3) { fn dim_line_endpoints(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> (DVec3, DVec3) {
let perp = Vec3::new(-axis.y, axis.x, 0.0); let perp = DVec3::new(-axis.y, axis.x, 0.0);
let dperp = def.dot(perp); let dperp = def.dot(perp);
let d1 = first + perp * (dperp - first.dot(perp)); let d1 = first + perp * (dperp - first.dot(perp));
let d2 = second + perp * (dperp - second.dot(perp)); let d2 = second + perp * (dperp - second.dot(perp));
(d1, d2) (d1, d2)
} }
fn linear_dimension_preview(first: Vec3, second: Vec3, def: Vec3, axis: Vec3) -> Vec<Vec3> { fn linear_dimension_preview(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> Vec<DVec3> {
let (d1, d2) = dim_line_endpoints(first, second, def, axis); let (d1, d2) = dim_line_endpoints(first, second, def, axis);
let nan = Vec3::new(f32::NAN, f32::NAN, f32::NAN); let nan = DVec3::new(f64::NAN, f64::NAN, f64::NAN);
vec![first, d1, nan, second, d2, nan, d1, d2] vec![first, d1, nan, second, d2, nan, d1, d2]
} }
fn linear_text_pos(first: Vec3, second: Vec3, def: Vec3, axis: Vec3) -> Vec3 { fn linear_text_pos(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> DVec3 {
let (d1, d2) = dim_line_endpoints(first, second, def, axis); let (d1, d2) = dim_line_endpoints(first, second, def, axis);
let perp = Vec3::new(-axis.y, axis.x, 0.0); let perp = DVec3::new(-axis.y, axis.x, 0.0);
(d1 + d2) * 0.5 + perp * 0.15 (d1 + d2) * 0.5 + perp * 0.15
} }

View file

@ -26,7 +26,7 @@ pub fn tool() -> ToolDef {
} }
pub struct MLeaderCommand { pub struct MLeaderCommand {
verts: Vec<Vec3>, verts: Vec<DVec3>,
ucs: Mat4, ucs: Mat4,
} }
@ -56,7 +56,7 @@ impl CadCommand for MLeaderCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.verts.push(pt); self.verts.push(pt);
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -75,23 +75,24 @@ impl CadCommand for MLeaderCommand {
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 self.verts.is_empty() { if self.verts.is_empty() {
return None; return None;
} }
let mut pts = self.verts.clone(); // Preview / rubber-band is GPU screen-space: downcast to f32 here.
pts.push(pt); let mut pts: Vec<Vec3> = self.verts.iter().map(|p| p.as_vec3()).collect();
pts.push(pt.as_vec3());
Some(preview_wire(&pts)) Some(preview_wire(&pts))
} }
} }
// ── Helpers ───────────────────────────────────────────────────────────────── // ── Helpers ─────────────────────────────────────────────────────────────────
fn v3(p: Vec3) -> Vector3 { fn v3(p: DVec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, p.z as f64) Vector3::new(p.x, p.y, p.z)
} }
fn build_mleader(text: &str, verts: &[Vec3], ucs: Mat4) -> MultiLeader { fn build_mleader(text: &str, verts: &[DVec3], ucs: Mat4) -> MultiLeader {
// Last vertex = content/text location; remaining = leader line points // Last vertex = content/text location; remaining = leader line points
let (leader_pts, content_pt) = verts.split_at(verts.len() - 1); let (leader_pts, content_pt) = verts.split_at(verts.len() - 1);
let content_pt = content_pt[0]; let content_pt = content_pt[0];
@ -113,7 +114,7 @@ fn build_mleader(text: &str, verts: &[Vec3], ucs: Mat4) -> MultiLeader {
let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X); let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X);
// Which side of the leader the text sits on, measured along the UCS X axis. // Which side of the leader the text sits on, measured along the UCS X axis.
let last_leader = leader_pts.last().copied().unwrap_or(content_pt); let last_leader = leader_pts.last().copied().unwrap_or(content_pt);
let to_right = (content_pt - last_leader).dot(ux) >= 0.0; let to_right = (content_pt - last_leader).dot(ux.as_dvec3()) >= 0.0;
let sign = if to_right { 1.0 } else { -1.0 }; let sign = if to_right { 1.0 } else { -1.0 };
let landing = ux * (sign as f32); let landing = ux * (sign as f32);

View file

@ -37,7 +37,7 @@ enum AddStep {
CollectPoints { CollectPoints {
handle: Handle, handle: Handle,
entity: EntityType, entity: EntityType,
pts: Vec<Vec3>, pts: Vec<DVec3>,
}, },
} }
@ -84,7 +84,7 @@ impl CadCommand for MLeaderAddCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &mut self.step { match &mut self.step {
AddStep::PickArrowhead { handle, entity } => { AddStep::PickArrowhead { handle, entity } => {
if let Some(ent) = entity.take() { if let Some(ent) = entity.take() {
@ -121,7 +121,7 @@ impl CadCommand for MLeaderAddCommand {
// Add a new leader root with the collected points // Add a new leader root with the collected points
let points: Vec<Vector3> = pts let points: Vec<Vector3> = pts
.iter() .iter()
.map(|p| Vector3::new(p.x as f64, p.y as f64, p.z as f64)) .map(|p| Vector3::new(p.x, p.y, p.z))
.collect(); .collect();
let root = ml.context.add_leader_root(); let root = ml.context.add_leader_root();
root.create_line(points); root.create_line(points);
@ -132,14 +132,16 @@ impl CadCommand for MLeaderAddCommand {
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> {
let existing_pts = match &self.step { let existing_pts = match &self.step {
AddStep::CollectPoints { pts, .. } => pts.clone(), AddStep::CollectPoints { pts, .. } => pts.clone(),
_ => return None, _ => return None,
}; };
let mut all_pts = existing_pts; let mut all_pts = existing_pts;
all_pts.push(pt); all_pts.push(pt);
Some(preview_wire(&all_pts)) Some(preview_wire(
&all_pts.iter().map(|p| p.as_vec3()).collect::<Vec<_>>(),
))
} }
fn inject_picked_entity(&mut self, entity: EntityType) { fn inject_picked_entity(&mut self, entity: EntityType) {
@ -275,7 +277,7 @@ pub fn tool_align() -> ToolDef {
enum AlignStep { enum AlignStep {
Gathering, Gathering,
PickDirection { handles: Vec<Handle> }, PickDirection { handles: Vec<Handle> },
PickEndDir { handles: Vec<Handle>, from: Vec3 }, PickEndDir { handles: Vec<Handle>, from: DVec3 },
} }
pub struct MLeaderAlignCommand { pub struct MLeaderAlignCommand {
@ -321,7 +323,7 @@ impl CadCommand for MLeaderAlignCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &mut self.step { match &mut self.step {
AlignStep::PickDirection { handles } => { AlignStep::PickDirection { handles } => {
let h = handles.clone(); let h = handles.clone();
@ -423,7 +425,7 @@ impl CadCommand for MLeaderCollectCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let CollectStep::PickLocation { handles } = &self.step { if let CollectStep::PickLocation { handles } = &self.step {
let h = handles.clone(); let h = handles.clone();
// Build a new combined multileader at the collection point, delete originals. // Build a new combined multileader at the collection point, delete originals.

View file

@ -28,7 +28,7 @@ pub fn tool() -> ToolDef {
enum Step { enum Step {
FeaturePoint, FeaturePoint,
LeaderEndpoint { feature: Vec3 }, LeaderEndpoint { feature: DVec3 },
} }
pub struct OrdinateDimCommand { pub struct OrdinateDimCommand {
@ -55,7 +55,7 @@ impl CadCommand for OrdinateDimCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
Step::FeaturePoint => { Step::FeaturePoint => {
self.step = Step::LeaderEndpoint { feature: pt }; self.step = Step::LeaderEndpoint { feature: pt };
@ -80,25 +80,30 @@ impl CadCommand for OrdinateDimCommand {
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
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> {
let feature = match self.step { let feature = match self.step {
Step::LeaderEndpoint { feature } => feature, Step::LeaderEndpoint { feature } => feature,
_ => return None, _ => return None,
}; };
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);
Some(preview_wire(vec![feature, elbow, pt])) // Screen-only rubber band: downcast to f32 at the preview boundary.
Some(preview_wire(vec![
feature.as_vec3(),
elbow.as_vec3(),
pt.as_vec3(),
]))
} }
} }
fn v3(p: Vec3) -> Vector3 { fn v3(p: DVec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, p.z as f64) Vector3::new(p.x, p.y, p.z)
} }
/// X-datum (labels the feature's X coordinate) when the leader runs more /// X-datum (labels the feature's X coordinate) when the leader runs more
/// vertically than horizontally; Y-datum otherwise. Mirrors the placement /// vertically than horizontally; Y-datum otherwise. Mirrors the placement
/// decision so the preview and the committed entity agree. /// decision so the preview and the committed entity agree.
fn is_x_type(feature: Vec3, leader: Vec3) -> bool { fn is_x_type(feature: DVec3, leader: DVec3) -> bool {
let dx = (leader.x - feature.x).abs(); let dx = (leader.x - feature.x).abs();
let dy = (leader.y - feature.y).abs(); let dy = (leader.y - feature.y).abs();
dy >= dx dy >= dx
@ -106,11 +111,11 @@ fn is_x_type(feature: Vec3, leader: Vec3) -> bool {
/// Orthogonal elbow of the ordinate leader: an X-datum runs along Y from the /// Orthogonal elbow of the ordinate leader: an X-datum runs along Y from the
/// feature then jogs across in X; a Y-datum runs along X then jogs in Y. /// feature then jogs across in X; a Y-datum runs along X then jogs in Y.
fn ordinate_elbow(feature: Vec3, leader: Vec3, is_x: bool) -> Vec3 { fn ordinate_elbow(feature: DVec3, leader: DVec3, is_x: bool) -> DVec3 {
if is_x { if is_x {
Vec3::new(feature.x, leader.y, feature.z) DVec3::new(feature.x, leader.y, feature.z)
} else { } else {
Vec3::new(leader.x, feature.y, feature.z) DVec3::new(leader.x, feature.y, feature.z)
} }
} }

View file

@ -9,7 +9,7 @@
use acadrust::entities::{Dimension, DimensionLinear}; use acadrust::entities::{Dimension, DimensionLinear};
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::{EntityType, Handle}; use acadrust::{EntityType, Handle};
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
@ -67,9 +67,11 @@ impl CadCommand for QdimCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::PlaceLine { handles } = &self.step { if let Step::PlaceLine { handles } = &self.step {
CmdResult::Relaunch( CmdResult::Relaunch(
// Keep the picked placement point in full f64 precision so the
// committed dimension coordinates are not quantized to f32.
format!("QDIM_PLACE {:.6} {:.6} {:.6}", pt.x, pt.y, pt.z), format!("QDIM_PLACE {:.6} {:.6} {:.6}", pt.x, pt.y, pt.z),
handles.clone(), handles.clone(),
) )
@ -112,8 +114,8 @@ impl CadCommand for QdimCommand {
/// Build a linear dimension between two points at the given dimension-line position. /// Build a linear dimension between two points at the given dimension-line position.
#[allow(dead_code)] #[allow(dead_code)]
pub fn make_linear_dim(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> EntityType { pub fn make_linear_dim(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> EntityType {
let v = |p: Vec3| Vector3::new(p.x as f64, p.y as f64, p.z as f64); let v = |p: DVec3| Vector3::new(p.x, p.y, p.z);
let mut dim = DimensionLinear::new(v(p1), v(p2)); let mut dim = DimensionLinear::new(v(p1), v(p2));
// Determine axis: horizontal if Δx > Δz, else vertical // Determine axis: horizontal if Δx > Δz, else vertical
let dx = (p2.x - p1.x).abs(); let dx = (p2.x - p1.x).abs();

View file

@ -20,8 +20,8 @@ pub fn tool() -> ToolDef {
enum Step { enum Step {
CenterPoint, CenterPoint,
RadiusPoint(Vec3), RadiusPoint(DVec3),
TextPoint { center: Vec3, point: Vec3 }, TextPoint { center: DVec3, point: DVec3 },
} }
pub struct RadiusDimensionCommand { pub struct RadiusDimensionCommand {
@ -69,7 +69,7 @@ impl CadCommand for RadiusDimensionCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
Step::CenterPoint => { Step::CenterPoint => {
self.step = Step::RadiusPoint(pt); self.step = Step::RadiusPoint(pt);
@ -84,7 +84,7 @@ impl CadCommand for RadiusDimensionCommand {
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);
dim.base.insertion_point = v3(pt); dim.base.insertion_point = v3(pt);
dim.leader_length = point.distance(pt) as f64; dim.leader_length = point.distance(pt);
dim.base.actual_measurement = dim.measurement(); dim.base.actual_measurement = dim.measurement();
dim.base.user_text = self.text_override.clone(); dim.base.user_text = self.text_override.clone();
// An explicit text angle overrides the default rotation. // An explicit text angle overrides the default rotation.
@ -163,23 +163,24 @@ impl CadCommand for RadiusDimensionCommand {
} }
} }
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 pt = pt.as_vec3();
match self.step { match self.step {
Step::CenterPoint => None, Step::CenterPoint => None,
Step::RadiusPoint(center) => Some(preview_wire(vec![center, pt])), Step::RadiusPoint(center) => Some(preview_wire(vec![center.as_vec3(), pt])),
Step::TextPoint { center, point } => Some(preview_wire(vec![ Step::TextPoint { center, point } => Some(preview_wire(vec![
center, center.as_vec3(),
point, point.as_vec3(),
Vec3::new(f32::NAN, f32::NAN, f32::NAN), Vec3::new(f32::NAN, f32::NAN, f32::NAN),
point, point.as_vec3(),
pt, pt,
])), ])),
} }
} }
} }
fn v3(pt: Vec3) -> Vector3 { fn v3(pt: DVec3) -> Vector3 {
Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64) Vector3::new(pt.x, pt.y, pt.z)
} }
fn preview_wire(points: Vec<Vec3>) -> WireModel { fn preview_wire(points: Vec<Vec3>) -> WireModel {

View file

@ -116,9 +116,9 @@ impl CadCommand for TableCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); 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 as f64, pt.y as f64, pt.z as f64); let ins = Vector3::new(pt.x, pt.y, pt.z);
let table = TableBuilder::new(rows, cols) let table = TableBuilder::new(rows, cols)
.at(ins) .at(ins)
.row_height(ROW_HEIGHT) .row_height(ROW_HEIGHT)

View file

@ -64,9 +64,9 @@ impl CadCommand for ToleranceCommand {
Some(CmdResult::NeedPoint) Some(CmdResult::NeedPoint)
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); 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 as f64, pt.y as f64, pt.z as f64); let ins = Vector3::new(pt.x, pt.y, pt.z);
let tol = Tolerance::with_text(ins, text.clone()); let tol = Tolerance::with_text(ins, text.clone());
CmdResult::CommitAndExit(EntityType::Tolerance(tol)) CmdResult::CommitAndExit(EntityType::Tolerance(tol))
} else { } else {

View file

@ -106,7 +106,7 @@ impl CadCommand for AttdefCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::Insertion { if let Step::Insertion {
tag, tag,
prompt, prompt,
@ -117,7 +117,7 @@ impl CadCommand for AttdefCommand {
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 as f64, pt.y as f64, pt.z as f64), insertion_point: Vector3::new(pt.x, pt.y, pt.z),
height: self.height, height: self.height,
..Default::default() ..Default::default()
}; };

View file

@ -8,13 +8,13 @@
use acadrust::entities::MLine; use acadrust::entities::MLine;
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
pub struct MlineCommand { pub struct MlineCommand {
points: Vec<Vec3>, points: Vec<DVec3>,
scale: f64, scale: f64,
waiting_scale: bool, waiting_scale: bool,
style_name: String, style_name: String,
@ -111,7 +111,7 @@ impl CadCommand for MlineCommand {
None None
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.points.push(pt); self.points.push(pt);
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -124,11 +124,16 @@ impl CadCommand for MlineCommand {
CmdResult::CommitAndExit(entity) CmdResult::CommitAndExit(entity)
} }
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 pt = pt.as_vec3();
if self.points.is_empty() { if self.points.is_empty() {
return None; return None;
} }
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| [p.x, p.y, p.z]).collect(); let mut pts: Vec<[f32; 3]> = self
.points
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect();
pts.push([pt.x, pt.y, pt.z]); pts.push([pt.x, pt.y, pt.z]);
Some(WireModel { Some(WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
@ -153,10 +158,10 @@ impl CadCommand for MlineCommand {
} }
} }
fn build_mline(pts: &[Vec3], scale: f64, closed: bool, style_name: &str) -> EntityType { fn build_mline(pts: &[DVec3], scale: f64, closed: bool, style_name: &str) -> EntityType {
let verts: Vec<Vector3> = pts let verts: Vec<Vector3> = pts
.iter() .iter()
.map(|p| Vector3::new(p.x as f64, p.y as f64, p.z as f64)) .map(|p| Vector3::new(p.x, p.y, p.z))
.collect(); .collect();
let mut mline = if closed { let mut mline = if closed {
MLine::closed_from_points(&verts) MLine::closed_from_points(&verts)

View file

@ -12,7 +12,7 @@
use acadrust::entities::LwVertex; use acadrust::entities::LwVertex;
use acadrust::types::Vector2; use acadrust::types::Vector2;
use acadrust::{EntityType, Handle, LwPolyline}; use acadrust::{EntityType, Handle, LwPolyline};
use glam::{DVec3, Vec2, Vec3}; use glam::{DVec2, DVec3, Vec2, Vec3};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
@ -40,7 +40,7 @@ enum SegMode {
// ── Command implementation ───────────────────────────────────────────────── // ── Command implementation ─────────────────────────────────────────────────
pub struct PlineCommand { pub struct PlineCommand {
vertices: Vec<Vec3>, vertices: Vec<DVec3>,
/// Bulge for segment i → i+1 (one entry per vertex; last entry unused on commit). /// Bulge for segment i → i+1 (one entry per vertex; last entry unused on commit).
bulges: Vec<f64>, bulges: Vec<f64>,
mode: SegMode, mode: SegMode,
@ -90,7 +90,7 @@ impl PlineCommand {
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, v)| { .map(|(i, v)| {
let mut lv = LwVertex::new(Vector2::new(v.x as f64, v.y as f64)); let mut lv = LwVertex::new(Vector2::new(v.x, v.y));
lv.bulge = self.bulges.get(i).copied().unwrap_or(0.0); lv.bulge = self.bulges.get(i).copied().unwrap_or(0.0);
lv lv
}) })
@ -108,14 +108,14 @@ impl PlineCommand {
/// Compute the bulge for the arc from `a` to `b` that is tangent to `tangent` at `a`. /// Compute the bulge for the arc from `a` to `b` that is tangent to `tangent` at `a`.
/// Returns 0.0 if the points are coincident or the tangent is parallel to the chord. /// Returns 0.0 if the points are coincident or the tangent is parallel to the chord.
fn compute_bulge(a: Vec2, tangent: Vec2, b: Vec2) -> f64 { fn compute_bulge(a: DVec2, tangent: DVec2, b: DVec2) -> f64 {
let d = b - a; let d = b - a;
let len_sq = d.length_squared(); let len_sq = d.length_squared();
if len_sq < 1e-10 { if len_sq < 1e-10 {
return 0.0; return 0.0;
} }
// Perpendicular to tangent (CCW) — this is the direction to the arc center. // Perpendicular to tangent (CCW) — this is the direction to the arc center.
let perp = Vec2::new(-tangent.y, tangent.x); let perp = DVec2::new(-tangent.y, tangent.x);
let dot = d.dot(perp); let dot = d.dot(perp);
if dot.abs() < 1e-10 { if dot.abs() < 1e-10 {
// Tangent is perpendicular to chord → straight line (bulge = 0). // Tangent is perpendicular to chord → straight line (bulge = 0).
@ -133,15 +133,15 @@ fn compute_bulge(a: Vec2, tangent: Vec2, b: Vec2) -> f64 {
if t > 0.0 { if t > 0.0 {
// CCW arc: ensure arc_angle is in (0, 2π]. // CCW arc: ensure arc_angle is in (0, 2π].
if arc_angle <= 0.0 { if arc_angle <= 0.0 {
arc_angle += std::f32::consts::TAU; arc_angle += std::f64::consts::TAU;
} }
} else { } else {
// CW arc: ensure arc_angle is in [-2π, 0). // CW arc: ensure arc_angle is in [-2π, 0).
if arc_angle >= 0.0 { if arc_angle >= 0.0 {
arc_angle -= std::f32::consts::TAU; arc_angle -= std::f64::consts::TAU;
} }
} }
(arc_angle as f64 / 4.0).tan() (arc_angle / 4.0).tan()
} }
/// Update `tangent` after an arc segment described by `bulge` from `a` to `b`. /// Update `tangent` after an arc segment described by `bulge` from `a` to `b`.
@ -240,26 +240,28 @@ impl CadCommand for PlineCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); 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_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 = Vec2::new(pt.x - last.x, pt.y - last.y); let d = DVec2::new(pt.x - last.x, pt.y - last.y);
if d.length_squared() > 1e-10 { if d.length_squared() > 1e-10 {
self.last_tangent = Some(d.normalize()); // Direction only — f32 is sufficient for tangent continuity.
self.last_tangent = Some(d.normalize().as_vec2());
} }
0.0 0.0
} }
SegMode::Arc => { SegMode::Arc => {
let a = Vec2::new(last.x, last.y); let a = DVec2::new(last.x, last.y);
let b = Vec2::new(pt.x, pt.y); let b = DVec2::new(pt.x, pt.y);
let tangent = self.last_tangent.unwrap_or_else(|| { let tangent = self
.last_tangent
.map(|t| t.as_dvec2())
// No previous tangent: default to pointing right (arbitrary). // No previous tangent: default to pointing right (arbitrary).
Vec2::new(1.0, 0.0) .unwrap_or(DVec2::new(1.0, 0.0));
});
let bulge = compute_bulge(a, tangent, b); let bulge = compute_bulge(a, tangent, b);
update_tangent_after_arc(&mut self.last_tangent, bulge); update_tangent_after_arc(&mut self.last_tangent, bulge);
bulge bulge
@ -318,11 +320,12 @@ impl CadCommand for PlineCommand {
} }
} }
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 pt = pt.as_vec3();
// 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) // cursor. (#119) Downcast to f32 for the pixel-space rubber band.
let last = *self.vertices.last()?; let last = self.vertices.last()?.as_vec3();
let mut pts: Vec<[f32; 3]> = Vec::new(); let mut pts: Vec<[f32; 3]> = Vec::new();
match self.mode { match self.mode {
@ -331,9 +334,12 @@ impl CadCommand for PlineCommand {
pts.push([pt.x, pt.y, pt.z]); pts.push([pt.x, pt.y, pt.z]);
} }
SegMode::Arc => { SegMode::Arc => {
let a = Vec2::new(last.x, last.y); let a = DVec2::new(last.x as f64, last.y as f64);
let b = Vec2::new(pt.x, pt.y); let b = DVec2::new(pt.x as f64, pt.y as f64);
let tangent = self.last_tangent.unwrap_or(Vec2::new(1.0, 0.0)); let tangent = self
.last_tangent
.map(|t| t.as_dvec2())
.unwrap_or(DVec2::new(1.0, 0.0));
let bulge = compute_bulge(a, tangent, b); let bulge = compute_bulge(a, tangent, b);
let arc_pts = arc_sample_points(last, bulge, pt, 16); let arc_pts = arc_sample_points(last, bulge, pt, 16);
pts.extend_from_slice(&arc_pts); pts.extend_from_slice(&arc_pts);

View file

@ -9,7 +9,7 @@
use acadrust::entities::RasterImage; use acadrust::entities::RasterImage;
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -18,7 +18,7 @@ pub struct ImageCommand {
file_path: String, file_path: String,
pixel_width: u32, pixel_width: u32,
pixel_height: u32, pixel_height: u32,
origin: Option<Vec3>, origin: Option<DVec3>,
} }
impl ImageCommand { impl ImageCommand {
@ -39,11 +39,11 @@ impl ImageCommand {
} }
} }
fn make_entity(&self, origin: Vec3, width_pt: Vec3) -> EntityType { fn make_entity(&self, origin: DVec3, width_pt: DVec3) -> EntityType {
let world_width = ((width_pt.x - origin.x) as f64).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();
let ins = Vector3::new(origin.x as f64, origin.y as f64, origin.z as f64); let ins = Vector3::new(origin.x, origin.y, origin.z);
let mut img = RasterImage::with_size( let mut img = RasterImage::with_size(
&self.file_path, &self.file_path,
@ -75,7 +75,7 @@ impl CadCommand for ImageCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Some(origin) = self.origin { if let Some(origin) = self.origin {
let entity = self.make_entity(origin, pt); let entity = self.make_entity(origin, pt);
CmdResult::CommitAndExit(entity) CmdResult::CommitAndExit(entity)
@ -89,7 +89,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 = Vec3::new(origin.x + default_w as f32, origin.y, origin.z); let width_pt = DVec3::new(origin.x + default_w, origin.y, origin.z);
let entity = self.make_entity(origin, width_pt); let entity = self.make_entity(origin, width_pt);
CmdResult::CommitAndExit(entity) CmdResult::CommitAndExit(entity)
} else { } else {
@ -98,7 +98,7 @@ 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 pt = pt.as_vec3();
let origin = self.origin?; let origin = self.origin?.as_vec3();
let world_width = (pt.x - origin.x).abs().max(0.001); let world_width = (pt.x - origin.x).abs().max(0.001);
let world_height = world_width / self.aspect() as f32; let world_height = world_width / self.aspect() as f32;

View file

@ -10,14 +10,14 @@ use acadrust::EntityType;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Vec3}; use glam::DVec3;
const DISPLAY_EXTENT: f32 = 1_000_000.0; const DISPLAY_EXTENT: f32 = 1_000_000.0;
// ── RAY ─────────────────────────────────────────────────────────────────── // ── RAY ───────────────────────────────────────────────────────────────────
pub struct RayCommand { pub struct RayCommand {
base: Option<Vec3>, base: Option<DVec3>,
} }
impl RayCommand { impl RayCommand {
@ -48,7 +48,7 @@ impl CadCommand for RayCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Some(base) = self.base { if let Some(base) = self.base {
let dir = pt - base; let dir = pt - base;
let len = dir.length(); let len = dir.length();
@ -57,8 +57,8 @@ impl CadCommand for RayCommand {
} }
let dir_n = dir / len; let dir_n = dir / len;
let ray = RayEnt::new( let ray = RayEnt::new(
Vector3::new(base.x as f64, base.y as f64, base.z as f64), Vector3::new(base.x, base.y, base.z),
Vector3::new(dir_n.x as f64, dir_n.y as f64, dir_n.z as f64), Vector3::new(dir_n.x, dir_n.y, dir_n.z),
); );
// Stay active: new base = same base (can keep clicking through points) // Stay active: new base = same base (can keep clicking through points)
// Actually AutoCAD prompts for new start after each ray — reset base. // Actually AutoCAD prompts for new start after each ray — reset base.
@ -78,8 +78,9 @@ impl CadCommand for RayCommand {
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> {
let base = self.base?; let pt = pt.as_vec3();
let base = self.base?.as_vec3();
let dir = (pt - base).normalize_or_zero(); let dir = (pt - base).normalize_or_zero();
let far = base + dir * DISPLAY_EXTENT; let far = base + dir * DISPLAY_EXTENT;
Some(WireModel { Some(WireModel {
@ -108,7 +109,7 @@ impl CadCommand for RayCommand {
// ── XLINE ───────────────────────────────────────────────────────────────── // ── XLINE ─────────────────────────────────────────────────────────────────
pub struct XLineCommand { pub struct XLineCommand {
base: Option<Vec3>, base: Option<DVec3>,
} }
impl XLineCommand { impl XLineCommand {
@ -139,7 +140,7 @@ impl CadCommand for XLineCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Some(base) = self.base { if let Some(base) = self.base {
let dir = pt - base; let dir = pt - base;
let len = dir.length(); let len = dir.length();
@ -148,8 +149,8 @@ impl CadCommand for XLineCommand {
} }
let dir_n = dir / len; let dir_n = dir / len;
let xline = XLineEnt::new( let xline = XLineEnt::new(
Vector3::new(base.x as f64, base.y as f64, base.z as f64), Vector3::new(base.x, base.y, base.z),
Vector3::new(dir_n.x as f64, dir_n.y as f64, dir_n.z as f64), Vector3::new(dir_n.x, dir_n.y, dir_n.z),
); );
self.base = None; self.base = None;
CmdResult::CommitEntity(EntityType::XLine(xline)) CmdResult::CommitEntity(EntityType::XLine(xline))
@ -167,8 +168,9 @@ impl CadCommand for XLineCommand {
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> {
let base = self.base?; let pt = pt.as_vec3();
let base = self.base?.as_vec3();
let dir = (pt - base).normalize_or_zero(); let dir = (pt - base).normalize_or_zero();
let far_pos = base + dir * DISPLAY_EXTENT; let far_pos = base + dir * DISPLAY_EXTENT;
let far_neg = base - dir * DISPLAY_EXTENT; let far_neg = base - dir * DISPLAY_EXTENT;

View file

@ -7,7 +7,7 @@
use acadrust::entities::LwPolyline; use acadrust::entities::LwPolyline;
use acadrust::types::Vector2; use acadrust::types::Vector2;
use acadrust::{entities::LwVertex, EntityType}; use acadrust::{entities::LwVertex, EntityType};
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
@ -27,7 +27,7 @@ pub fn tool() -> ToolDef {
const DEFAULT_ARC_LEN: f64 = 1.0; // default arc bump length const DEFAULT_ARC_LEN: f64 = 1.0; // default arc bump length
pub struct RevCloudCommand { pub struct RevCloudCommand {
points: Vec<Vec3>, points: Vec<DVec3>,
arc_length: f64, arc_length: f64,
} }
@ -59,7 +59,7 @@ impl CadCommand for RevCloudCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.points.push(pt); self.points.push(pt);
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -72,13 +72,22 @@ impl CadCommand for RevCloudCommand {
CmdResult::CommitAndExit(entity) CmdResult::CommitAndExit(entity)
} }
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 pt = pt.as_vec3();
if self.points.is_empty() { if self.points.is_empty() {
return None; return None;
} }
let mut preview_pts: Vec<[f32; 3]> = self.points.iter().map(|p| [p.x, p.y, p.z]).collect(); let mut preview_pts: Vec<[f32; 3]> = self
.points
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect();
preview_pts.push([pt.x, pt.y, pt.z]); preview_pts.push([pt.x, pt.y, pt.z]);
preview_pts.push([self.points[0].x, self.points[0].y, self.points[0].z]); preview_pts.push([
self.points[0].x as f32,
self.points[0].y as f32,
self.points[0].z as f32,
]);
Some(WireModel { Some(WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "revcloud_preview".into(), name: "revcloud_preview".into(),
@ -102,7 +111,7 @@ impl CadCommand for RevCloudCommand {
} }
} }
fn make_revcloud(pts: &[Vec3], arc_len: f64) -> EntityType { fn make_revcloud(pts: &[DVec3], arc_len: f64) -> EntityType {
let n = pts.len(); let n = pts.len();
let mut vertices: Vec<LwVertex> = Vec::new(); let mut vertices: Vec<LwVertex> = Vec::new();
@ -112,19 +121,19 @@ fn make_revcloud(pts: &[Vec3], arc_len: f64) -> EntityType {
for i in 0..n { for i in 0..n {
let p0 = pts[i]; let p0 = pts[i];
let p1 = pts[(i + 1) % n]; let p1 = pts[(i + 1) % n];
let seg_len = ((p1.x - p0.x).powi(2) + (p1.y - p0.y).powi(2)).sqrt() as f64; let seg_len = ((p1.x - p0.x).powi(2) + (p1.y - p0.y).powi(2)).sqrt();
if seg_len < 1e-6 { if seg_len < 1e-6 {
continue; continue;
} }
// How many full arcs fit? // How many full arcs fit?
let num_arcs = ((seg_len / arc_len).round() as usize).max(1); let num_arcs = ((seg_len / arc_len).round() as usize).max(1);
let step_x = (p1.x - p0.x) as f64 / num_arcs as f64; let step_x = (p1.x - p0.x) / num_arcs as f64;
let step_y = (p1.y - p0.y) as f64 / num_arcs as f64; let step_y = (p1.y - p0.y) / num_arcs as f64;
for j in 0..num_arcs { for j in 0..num_arcs {
let x = p0.x as f64 + step_x * j as f64; let x = p0.x + step_x * j as f64;
let y = p0.y as f64 + step_y * j as f64; // DXF Y let y = p0.y + step_y * j as f64; // DXF Y
let mut v = LwVertex::new(Vector2::new(x, y)); let mut v = LwVertex::new(Vector2::new(x, y));
v.bulge = bump_bulge; v.bulge = bump_bulge;
vertices.push(v); vertices.push(v);

View file

@ -9,7 +9,7 @@ use acadrust::{EntityType, Spline};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
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;
use truck_modeling::base::{BoundedCurve, ParametricCurve}; use truck_modeling::base::{BoundedCurve, ParametricCurve};
use truck_modeling::{BSplineCurve, KnotVec, Point3}; use truck_modeling::{BSplineCurve, KnotVec, Point3};
@ -24,7 +24,7 @@ pub fn tool() -> ToolDef {
} }
pub struct SplineCommand { pub struct SplineCommand {
pts: Vec<Vec3>, pts: Vec<DVec3>,
} }
impl SplineCommand { impl SplineCommand {
@ -39,7 +39,7 @@ impl SplineCommand {
let control_points = self let control_points = self
.pts .pts
.iter() .iter()
.map(|p| Vector3::new(p.x as f64, p.y as f64, p.z as f64)) .map(|p| Vector3::new(p.x, p.y, p.z))
.collect(); .collect();
let n = self.pts.len(); let n = self.pts.len();
// Uniform open knot vector for degree-3 clamped B-spline // Uniform open knot vector for degree-3 clamped B-spline
@ -77,15 +77,18 @@ fn uniform_knots(n: usize, d: usize) -> Vec<f64> {
/// built with the same control points + knot vector `build()` commits so the /// built with the same control points + knot vector `build()` commits so the
/// preview traces the exact final curve — not the straight control polygon. /// preview traces the exact final curve — not the straight control polygon.
/// Fewer than 2 points has no curve; the points are returned as-is. /// Fewer than 2 points has no curve; the points are returned as-is.
fn sample_curve(pts: &[Vec3]) -> Vec<[f32; 3]> { fn sample_curve(pts: &[DVec3]) -> Vec<[f32; 3]> {
let n = pts.len(); let n = pts.len();
if n < 2 { if n < 2 {
return pts.iter().map(|p| [p.x, p.y, p.z]).collect(); return pts
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect();
} }
let degree = 3_usize.min(n - 1); let degree = 3_usize.min(n - 1);
let ctrl: Vec<Point3> = pts let ctrl: Vec<Point3> = pts
.iter() .iter()
.map(|p| Point3::new(p.x as f64, p.y as f64, p.z as f64)) .map(|p| Point3::new(p.x, p.y, p.z))
.collect(); .collect();
let curve = BSplineCurve::new(KnotVec::from(uniform_knots(n, degree)), ctrl); let curve = BSplineCurve::new(KnotVec::from(uniform_knots(n, degree)), ctrl);
let (t0, t1) = curve.range_tuple(); let (t0, t1) = curve.range_tuple();
@ -125,7 +128,7 @@ impl CadCommand for SplineCommand {
opts opts
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.pts.push(pt); self.pts.push(pt);
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -170,7 +173,7 @@ impl CadCommand for SplineCommand {
} }
} }
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 self.pts.is_empty() { if self.pts.is_empty() {
return None; return None;
} }

View file

@ -7,7 +7,7 @@
use acadrust::entities::Wipeout; use acadrust::entities::Wipeout;
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
@ -26,8 +26,8 @@ pub fn tool() -> ToolDef {
pub struct WipeoutCommand { pub struct WipeoutCommand {
mode: WipeoutMode, mode: WipeoutMode,
first: Option<Vec3>, first: Option<DVec3>,
poly_pts: Vec<Vec3>, poly_pts: Vec<DVec3>,
} }
enum WipeoutMode { enum WipeoutMode {
@ -79,7 +79,7 @@ impl CadCommand for WipeoutCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.mode { match &self.mode {
WipeoutMode::Rectangular => { WipeoutMode::Rectangular => {
if let Some(p1) = self.first { if let Some(p1) = self.first {
@ -110,7 +110,7 @@ impl CadCommand for WipeoutCommand {
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 pt = pt.as_vec3();
match &self.mode { match &self.mode {
WipeoutMode::Rectangular => { WipeoutMode::Rectangular => {
let p1 = self.first?; let p1 = self.first?.as_vec3();
let min = p1.min(pt); let min = p1.min(pt);
let max = p1.max(pt); let max = p1.max(pt);
Some(WireModel { Some(WireModel {
@ -144,10 +144,17 @@ impl CadCommand for WipeoutCommand {
if self.poly_pts.is_empty() { if self.poly_pts.is_empty() {
return None; return None;
} }
let mut pts: Vec<[f32; 3]> = let mut pts: Vec<[f32; 3]> = self
self.poly_pts.iter().map(|p| [p.x, p.y, p.z]).collect(); .poly_pts
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect();
pts.push([pt.x, pt.y, pt.z]); pts.push([pt.x, pt.y, pt.z]);
pts.push([self.poly_pts[0].x, self.poly_pts[0].y, self.poly_pts[0].z]); pts.push([
self.poly_pts[0].x as f32,
self.poly_pts[0].y as f32,
self.poly_pts[0].z as f32,
]);
Some(WireModel { Some(WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "wipeout_poly_preview".into(), name: "wipeout_poly_preview".into(),
@ -173,20 +180,17 @@ impl CadCommand for WipeoutCommand {
} }
} }
fn make_rect_wipeout(p1: Vec3, p2: Vec3) -> EntityType { fn make_rect_wipeout(p1: DVec3, p2: DVec3) -> EntityType {
// Drawing plane is world XY (z = elevation). // Drawing plane is world XY (z = elevation).
let c1 = Vector3::new(p1.x.min(p2.x) as f64, p1.y.min(p2.y) as f64, p1.z as f64); let c1 = Vector3::new(p1.x.min(p2.x), p1.y.min(p2.y), p1.z);
let c2 = Vector3::new(p1.x.max(p2.x) as f64, p1.y.max(p2.y) as f64, p1.z as f64); let c2 = Vector3::new(p1.x.max(p2.x), p1.y.max(p2.y), p1.z);
EntityType::Wipeout(Wipeout::from_corners(c1, c2)) EntityType::Wipeout(Wipeout::from_corners(c1, c2))
} }
fn make_poly_wipeout(pts: &[Vec3]) -> EntityType { fn make_poly_wipeout(pts: &[DVec3]) -> EntityType {
use acadrust::types::Vector2; use acadrust::types::Vector2;
let z = pts[0].z as f64; let z = pts[0].z;
let verts: Vec<Vector2> = pts let verts: Vec<Vector2> = pts.iter().map(|p| Vector2::new(p.x, p.y)).collect();
.iter()
.map(|p| Vector2::new(p.x as f64, p.y as f64))
.collect();
EntityType::Wipeout(Wipeout::polygonal(&verts, z)) EntityType::Wipeout(Wipeout::polygonal(&verts, z))
} }

View file

@ -10,17 +10,17 @@
// With 2 pairs: translate + rotate (+ optional uniform scale to fit) // With 2 pairs: translate + rotate (+ optional uniform scale to fit)
use acadrust::Handle; use acadrust::Handle;
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult, EntityTransform}; use crate::command::{CadCommand, CmdResult, EntityTransform};
pub struct AlignCommand { pub struct AlignCommand {
state: AlignState, state: AlignState,
handles: Vec<Handle>, handles: Vec<Handle>,
src1: Option<Vec3>, src1: Option<DVec3>,
dst1: Option<Vec3>, dst1: Option<DVec3>,
src2: Option<Vec3>, src2: Option<DVec3>,
dst2: Option<Vec3>, dst2: Option<DVec3>,
} }
#[derive(PartialEq)] #[derive(PartialEq)]
@ -74,7 +74,7 @@ impl CadCommand for AlignCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.state { match self.state {
AlignState::Gathering => CmdResult::NeedPoint, AlignState::Gathering => CmdResult::NeedPoint,
AlignState::Src1 => { AlignState::Src1 => {
@ -117,7 +117,7 @@ impl CadCommand for AlignCommand {
let delta = d - s; let delta = d - s;
CmdResult::TransformSelected( CmdResult::TransformSelected(
self.handles.clone(), self.handles.clone(),
EntityTransform::Translate(delta.as_dvec3()), EntityTransform::Translate(delta),
) )
} }
_ => CmdResult::Cancel, _ => CmdResult::Cancel,
@ -163,7 +163,7 @@ impl AlignCommand {
let delta = d1 - s1; let delta = d1 - s1;
return CmdResult::TransformSelected( return CmdResult::TransformSelected(
self.handles.clone(), self.handles.clone(),
EntityTransform::Translate(delta.as_dvec3()), EntityTransform::Translate(delta),
); );
} }
@ -182,10 +182,12 @@ impl AlignCommand {
// Compose via AlignTransform CmdResult // Compose via AlignTransform CmdResult
CmdResult::AlignSelected { CmdResult::AlignSelected {
handles: self.handles.clone(), handles: self.handles.clone(),
src1: s1.as_dvec3(), src1: s1,
dst1: d1.as_dvec3(), dst1: d1,
angle_rad: angle, // Reference points (src1/dst1) stay f64 exact; the derived
scale: scale_factor, // similarity scalars feed the f32 EntityTransform, so downcast here.
angle_rad: angle as f32,
scale: scale_factor as f32,
} }
} }
} }

View file

@ -14,7 +14,7 @@
// 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, Vec3}; use glam::{DVec3, Mat4};
use crate::command::{CadCommand, CmdResult, EntityTransform}; use crate::command::{CadCommand, CmdResult, EntityTransform};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
@ -246,8 +246,8 @@ impl CadCommand for ArrayRectCommand {
enum PolarStep { enum PolarStep {
Center, Center,
Count { center: Vec3 }, Count { center: DVec3 },
Angle { center: Vec3, count: u32 }, Angle { center: DVec3, count: u32 },
} }
pub struct ArrayPolarCommand { pub struct ArrayPolarCommand {
@ -331,7 +331,6 @@ impl CadCommand for ArrayPolarCommand {
v v
}; };
let step_rad = total_deg.to_radians() / count as f32; let step_rad = total_deg.to_radians() / count as f32;
let center = center.as_dvec3();
let transforms = (1..count) let transforms = (1..count)
.map(|n| EntityTransform::Rotate { .map(|n| EntityTransform::Rotate {
center, center,
@ -348,8 +347,12 @@ impl CadCommand for ArrayPolarCommand {
let pt = pt.as_vec3(); let pt = pt.as_vec3();
let (center, count, total_deg) = match &self.step { let (center, count, total_deg) = match &self.step {
PolarStep::Center => (pt, self.default_count, self.default_angle), PolarStep::Center => (pt, self.default_count, self.default_angle),
PolarStep::Count { center } => (*center, self.default_count, self.default_angle), PolarStep::Count { center } => {
PolarStep::Angle { center, count } => (*center, *count, self.default_angle), (center.as_vec3(), self.default_count, self.default_angle)
}
PolarStep::Angle { center, count } => {
(center.as_vec3(), *count, self.default_angle)
}
}; };
let step_rad = total_deg.to_radians() / count as f32; let step_rad = total_deg.to_radians() / count as f32;
let mut out: Vec<WireModel> = (1..count) let mut out: Vec<WireModel> = (1..count)
@ -373,7 +376,6 @@ impl CadCommand for ArrayPolarCommand {
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
let pt = pt.as_vec3();
if let PolarStep::Center = self.step { if let PolarStep::Center = self.step {
self.step = PolarStep::Count { center: pt }; self.step = PolarStep::Count { center: pt };
} }
@ -398,8 +400,8 @@ impl CadCommand for ArrayPolarCommand {
// → Returns BatchCopy with Translate transforms derived from path samples. // → Returns BatchCopy with Translate transforms derived from path samples.
use acadrust::EntityType; use acadrust::EntityType;
use std::f32::consts::PI as FPI; use std::f64::consts::PI as FPI;
use std::f32::consts::TAU as FTAU; use std::f64::consts::TAU as FTAU;
// ── Path geometry helpers ────────────────────────────────────────────────── // ── Path geometry helpers ──────────────────────────────────────────────────
@ -408,7 +410,7 @@ use std::f32::consts::TAU as FTAU;
/// sweep) and every point is mapped OCS→WCS via the polyline's normal. The /// sweep) and every point is mapped OCS→WCS via the polyline's normal. The
/// previous ad-hoc bulge math placed the arc centre on the wrong side of the /// previous ad-hoc bulge math placed the arc centre on the wrong side of the
/// chord and ignored the extrusion, so arc-segment paths came out mirrored. /// chord and ignored the extrusion, so arc-segment paths came out mirrored.
fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<Vec3> { fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<DVec3> {
use crate::entities::common::BulgeArc; use crate::entities::common::BulgeArc;
use crate::scene::view::transform::ocs_point_to_wcs; use crate::scene::view::transform::ocs_point_to_wcs;
@ -418,14 +420,14 @@ fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<Vec3> {
} }
let normal = (p.normal.x, p.normal.y, p.normal.z); let normal = (p.normal.x, p.normal.y, p.normal.z);
let elev = p.elevation; let elev = p.elevation;
let to_wcs = |x: f64, y: f64| -> Vec3 { let to_wcs = |x: f64, y: f64| -> DVec3 {
let (wx, wy, wz) = ocs_point_to_wcs((x, y, elev), normal); let (wx, wy, wz) = ocs_point_to_wcs((x, y, elev), normal);
Vec3::new(wx as f32, wy as f32, wz as f32) DVec3::new(wx, wy, wz)
}; };
let n = verts.len(); let n = verts.len();
let segs = if p.is_closed { n } else { n.saturating_sub(1) }; let segs = if p.is_closed { n } else { n.saturating_sub(1) };
let mut out: Vec<Vec3> = vec![]; let mut out: Vec<DVec3> = vec![];
for i in 0..segs { for i in 0..segs {
let v0 = &verts[i]; let v0 = &verts[i];
@ -456,15 +458,15 @@ fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<Vec3> {
} }
/// Walk `pts` (ordered) and return `count` points at equal arc-length spacing. /// Walk `pts` (ordered) and return `count` points at equal arc-length spacing.
fn subsample_equidistant(pts: &[Vec3], count: usize) -> Vec<Vec3> { fn subsample_equidistant(pts: &[DVec3], count: usize) -> Vec<DVec3> {
if count == 0 { if count == 0 {
return vec![]; return vec![];
} }
if pts.len() < 2 { if pts.len() < 2 {
return vec![pts.first().copied().unwrap_or(Vec3::ZERO); count]; return vec![pts.first().copied().unwrap_or(DVec3::ZERO); count];
} }
let mut cum = vec![0.0f32; pts.len()]; let mut cum = vec![0.0f64; pts.len()];
for i in 1..pts.len() { for i in 1..pts.len() {
cum[i] = cum[i - 1] + pts[i].distance(pts[i - 1]); cum[i] = cum[i - 1] + pts[i].distance(pts[i - 1]);
} }
@ -477,7 +479,7 @@ fn subsample_equidistant(pts: &[Vec3], count: usize) -> Vec<Vec3> {
let mut seg = 0usize; let mut seg = 0usize;
for i in 0..count { for i in 0..count {
let target = if count > 1 { let target = if count > 1 {
total * i as f32 / (count - 1) as f32 total * i as f64 / (count - 1) as f64
} else { } else {
0.0 0.0
}; };
@ -512,7 +514,7 @@ pub struct ArrayPathCommand {
/// path end nearest this point, so clicking near either end picks the /// path end nearest this point, so clicking near either end picks the
/// travel direction (a stored arc is always CCW regardless of how it was /// travel direction (a stored arc is always CCW regardless of how it was
/// drawn, so without this the array could run opposite to expectation). /// drawn, so without this the array could run opposite to expectation).
pick_pt: Vec3, pick_pt: DVec3,
} }
impl ArrayPathCommand { impl ArrayPathCommand {
@ -527,13 +529,13 @@ impl ArrayPathCommand {
all_entities, all_entities,
step: PathStep::SelectPath, step: PathStep::SelectPath,
default_count: defaults::get_array_path_count() as u32, default_count: defaults::get_array_path_count() as u32,
pick_pt: Vec3::ZERO, pick_pt: DVec3::ZERO,
} }
} }
/// Sample the path and orient the points so index 0 is the end nearest the /// Sample the path and orient the points so index 0 is the end nearest the
/// pick point, giving the user control over the array's travel direction. /// pick point, giving the user control over the array's travel direction.
fn oriented_samples(&self, entity: &EntityType, count: usize) -> Vec<Vec3> { fn oriented_samples(&self, entity: &EntityType, count: usize) -> Vec<DVec3> {
let mut pts = Self::sample_path(entity, count); let mut pts = Self::sample_path(entity, count);
if pts.len() >= 2 { if pts.len() >= 2 {
let d_first = self.pick_pt.distance_squared(pts[0]); let d_first = self.pick_pt.distance_squared(pts[0]);
@ -546,16 +548,16 @@ impl ArrayPathCommand {
} }
/// Sample `count` evenly-spaced points along `entity`. /// Sample `count` evenly-spaced points along `entity`.
fn sample_path(entity: &EntityType, count: usize) -> Vec<Vec3> { fn sample_path(entity: &EntityType, count: usize) -> Vec<DVec3> {
if count == 0 { if count == 0 {
return vec![]; return vec![];
} }
match entity { match entity {
EntityType::Line(l) => { EntityType::Line(l) => {
let p0 = Vec3::new(l.start.x as f32, l.start.y as f32, 0.0); let p0 = DVec3::new(l.start.x, l.start.y, 0.0);
let p1 = Vec3::new(l.end.x as f32, l.end.y as f32, 0.0); let p1 = DVec3::new(l.end.x, l.end.y, 0.0);
let d = (count - 1).max(1) as f32; let d = (count - 1).max(1) as f64;
(0..count).map(|i| p0.lerp(p1, i as f32 / d)).collect() (0..count).map(|i| p0.lerp(p1, i as f64 / d)).collect()
} }
EntityType::Arc(a) => { EntityType::Arc(a) => {
// Sample in WCS via the arc's OCS basis so flipped-normal arcs // Sample in WCS via the arc's OCS basis so flipped-normal arcs
@ -576,10 +578,10 @@ impl ArrayPathCommand {
.map(|i| { .map(|i| {
let ang = sa + span * (i as f64 / d); let ang = sa + span * (i as f64 / d);
let (c, s) = (ang.cos(), ang.sin()); let (c, s) = (ang.cos(), ang.sin());
Vec3::new( DVec3::new(
(cx + r * c * ax.0 + r * s * ay.0) as f32, cx + r * c * ax.0 + r * s * ay.0,
(cy + r * c * ax.1 + r * s * ay.1) as f32, cy + r * c * ax.1 + r * s * ay.1,
(cz + r * c * ax.2 + r * s * ay.2) as f32, cz + r * c * ax.2 + r * s * ay.2,
) )
}) })
.collect() .collect()
@ -595,25 +597,25 @@ impl ArrayPathCommand {
.map(|i| { .map(|i| {
let ang = i as f64 / count as f64 * std::f64::consts::TAU; let ang = i as f64 / count as f64 * std::f64::consts::TAU;
let (cs, sn) = (ang.cos(), ang.sin()); let (cs, sn) = (ang.cos(), ang.sin());
Vec3::new( DVec3::new(
(cx + r * cs * ax.0 + r * sn * ay.0) as f32, cx + r * cs * ax.0 + r * sn * ay.0,
(cy + r * cs * ax.1 + r * sn * ay.1) as f32, cy + r * cs * ax.1 + r * sn * ay.1,
(cz + r * cs * ax.2 + r * sn * ay.2) as f32, cz + r * cs * ax.2 + r * sn * ay.2,
) )
}) })
.collect() .collect()
} }
EntityType::LwPolyline(p) => subsample_equidistant(&lw_dense_pts(p), count), EntityType::LwPolyline(p) => subsample_equidistant(&lw_dense_pts(p), count),
_ => vec![Vec3::ZERO; count], _ => vec![DVec3::ZERO; count],
} }
} }
/// Continuous path-tangent angle (XY) at each sample, via central /// Continuous path-tangent angle (XY) at each sample, via central
/// differences and unwrapped so the sequence stays smooth across an arc /// differences and unwrapped so the sequence stays smooth across an arc
/// that turns more than ±π in total. /// that turns more than ±π in total.
fn tangents(pts: &[Vec3]) -> Vec<f32> { fn tangents(pts: &[DVec3]) -> Vec<f64> {
let n = pts.len(); let n = pts.len();
let mut a = vec![0.0f32; n]; let mut a = vec![0.0f64; n];
for i in 0..n { for i in 0..n {
let prev = if i > 0 { pts[i - 1] } else { pts[i] }; let prev = if i > 0 { pts[i - 1] } else { pts[i] };
let next = if i + 1 < n { pts[i + 1] } else { pts[i] }; let next = if i + 1 < n { pts[i + 1] } else { pts[i] };
@ -645,7 +647,7 @@ impl ArrayPathCommand {
/// orientation; each copy rotates by the tangent change at its sample). /// orientation; each copy rotates by the tangent change at its sample).
/// On a straight path the tangent never changes, so every copy is a pure /// On a straight path the tangent never changes, so every copy is a pure
/// `Translate` — identical to a non-aligned array. /// `Translate` — identical to a non-aligned array.
fn build_transforms(pts: &[Vec3]) -> Vec<EntityTransform> { fn build_transforms(pts: &[DVec3]) -> Vec<EntityTransform> {
if pts.len() < 2 { if pts.len() < 2 {
return vec![]; return vec![];
} }
@ -658,14 +660,14 @@ impl ArrayPathCommand {
.map(|(i, &p)| { .map(|(i, &p)| {
let dth = tans[i] - t0; let dth = tans[i] - t0;
if dth.abs() < 1e-5 { if dth.abs() < 1e-5 {
EntityTransform::Translate((p - p0).as_dvec3()) EntityTransform::Translate(p - p0)
} else { } else {
// The aligned copy is the rigid motion x' = p + R(x - p0), // The aligned copy is the rigid motion x' = p + R(x - p0),
// which is a pure rotation by `dth` about its fixed point. // which is a pure rotation by `dth` about its fixed point.
let center = Self::rigid_center(p0, p, dth); let center = Self::rigid_center(p0, p, dth);
EntityTransform::Rotate { EntityTransform::Rotate {
center: center.as_dvec3(), center,
angle_rad: dth, angle_rad: dth as f32,
} }
} }
}) })
@ -674,7 +676,7 @@ impl ArrayPathCommand {
/// Fixed point of the rigid motion `x' = pi + R(dth)·(x - p0)`; rotating an /// Fixed point of the rigid motion `x' = pi + R(dth)·(x - p0)`; rotating an
/// entity by `dth` about this point reproduces that motion exactly. /// entity by `dth` about this point reproduces that motion exactly.
fn rigid_center(p0: Vec3, pi: Vec3, dth: f32) -> Vec3 { fn rigid_center(p0: DVec3, pi: DVec3, dth: f64) -> DVec3 {
let (s, c) = dth.sin_cos(); let (s, c) = dth.sin_cos();
// b = pi - R·p0 // b = pi - R·p0
let bx = pi.x - (c * p0.x - s * p0.y); let bx = pi.x - (c * p0.x - s * p0.y);
@ -682,7 +684,7 @@ impl ArrayPathCommand {
// c = (I - R)^{-1} · b, with (I-R)^{-1} = 1/(2(1-c)) [[1-c, -s],[s, 1-c]] // c = (I - R)^{-1} · b, with (I-R)^{-1} = 1/(2(1-c)) [[1-c, -s],[s, 1-c]]
let one_c = 1.0 - c; let one_c = 1.0 - c;
let det = 2.0 * one_c; let det = 2.0 * one_c;
Vec3::new( DVec3::new(
(one_c * bx - s * by) / det, (one_c * bx - s * by) / det,
(s * bx + one_c * by) / det, (s * bx + one_c * by) / det,
p0.z, p0.z,
@ -724,7 +726,6 @@ impl CadCommand for ArrayPathCommand {
} }
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
let pt = pt.as_vec3();
if handle.is_null() || self.handles.contains(&handle) { if handle.is_null() || self.handles.contains(&handle) {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
@ -758,7 +759,9 @@ impl CadCommand for ArrayPathCommand {
if pts.len() >= 2 { if pts.len() >= 2 {
return vec![WireModel::solid( return vec![WireModel::solid(
"arraypath_hover".into(), "arraypath_hover".into(),
pts.iter().map(|p| [p.x, p.y, p.z]).collect(), pts.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect(),
WireModel::CYAN, WireModel::CYAN,
false, false,
)]; )];

View file

@ -15,7 +15,7 @@ use acadrust::entities::{
}; };
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::{EntityType, Handle}; use acadrust::{EntityType, Handle};
use glam::{DVec3, Vec3}; use glam::DVec3;
use truck_modeling::base::{BoundedCurve, Cut}; use truck_modeling::base::{BoundedCurve, Cut};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
@ -38,7 +38,7 @@ pub fn tool() -> ToolDef {
/// Break `entity` between world-space points `p1` and `p2`. /// Break `entity` between world-space points `p1` and `p2`.
/// Returns the replacement entities (empty vec means "erase, no replacement"). /// Returns the replacement entities (empty vec means "erase, no replacement").
pub fn break_entity(entity: &EntityType, p1: Vec3, p2: Vec3) -> Option<Vec<EntityType>> { pub fn break_entity(entity: &EntityType, p1: DVec3, p2: DVec3) -> Option<Vec<EntityType>> {
match entity { match entity {
EntityType::Line(line) => Some(break_line(line, p1, p2)), EntityType::Line(line) => Some(break_line(line, p1, p2)),
EntityType::Arc(arc) => Some(break_arc(arc, p1, p2)), EntityType::Arc(arc) => Some(break_arc(arc, p1, p2)),
@ -50,13 +50,9 @@ pub fn break_entity(entity: &EntityType, p1: Vec3, p2: Vec3) -> Option<Vec<Entit
} }
} }
fn break_line(line: &LineEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> { fn break_line(line: &LineEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let s = Vec3::new( let s = DVec3::new(line.start.x, line.start.y, line.start.z);
line.start.x as f32, let e = DVec3::new(line.end.x, line.end.y, line.end.z);
line.start.y as f32,
line.start.z as f32,
);
let e = Vec3::new(line.end.x as f32, line.end.y as f32, line.end.z as f32);
let dir = e - s; let dir = e - s;
let len2 = dir.length_squared(); let len2 = dir.length_squared();
if len2 < 1e-12 { if len2 < 1e-12 {
@ -94,17 +90,17 @@ fn break_line(line: &LineEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
result result
} }
fn break_arc(arc: &ArcEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> { fn break_arc(arc: &ArcEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let cx = arc.center.x as f32; let cx = arc.center.x;
let cy = arc.center.y as f32; let cy = arc.center.y;
let r = arc.radius as f32; let r = arc.radius;
// Project p1 and p2 onto the arc (world XY plane) // Project p1 and p2 onto the arc (world XY plane)
let a1 = angle_on_arc(cx, cy, p1); let a1 = angle_on_arc(cx, cy, p1);
let a2 = angle_on_arc(cx, cy, p2); let a2 = angle_on_arc(cx, cy, p2);
let start = arc.start_angle as f32; let start = arc.start_angle;
let end = arc.end_angle as f32; let end = arc.end_angle;
// Normalize: clamp a1 to arc range, then remove CCW from a1 to a2 // Normalize: clamp a1 to arc range, then remove CCW from a1 to a2
let a1_on = clamp_to_arc(a1, start, end); let a1_on = clamp_to_arc(a1, start, end);
@ -120,14 +116,14 @@ fn break_arc(arc: &ArcEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
let _ = r; // radius unchanged let _ = r; // radius unchanged
let mut result = arc.clone(); let mut result = arc.clone();
result.common.handle = Handle::NULL; result.common.handle = Handle::NULL;
result.start_angle = a2_on as f64; result.start_angle = a2_on;
result.end_angle = a1_on as f64; result.end_angle = a1_on;
vec![EntityType::Arc(result)] vec![EntityType::Arc(result)]
} }
fn break_circle(circle: &acadrust::entities::Circle, p1: Vec3, p2: Vec3) -> Vec<EntityType> { fn break_circle(circle: &acadrust::entities::Circle, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let cx = circle.center.x as f32; let cx = circle.center.x;
let cy = circle.center.y as f32; let cy = circle.center.y;
let a1 = angle_on_arc(cx, cy, p1); let a1 = angle_on_arc(cx, cy, p1);
let a2 = angle_on_arc(cx, cy, p2); let a2 = angle_on_arc(cx, cy, p2);
@ -143,12 +139,12 @@ fn break_circle(circle: &acadrust::entities::Circle, p1: Vec3, p2: Vec3) -> Vec<
arc.center = circle.center.clone(); arc.center = circle.center.clone();
arc.radius = circle.radius; arc.radius = circle.radius;
arc.normal = circle.normal.clone(); arc.normal = circle.normal.clone();
arc.start_angle = a2 as f64; arc.start_angle = a2;
arc.end_angle = a1 as f64; arc.end_angle = a1;
vec![EntityType::Arc(arc)] vec![EntityType::Arc(arc)]
} }
fn break_lwpolyline(p: &LwPolyline, p1: Vec3, p2: Vec3) -> Vec<EntityType> { fn break_lwpolyline(p: &LwPolyline, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
// For LwPolyline, find the nearest vertex indices for p1 and p2, // For LwPolyline, find the nearest vertex indices for p1 and p2,
// then split into two polylines at those vertices. // then split into two polylines at those vertices.
let n = p.vertices.len(); let n = p.vertices.len();
@ -191,7 +187,7 @@ fn break_lwpolyline(p: &LwPolyline, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
} }
} }
fn break_ellipse(ell: &EllipseEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> { fn break_ellipse(ell: &EllipseEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
// Compute the eccentric-anomaly parameter of a world point relative to ellipse. // Compute the eccentric-anomaly parameter of a world point relative to ellipse.
// World = DXF (XY drawing plane). // World = DXF (XY drawing plane).
let cx = ell.center.x; let cx = ell.center.x;
@ -205,9 +201,9 @@ fn break_ellipse(ell: &EllipseEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
let ny = ell.major_axis.y / a; let ny = ell.major_axis.y / a;
// Project a point onto the ellipse parameter (eccentric anomaly) // Project a point onto the ellipse parameter (eccentric anomaly)
let param_of = |pt: Vec3| -> f64 { let param_of = |pt: DVec3| -> f64 {
let rx = pt.x as f64 - cx; let rx = pt.x - cx;
let ry = pt.y as f64 - cy; let ry = pt.y - cy;
let xl = rx * nx + ry * ny; let xl = rx * nx + ry * ny;
let yl = -rx * ny + ry * nx; let yl = -rx * ny + ry * nx;
yl.atan2(xl) // atan2(yl/b*b, xl/a*a) simplifies to atan2(yl,xl) for ordering yl.atan2(xl) // atan2(yl/b*b, xl/a*a) simplifies to atan2(yl,xl) for ordering
@ -257,19 +253,19 @@ fn break_ellipse(ell: &EllipseEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
// ── Small utilities ──────────────────────────────────────────────────────── // ── Small utilities ────────────────────────────────────────────────────────
/// Returns the angle (radians, 0-2π) of `pt` viewed from (cx, cy) in the world XY plane. /// Returns the angle (radians, 0-2π) of `pt` viewed from (cx, cy) in the world XY plane.
fn angle_on_arc(cx: f32, cy: f32, pt: Vec3) -> f32 { fn angle_on_arc(cx: f64, cy: f64, pt: DVec3) -> f64 {
let dx = pt.x - cx; let dx = pt.x - cx;
let dy = pt.y - cy; let dy = pt.y - cy;
dy.atan2(dx).rem_euclid(std::f32::consts::TAU) dy.atan2(dx).rem_euclid(std::f64::consts::TAU)
} }
/// Clamp angle `a` to within the arc's angular range (CCW from `start` to `end`). /// Clamp angle `a` to within the arc's angular range (CCW from `start` to `end`).
fn clamp_to_arc(a: f32, start: f32, end: f32) -> f32 { fn clamp_to_arc(a: f64, start: f64, end: f64) -> f64 {
let span = (end - start).rem_euclid(std::f32::consts::TAU); let span = (end - start).rem_euclid(std::f64::consts::TAU);
let rel = (a - start).rem_euclid(std::f32::consts::TAU); let rel = (a - start).rem_euclid(std::f64::consts::TAU);
if rel <= span { if rel <= span {
a a
} else if rel < span + (std::f32::consts::TAU - span) / 2.0 { } else if rel < span + (std::f64::consts::TAU - span) / 2.0 {
end end
} else { } else {
start start
@ -277,35 +273,35 @@ fn clamp_to_arc(a: f32, start: f32, end: f32) -> f32 {
} }
/// Find the index of the polyline vertex closest to `pt`. /// Find the index of the polyline vertex closest to `pt`.
fn nearest_pline_param(p: &LwPolyline, pt: Vec3) -> usize { fn nearest_pline_param(p: &LwPolyline, pt: DVec3) -> usize {
p.vertices p.vertices
.iter() .iter()
.enumerate() .enumerate()
.min_by_key(|(_, v)| { .min_by_key(|(_, v)| {
let dx = v.location.x as f32 - pt.x; let dx = v.location.x - pt.x;
let dy = v.location.y as f32 - pt.y; let dy = v.location.y - pt.y;
((dx * dx + dy * dy) * 1e6) as i64 ((dx * dx + dy * dy) * 1e6) as i64
}) })
.map(|(i, _)| i) .map(|(i, _)| i)
.unwrap_or(0) .unwrap_or(0)
} }
fn world_to_dxf(v: Vec3) -> Vec3 { fn world_to_dxf(v: DVec3) -> DVec3 {
// World = DXF (identity). // World = DXF (identity).
Vec3::new(v.x, v.y, v.z) DVec3::new(v.x, v.y, v.z)
} }
fn vec3_to_v3(v: Vec3) -> Vector3 { fn vec3_to_v3(v: DVec3) -> Vector3 {
Vector3::new(v.x as f64, v.y as f64, v.z as f64) Vector3::new(v.x, v.y, v.z)
} }
fn break_spline(spl: &SplineEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> { fn break_spline(spl: &SplineEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
// Find the two nearest parameters to p1 and p2 (DXF XY: world x, z). // Find the two nearest parameters to p1 and p2 (DXF XY: world x, z).
let t1 = match spline_nearest_t(spl, p1.x as f64, p1.y as f64) { let t1 = match spline_nearest_t(spl, p1.x, p1.y) {
Some(t) => t, Some(t) => t,
None => return vec![EntityType::Spline(spl.clone())], None => return vec![EntityType::Spline(spl.clone())],
}; };
let t2 = match spline_nearest_t(spl, p2.x as f64, p2.y as f64) { let t2 = match spline_nearest_t(spl, p2.x, p2.y) {
Some(t) => t, Some(t) => t,
None => return vec![EntityType::Spline(spl.clone())], None => return vec![EntityType::Spline(spl.clone())],
}; };
@ -351,7 +347,7 @@ fn break_spline(spl: &SplineEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
/// BreakEntity CmdResult variant added below. /// BreakEntity CmdResult variant added below.
pub struct BreakInteractiveCommand { pub struct BreakInteractiveCommand {
target: Option<Handle>, target: Option<Handle>,
p1: Option<Vec3>, p1: Option<DVec3>,
} }
impl BreakInteractiveCommand { impl BreakInteractiveCommand {
@ -382,7 +378,7 @@ impl CadCommand for BreakInteractiveCommand {
self.target.is_none() self.target.is_none()
} }
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
if handle.is_null() { if handle.is_null() {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
@ -391,7 +387,7 @@ impl CadCommand for BreakInteractiveCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
let handle = match self.target { let handle = match self.target {
Some(h) => h, Some(h) => h,
None => return CmdResult::Cancel, None => return CmdResult::Cancel,
@ -403,7 +399,7 @@ impl CadCommand for BreakInteractiveCommand {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
}; };
CmdResult::BreakEntity { handle, p1: p1.as_dvec3(), p2: pt.as_dvec3() } CmdResult::BreakEntity { handle, p1, p2: pt }
} }
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
@ -452,12 +448,12 @@ impl CadCommand for BreakAtPointCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.target { match self.target {
Some(handle) => CmdResult::BreakEntity { Some(handle) => CmdResult::BreakEntity {
handle, handle,
p1: pt.as_dvec3(), p1: pt,
p2: pt.as_dvec3(), p2: pt,
}, },
None => CmdResult::Cancel, None => CmdResult::Cancel,
} }

View file

@ -159,11 +159,16 @@ pub fn lengthen_entity(entity: &EntityType, pick_pt: Vec3, mode: &LenMode) -> Op
} }
fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> { fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
let s = Vec3::new(line.start.x as f32, line.start.y as f32, 0.0); // Keep the line's own coordinates on the f64 grid; only pick_pt (screen-only,
let e = Vec3::new(line.end.x as f32, line.end.y as f32, 0.0); // nearest-end selection) widens for the distance compare.
let p = Vec3::new(pick_pt.x, pick_pt.y, 0.0); let sx = line.start.x;
let sy = line.start.y;
let ex = line.end.x;
let ey = line.end.y;
let current_len = (e - s).length() as f64; let dx = ex - sx;
let dy = ey - sy;
let current_len = (dx * dx + dy * dy).sqrt();
if current_len < 1e-10 { if current_len < 1e-10 {
return None; return None;
} }
@ -173,23 +178,28 @@ fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Entity
return None; return None;
} }
let dir = (e - s) / current_len as f32; let ux = dx / current_len;
let uy = dy / current_len;
// Which end is closer to pick? // Which end is closer to pick?
let dist_to_start = (p - s).length(); let px = pick_pt.x as f64;
let dist_to_end = (p - e).length(); let py = pick_pt.y as f64;
let dist_to_start = (px - sx).hypot(py - sy);
let dist_to_end = (px - ex).hypot(py - ey);
let mut result = line.clone(); let mut result = line.clone();
result.common.handle = Handle::NULL; result.common.handle = Handle::NULL;
if dist_to_end <= dist_to_start { if dist_to_end <= dist_to_start {
// Extend/trim the end // Extend/trim the end
let new_end = s + dir * new_len as f32; let new_x = sx + ux * new_len;
result.end = xz_to_v3(new_end, line.end.z); let new_y = sy + uy * new_len;
result.end = Vector3::new(new_x, new_y, line.end.z);
} else { } else {
// Extend/trim the start (move start backward along dir) // Extend/trim the start (move start backward along dir)
let new_start = e - dir * new_len as f32; let new_x = ex - ux * new_len;
result.start = xz_to_v3(new_start, line.start.z); let new_y = ey - uy * new_len;
result.start = Vector3::new(new_x, new_y, line.start.z);
} }
Some(EntityType::Line(result)) Some(EntityType::Line(result))
} }
@ -335,11 +345,6 @@ fn arc_span_rad(start: f64, end: f64) -> f64 {
} }
} }
fn xz_to_v3(v: Vec3, z: f64) -> Vector3 {
// v is (world_x, world_z, 0) → DXF (x, world_z, z)
Vector3::new(v.x as f64, v.y as f64, z)
}
fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> { fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
let n = poly.vertices.len(); let n = poly.vertices.len();
if n < 2 { if n < 2 {

View file

@ -16,7 +16,7 @@
// All others : move the whole entity if any point is inside. // All others : move the whole entity if any point is inside.
use acadrust::Handle; use acadrust::Handle;
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
@ -39,14 +39,14 @@ enum Step {
/// Waiting for the first crossing-window corner. /// Waiting for the first crossing-window corner.
WindowCorner1, WindowCorner1,
/// Waiting for the second corner; `c1` is the first corner. /// Waiting for the second corner; `c1` is the first corner.
WindowCorner2(Vec3), WindowCorner2(DVec3),
/// Crossing window defined; waiting for base point. /// Crossing window defined; waiting for base point.
Base { win_min: Vec3, win_max: Vec3 }, Base { win_min: DVec3, win_max: DVec3 },
/// Waiting for target point. /// Waiting for target point.
Target { Target {
win_min: Vec3, win_min: DVec3,
win_max: Vec3, win_max: DVec3,
base: Vec3, base: DVec3,
}, },
} }
@ -86,7 +86,7 @@ impl CadCommand for StretchCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step { match &self.step {
Step::WindowCorner1 => { Step::WindowCorner1 => {
self.step = Step::WindowCorner2(pt); self.step = Step::WindowCorner2(pt);
@ -115,9 +115,9 @@ impl CadCommand for StretchCommand {
let delta = pt - *base; let delta = pt - *base;
CmdResult::StretchEntities { CmdResult::StretchEntities {
handles: self.handles.clone(), handles: self.handles.clone(),
win_min: win_min.as_dvec3(), win_min: *win_min,
win_max: win_max.as_dvec3(), win_max: *win_max,
delta: delta.as_dvec3(), delta,
} }
} }
} }
@ -140,12 +140,12 @@ impl CadCommand for StretchCommand {
// Expose the first corner so the host draws a filled crossing marquee to // Expose the first corner so the host draws a filled crossing marquee to
// the cursor, matching a normal box selection instead of a bare outline. // the cursor, matching a normal box selection instead of a bare outline.
match &self.step { match &self.step {
Step::WindowCorner2(c1) => Some(c1.as_dvec3()), Step::WindowCorner2(c1) => Some(*c1),
_ => None, _ => None,
} }
} }
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> { let pt = pt.as_vec3(); fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
match &self.step { match &self.step {
// The crossing-window rectangle is drawn as a filled selection // The crossing-window rectangle is drawn as a filled selection
// marquee by the host (via window_first_corner) so it matches a // marquee by the host (via window_first_corner) so it matches a
@ -158,15 +158,19 @@ impl CadCommand for StretchCommand {
} => { } => {
let delta = pt - *base; let delta = pt - *base;
// Live ghost: vertices inside the crossing window follow the // Live ghost: vertices inside the crossing window follow the
// cursor, the rest stay anchored. // cursor, the rest stay anchored. This is the preview/GPU path,
// so downcast to f32 only at the WireModel boundary.
let mut out: Vec<WireModel> = self let mut out: Vec<WireModel> = self
.wire_models .wire_models
.iter() .iter()
.map(|w| w.stretched(*win_min, *win_max, delta)) .map(|w| w.stretched((*win_min).as_vec3(), (*win_max).as_vec3(), delta.as_vec3()))
.collect(); .collect();
out.push(WireModel::solid( out.push(WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]], vec![
[base.x as f32, base.y as f32, base.z as f32],
[pt.x as f32, pt.y as f32, pt.z as f32],
],
WireModel::CYAN, WireModel::CYAN,
false, false,
)); ));

View file

@ -6,7 +6,7 @@
// Step 2: If first point picked, pick second point to define angle vector. // Step 2: If first point picked, pick second point to define angle vector.
use acadrust::Handle; use acadrust::Handle;
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult, DynField}; use crate::command::{CadCommand, CmdResult, DynField};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -16,7 +16,7 @@ use acadrust::EntityType;
enum Step { enum Step {
AngleOrFirstPoint, AngleOrFirstPoint,
SecondPoint { first_point: Vec3 }, SecondPoint { first_point: DVec3 },
} }
pub struct TorientCommand { pub struct TorientCommand {
@ -122,7 +122,7 @@ impl CadCommand for TorientCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
Step::AngleOrFirstPoint => { Step::AngleOrFirstPoint => {
self.step = Step::SecondPoint { first_point: pt }; self.step = Step::SecondPoint { first_point: pt };
@ -131,7 +131,7 @@ impl CadCommand for TorientCommand {
Step::SecondPoint { first_point } => { Step::SecondPoint { first_point } => {
let dx = pt.x - first_point.x; let dx = pt.x - first_point.x;
let dy = pt.y - first_point.y; let dy = pt.y - first_point.y;
let angle = dy.atan2(dx) as f64; let angle = dy.atan2(dx);
self.commit_angle(Some(angle)) self.commit_angle(Some(angle))
} }
} }
@ -157,8 +157,10 @@ impl CadCommand for TorientCommand {
} }
} }
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> { let pt = pt.as_vec3(); fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let pt = pt.as_vec3();
if let Step::SecondPoint { first_point } = self.step { if let Step::SecondPoint { first_point } = self.step {
let first_point = first_point.as_vec3();
vec![WireModel::solid( vec![WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
vec![[first_point.x, first_point.y, first_point.z], [pt.x, pt.y, pt.z]], vec![[first_point.x, first_point.y, first_point.z], [pt.x, pt.y, pt.z]],

View file

@ -135,14 +135,11 @@ impl CadCommand for InsertBlockCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
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( let mut ins = Insert::new(name.clone(), Vector3::new(pt.x, pt.y, pt.z));
name.clone(),
Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64),
);
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;

View file

@ -7,7 +7,7 @@
// live in the Model tab (`modules::model::primitive_cmd`). // live in the Model tab (`modules::model::primitive_cmd`).
use acadrust::{entities::Solid3D, EntityType}; use acadrust::{entities::Solid3D, EntityType};
use glam::{DVec3, Vec3}; use glam::DVec3;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
@ -95,8 +95,8 @@ impl CadCommand for ExtrudeCommand {
pub struct RevolveCommand { pub struct RevolveCommand {
step: RevolveStep, step: RevolveStep,
target_handle: acadrust::Handle, target_handle: acadrust::Handle,
axis_start: Vec3, axis_start: DVec3,
axis_end: Vec3, axis_end: DVec3,
color: [f32; 4], color: [f32; 4],
} }
@ -113,8 +113,8 @@ impl RevolveCommand {
Self { Self {
step: RevolveStep::Pick, step: RevolveStep::Pick,
target_handle: acadrust::Handle::NULL, target_handle: acadrust::Handle::NULL,
axis_start: Vec3::ZERO, axis_start: DVec3::ZERO,
axis_end: Vec3::new(0.0, 0.0, 1.0), axis_end: DVec3::new(0.0, 0.0, 1.0),
color, color,
} }
} }
@ -143,7 +143,7 @@ impl CadCommand for RevolveCommand {
self.step = RevolveStep::AxisStart; self.step = RevolveStep::AxisStart;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
RevolveStep::AxisStart => { RevolveStep::AxisStart => {
self.axis_start = pt; self.axis_start = pt;
@ -186,8 +186,8 @@ impl RevolveCommand {
fn make_revolve(&self, angle_deg: f32) -> CmdResult { fn make_revolve(&self, angle_deg: f32) -> CmdResult {
CmdResult::RevolveEntity { CmdResult::RevolveEntity {
handle: self.target_handle, handle: self.target_handle,
axis_start: self.axis_start.as_dvec3(), axis_start: self.axis_start,
axis_end: self.axis_end.as_dvec3(), axis_end: self.axis_end,
angle_deg, angle_deg,
color: self.color, color: self.color,
} }

View file

@ -11,7 +11,7 @@ use acadrust::EntityType;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
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;
// ── Ribbon definition ───────────────────────────────────────────────────── // ── Ribbon definition ─────────────────────────────────────────────────────
@ -27,7 +27,7 @@ pub fn tool() -> ToolDef {
// ── Command ─────────────────────────────────────────────────────────────── // ── Command ───────────────────────────────────────────────────────────────
pub struct MviewCommand { pub struct MviewCommand {
corner1: Option<Vec3>, corner1: Option<DVec3>,
} }
impl MviewCommand { impl MviewCommand {
@ -49,16 +49,16 @@ impl CadCommand for MviewCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Some(c1) = self.corner1 { if let Some(c1) = self.corner1 {
let w = (pt.x - c1.x).abs() as f64; let w = (pt.x - c1.x).abs();
let h = (pt.y - c1.y).abs() as f64; let h = (pt.y - c1.y).abs();
if w < 1.0 || h < 1.0 { if w < 1.0 || h < 1.0 {
return CmdResult::Cancel; return CmdResult::Cancel;
} }
let cx = ((c1.x + pt.x) / 2.0) as f64; let cx = (c1.x + pt.x) / 2.0;
let cy = ((c1.y + pt.y) / 2.0) as f64; let cy = (c1.y + pt.y) / 2.0;
let cz = c1.z as f64; let cz = c1.z;
let mut vp = Viewport::new(); let mut vp = Viewport::new();
vp.center = Vector3::new(cx, cy, cz); vp.center = Vector3::new(cx, cy, cz);
@ -77,17 +77,18 @@ impl CadCommand for MviewCommand {
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> {
let pt = pt.as_vec3();
let c1 = self.corner1?; let c1 = self.corner1?;
Some(WireModel { Some(WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),
name: "mview_preview".to_string(), name: "mview_preview".to_string(),
points: vec![ points: vec![
[c1.x, c1.y, c1.z], [c1.x as f32, c1.y as f32, c1.z as f32],
[pt.x, c1.y, c1.z], [pt.x, c1.y as f32, c1.z as f32],
[pt.x, pt.y, c1.z], [pt.x, pt.y, c1.z as f32],
[c1.x, pt.y, c1.z], [c1.x as f32, pt.y, c1.z as f32],
[c1.x, c1.y, c1.z], [c1.x as f32, c1.y as f32, c1.z as f32],
], ],
points_low: Vec::new(), points_low: Vec::new(),
color: WireModel::CYAN, color: WireModel::CYAN,

View file

@ -9,7 +9,7 @@
use acadrust::entities::Solid3D; use acadrust::entities::Solid3D;
use acadrust::{primitives, EntityType}; use acadrust::{primitives, EntityType};
use glam::{DVec3, Vec3}; use glam::DVec3;
use truck_modeling::Solid; use truck_modeling::Solid;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
@ -63,7 +63,7 @@ impl Shape {
pub struct PrimitiveCommand { pub struct PrimitiveCommand {
shape: Shape, shape: Shape,
/// Footprint points collected so far (local/world XY, z = 0). /// Footprint points collected so far (local/world XY, z = 0).
pts: Vec<Vec3>, 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,
} }
@ -90,9 +90,9 @@ impl PrimitiveCommand {
match self.shape { match self.shape {
Shape::Box | Shape::Wedge => { Shape::Box | Shape::Wedge => {
let d = self.pts[1] - self.pts[0]; let d = self.pts[1] - self.pts[0];
(d.x.abs().max(d.y.abs())) as f64 d.x.abs().max(d.y.abs())
} }
_ => (self.pts[1] - self.pts[0]).length() as f64, _ => (self.pts[1] - self.pts[0]).length(),
} }
.max(1.0) .max(1.0)
} }
@ -103,23 +103,19 @@ impl PrimitiveCommand {
let (doc, solid) = match self.shape { let (doc, solid) = match self.shape {
Shape::Box | Shape::Wedge => { Shape::Box | Shape::Wedge => {
let (a, b) = (self.pts[0], self.pts[1]); let (a, b) = (self.pts[0], self.pts[1]);
let length = (b.x - a.x).abs() as f64; let length = (b.x - a.x).abs();
let width = (b.y - a.y).abs() as f64; let width = (b.y - a.y).abs();
if length < 1e-6 || width < 1e-6 || height < 1e-6 { if length < 1e-6 || width < 1e-6 || height < 1e-6 {
return None; return None;
} }
if self.shape == Shape::Box { if self.shape == Shape::Box {
let center = [ let center = [(a.x + b.x) / 2.0, (a.y + b.y) / 2.0, height / 2.0];
(a.x + b.x) as f64 / 2.0,
(a.y + b.y) as f64 / 2.0,
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) as f64, a.y.min(b.y) as f64, 0.0]; let origin = [a.x.min(b.x), a.y.min(b.y), 0.0];
( (
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),
@ -128,11 +124,11 @@ impl PrimitiveCommand {
} }
Shape::Cylinder | Shape::Cone => { Shape::Cylinder | Shape::Cone => {
let c = self.pts[0]; let c = self.pts[0];
let r = (self.pts[1] - c).length() as f64; let r = (self.pts[1] - c).length();
if r < 1e-6 || height < 1e-6 { if r < 1e-6 || height < 1e-6 {
return None; return None;
} }
let center = [c.x as f64, c.y as f64, 0.0]; let center = [c.x, c.y, 0.0];
if self.shape == Shape::Cylinder { if self.shape == Shape::Cylinder {
( (
primitives::build_cylinder(center, r, height), primitives::build_cylinder(center, r, height),
@ -147,11 +143,11 @@ impl PrimitiveCommand {
} }
Shape::Sphere => { Shape::Sphere => {
let c = self.pts[0]; let c = self.pts[0];
let r = (self.pts[1] - c).length() as f64; let r = (self.pts[1] - c).length();
if r < 1e-6 { if r < 1e-6 {
return None; return None;
} }
let center = [c.x as f64, c.y as f64, 0.0]; let center = [c.x, c.y, 0.0];
( (
primitives::build_sphere(center, r), primitives::build_sphere(center, r),
solid_model::sphere_solid(center, r), solid_model::sphere_solid(center, r),
@ -159,12 +155,12 @@ impl PrimitiveCommand {
} }
Shape::Torus => { Shape::Torus => {
let c = self.pts[0]; let c = self.pts[0];
let major = (self.pts[1] - c).length() as f64; let major = (self.pts[1] - c).length();
let minor = (self.pts[2] - self.pts[1]).length() as f64; let minor = (self.pts[2] - self.pts[1]).length();
if major < 1e-6 || minor < 1e-6 { if major < 1e-6 || minor < 1e-6 {
return None; return None;
} }
let center = [c.x as f64, c.y as f64, 0.0]; let center = [c.x, c.y, 0.0];
( (
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),
@ -209,11 +205,11 @@ impl CadCommand for PrimitiveCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
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() as f64; let h = (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(pt);
@ -253,7 +249,7 @@ impl CadCommand for PrimitiveCommand {
Some(self.commit(h)) Some(self.commit(h))
} }
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 self.height_step || self.pts.is_empty() { if self.height_step || self.pts.is_empty() {
return None; return None;
} }
@ -265,7 +261,7 @@ impl CadCommand for PrimitiveCommand {
// ── Footprint preview ─────────────────────────────────────────────────────── // ── Footprint preview ───────────────────────────────────────────────────────
fn footprint_wire(shape: Shape, pts: &[Vec3]) -> WireModel { fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel {
let mut points: Vec<[f32; 3]> = Vec::new(); let mut points: Vec<[f32; 3]> = Vec::new();
if shape.radial() { if shape.radial() {
let c = pts[0]; let c = pts[0];
@ -280,21 +276,25 @@ fn footprint_wire(shape: Shape, pts: &[Vec3]) -> 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, a.y, 0.0], [a.x as f32, a.y as f32, 0.0],
[b.x, a.y, 0.0], [b.x as f32, a.y as f32, 0.0],
[b.x, b.y, 0.0], [b.x as f32, b.y as f32, 0.0],
[a.x, b.y, 0.0], [a.x as f32, b.y as f32, 0.0],
[a.x, a.y, 0.0], [a.x as f32, a.y as f32, 0.0],
]); ]);
} }
wire("primitive_preview", points) wire("primitive_preview", points)
} }
fn circle_points(out: &mut Vec<[f32; 3]>, c: Vec3, r: f32) { fn circle_points(out: &mut Vec<[f32; 3]>, c: DVec3, r: f64) {
const SEG: usize = 48; const SEG: usize = 48;
for i in 0..=SEG { for i in 0..=SEG {
let t = i as f32 / SEG as f32 * std::f32::consts::TAU; let t = i as f64 / SEG as f64 * std::f64::consts::TAU;
out.push([c.x + r * t.cos(), c.y + r * t.sin(), 0.0]); out.push([
(c.x + r * t.cos()) as f32,
(c.y + r * t.sin()) as f32,
0.0,
]);
} }
} }

View file

@ -5,10 +5,10 @@
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Vec3}; use glam::DVec3;
pub struct PlotWindowCommand { pub struct PlotWindowCommand {
p1: Option<Vec3>, p1: Option<DVec3>,
} }
impl PlotWindowCommand { impl PlotWindowCommand {
@ -30,13 +30,13 @@ impl CadCommand for PlotWindowCommand {
} }
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.p1 { match self.p1 {
None => { None => {
self.p1 = Some(pt); self.p1 = Some(pt);
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Some(p1) => CmdResult::SetPlotWindow { p1: p1.as_dvec3(), p2: pt.as_dvec3() }, Some(p1) => CmdResult::SetPlotWindow { p1, p2: pt },
} }
} }
@ -45,7 +45,7 @@ impl CadCommand for PlotWindowCommand {
} }
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 pt = pt.as_vec3();
let p1 = self.p1?; let p1 = self.p1?.as_vec3();
// Draw the selection rectangle. // Draw the selection rectangle.
Some(WireModel { Some(WireModel {
text_verts: Vec::new(), text_verts: Vec::new(),

View file

@ -78,12 +78,12 @@ impl StatusBar {
// LWDISPLAY header flag — controls lineweight visibility in the viewport. // LWDISPLAY header flag — controls lineweight visibility in the viewport.
lineweight_display: bool, lineweight_display: bool,
// Live cursor position in model coordinates, for the coordinate readout. // Live cursor position in model coordinates, for the coordinate readout.
cursor_world: glam::Vec3, cursor_world: glam::DVec3,
// $COORDS readout mode: 0 = static (updates only on a pick), 1 = live // $COORDS readout mode: 0 = static (updates only on a pick), 1 = live
// absolute, 2 = polar (distance<angle from the last point while picking). // absolute, 2 = polar (distance<angle from the last point while picking).
coords_mode: i16, coords_mode: i16,
// The last committed point, for the static (0) and polar (2) readouts. // The last committed point, for the static (0) and polar (2) readouts.
last_point: Option<glam::Vec3>, last_point: Option<glam::DVec3>,
// True while a command is prompting for a point (enables the polar readout). // True while a command is prompting for a point (enables the polar readout).
picking: bool, picking: bool,
// True while clean-screen mode hides the ribbon and side panels. // True while clean-screen mode hides the ribbon and side panels.
@ -385,8 +385,8 @@ impl StatusBar {
// ── Coordinate readout ──────────────────────────────────────────────────── // ── Coordinate readout ────────────────────────────────────────────────────
fn format_coords(cursor: glam::Vec3, last: Option<glam::Vec3>, mode: i16, picking: bool) -> String { fn format_coords(cursor: glam::DVec3, last: Option<glam::DVec3>, mode: i16, picking: bool) -> String {
let abs = |p: glam::Vec3| format!("{:.4}, {:.4}, {:.4}", p.x, p.y, p.z); let abs = |p: glam::DVec3| format!("{:.4}, {:.4}, {:.4}", p.x, p.y, p.z);
match mode { match mode {
// Static: show the last picked point; the readout freezes between picks. // Static: show the last picked point; the readout freezes between picks.
0 => abs(last.unwrap_or(cursor)), 0 => abs(last.unwrap_or(cursor)),