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 SevenandClaude Opus 4.8 committed 2026-07-11 21:26:40 +03:00
1 parent 8ed10ceb2a
commit 57710eec2b
47 files changed
+699 -606

No files matched your search

+2 -2
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 {
tag,
prompt,
@@ -117,7 +117,7 @@ impl CadCommand for AttdefCommand {
tag: tag.clone(),
prompt: prompt.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,
..Default::default()
};
+12 -7
View File
@@ -8,13 +8,13 @@
use acadrust::entities::MLine;
use acadrust::types::Vector3;
use acadrust::EntityType;
use glam::{DVec3, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel;
pub struct MlineCommand {
points: Vec<Vec3>,
points: Vec<DVec3>,
scale: f64,
waiting_scale: bool,
style_name: String,
@@ -111,7 +111,7 @@ impl CadCommand for MlineCommand {
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);
CmdResult::NeedPoint
}
@@ -124,11 +124,16 @@ impl CadCommand for MlineCommand {
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() {
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]);
Some(WireModel {
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
.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();
let mut mline = if closed {
MLine::closed_from_points(&verts)
+28 -22
View File
@@ -12,7 +12,7 @@
use acadrust::entities::LwVertex;
use acadrust::types::Vector2;
use acadrust::{EntityType, Handle, LwPolyline};
use glam::{DVec3, Vec2, Vec3};
use glam::{DVec2, DVec3, Vec2, Vec3};
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
@@ -40,7 +40,7 @@ enum SegMode {
// ── Command implementation ─────────────────────────────────────────────────
pub struct PlineCommand {
vertices: Vec<Vec3>,
vertices: Vec<DVec3>,
/// Bulge for segment i → i+1 (one entry per vertex; last entry unused on commit).
bulges: Vec<f64>,
mode: SegMode,
@@ -90,7 +90,7 @@ impl PlineCommand {
.iter()
.enumerate()
.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
})
@@ -108,14 +108,14 @@ impl PlineCommand {
/// 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.
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 len_sq = d.length_squared();
if len_sq < 1e-10 {
return 0.0;
}
// 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);
if dot.abs() < 1e-10 {
// 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 {
// CCW arc: ensure arc_angle is in (0, 2π].
if arc_angle <= 0.0 {
arc_angle += std::f32::consts::TAU;
arc_angle += std::f64::consts::TAU;
}
} else {
// CW arc: ensure arc_angle is in [-2π, 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`.
@@ -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() {
let last = *self.vertices.last().unwrap();
let last_idx = self.vertices.len() - 1;
let bulge = match self.mode {
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 {
self.last_tangent = Some(d.normalize());
// Direction only — f32 is sufficient for tangent continuity.
self.last_tangent = Some(d.normalize().as_vec2());
}
0.0
}
SegMode::Arc => {
let a = Vec2::new(last.x, last.y);
let b = Vec2::new(pt.x, pt.y);
let tangent = self.last_tangent.unwrap_or_else(|| {
let a = DVec2::new(last.x, last.y);
let b = DVec2::new(pt.x, pt.y);
let tangent = self
.last_tangent
.map(|t| t.as_dvec2())
// 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);
update_tangent_after_arc(&mut self.last_tangent, 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 preview is just the pending segment from the last vertex to the
// cursor. (#119)
let last = *self.vertices.last()?;
// cursor. (#119) Downcast to f32 for the pixel-space rubber band.
let last = self.vertices.last()?.as_vec3();
let mut pts: Vec<[f32; 3]> = Vec::new();
match self.mode {
@@ -331,9 +334,12 @@ impl CadCommand for PlineCommand {
pts.push([pt.x, pt.y, pt.z]);
}
SegMode::Arc => {
let a = Vec2::new(last.x, last.y);
let b = Vec2::new(pt.x, pt.y);
let tangent = self.last_tangent.unwrap_or(Vec2::new(1.0, 0.0));
let a = DVec2::new(last.x as f64, last.y as f64);
let b = DVec2::new(pt.x as f64, pt.y as f64);
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 arc_pts = arc_sample_points(last, bulge, pt, 16);
pts.extend_from_slice(&arc_pts);
+8 -8
View File
@@ -9,7 +9,7 @@
use acadrust::entities::RasterImage;
use acadrust::types::Vector3;
use acadrust::EntityType;
use glam::{DVec3, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel;
@@ -18,7 +18,7 @@ pub struct ImageCommand {
file_path: String,
pixel_width: u32,
pixel_height: u32,
origin: Option<Vec3>,
origin: Option<DVec3>,
}
impl ImageCommand {
@@ -39,11 +39,11 @@ impl ImageCommand {
}
}
fn make_entity(&self, origin: Vec3, width_pt: Vec3) -> EntityType {
let world_width = ((width_pt.x - origin.x) as f64).abs().max(0.001);
fn make_entity(&self, origin: DVec3, width_pt: DVec3) -> EntityType {
let world_width = (width_pt.x - origin.x).abs().max(0.001);
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(
&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 {
let entity = self.make_entity(origin, pt);
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 let Some(origin) = self.origin {
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);
CmdResult::CommitAndExit(entity)
} else {
@@ -98,7 +98,7 @@ impl CadCommand for ImageCommand {
}
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_height = world_width / self.aspect() as f32;
+15 -13
View File
@@ -10,14 +10,14 @@ use acadrust::EntityType;
use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Vec3};
use glam::DVec3;
const DISPLAY_EXTENT: f32 = 1_000_000.0;
// ── RAY ───────────────────────────────────────────────────────────────────
pub struct RayCommand {
base: Option<Vec3>,
base: Option<DVec3>,
}
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 {
let dir = pt - base;
let len = dir.length();
@@ -57,8 +57,8 @@ impl CadCommand for RayCommand {
}
let dir_n = dir / len;
let ray = RayEnt::new(
Vector3::new(base.x as f64, base.y as f64, base.z as f64),
Vector3::new(dir_n.x as f64, dir_n.y as f64, dir_n.z as f64),
Vector3::new(base.x, base.y, base.z),
Vector3::new(dir_n.x, dir_n.y, dir_n.z),
);
// Stay active: new base = same base (can keep clicking through points)
// Actually AutoCAD prompts for new start after each ray — reset base.
@@ -78,8 +78,9 @@ impl CadCommand for RayCommand {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
let base = self.base?;
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let pt = pt.as_vec3();
let base = self.base?.as_vec3();
let dir = (pt - base).normalize_or_zero();
let far = base + dir * DISPLAY_EXTENT;
Some(WireModel {
@@ -108,7 +109,7 @@ impl CadCommand for RayCommand {
// ── XLINE ─────────────────────────────────────────────────────────────────
pub struct XLineCommand {
base: Option<Vec3>,
base: Option<DVec3>,
}
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 {
let dir = pt - base;
let len = dir.length();
@@ -148,8 +149,8 @@ impl CadCommand for XLineCommand {
}
let dir_n = dir / len;
let xline = XLineEnt::new(
Vector3::new(base.x as f64, base.y as f64, base.z as f64),
Vector3::new(dir_n.x as f64, dir_n.y as f64, dir_n.z as f64),
Vector3::new(base.x, base.y, base.z),
Vector3::new(dir_n.x, dir_n.y, dir_n.z),
);
self.base = None;
CmdResult::CommitEntity(EntityType::XLine(xline))
@@ -167,8 +168,9 @@ impl CadCommand for XLineCommand {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
let base = self.base?;
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let pt = pt.as_vec3();
let base = self.base?.as_vec3();
let dir = (pt - base).normalize_or_zero();
let far_pos = base + dir * DISPLAY_EXTENT;
let far_neg = base - dir * DISPLAY_EXTENT;
+21 -12
View File
@@ -7,7 +7,7 @@
use acadrust::entities::LwPolyline;
use acadrust::types::Vector2;
use acadrust::{entities::LwVertex, EntityType};
use glam::{DVec3, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
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
pub struct RevCloudCommand {
points: Vec<Vec3>,
points: Vec<DVec3>,
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);
CmdResult::NeedPoint
}
@@ -72,13 +72,22 @@ impl CadCommand for RevCloudCommand {
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() {
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([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 {
text_verts: Vec::new(),
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 mut vertices: Vec<LwVertex> = Vec::new();
@@ -112,19 +121,19 @@ fn make_revcloud(pts: &[Vec3], arc_len: f64) -> EntityType {
for i in 0..n {
let p0 = pts[i];
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 {
continue;
}
// How many full arcs fit?
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_y = (p1.y - p0.y) as f64 / num_arcs as f64;
let step_x = (p1.x - p0.x) / num_arcs as f64;
let step_y = (p1.y - p0.y) / num_arcs as f64;
for j in 0..num_arcs {
let x = p0.x as f64 + step_x * j as f64;
let y = p0.y as f64 + step_y * j as f64; // DXF Y
let x = p0.x + step_x * j as f64;
let y = p0.y + step_y * j as f64; // DXF Y
let mut v = LwVertex::new(Vector2::new(x, y));
v.bulge = bump_bulge;
vertices.push(v);
+11 -8
View File
@@ -9,7 +9,7 @@ use acadrust::{EntityType, Spline};
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Vec3};
use glam::DVec3;
use truck_modeling::base::{BoundedCurve, ParametricCurve};
use truck_modeling::{BSplineCurve, KnotVec, Point3};
@@ -24,7 +24,7 @@ pub fn tool() -> ToolDef {
}
pub struct SplineCommand {
pts: Vec<Vec3>,
pts: Vec<DVec3>,
}
impl SplineCommand {
@@ -39,7 +39,7 @@ impl SplineCommand {
let control_points = self
.pts
.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();
let n = self.pts.len();
// 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
/// preview traces the exact final curve — not the straight control polygon.
/// 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();
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 ctrl: Vec<Point3> = pts
.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();
let curve = BSplineCurve::new(KnotVec::from(uniform_knots(n, degree)), ctrl);
let (t0, t1) = curve.range_tuple();
@@ -125,7 +128,7 @@ impl CadCommand for SplineCommand {
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);
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() {
return None;
}
+21 -17
View File
@@ -7,7 +7,7 @@
use acadrust::entities::Wipeout;
use acadrust::types::Vector3;
use acadrust::EntityType;
use glam::{DVec3, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
@@ -26,8 +26,8 @@ pub fn tool() -> ToolDef {
pub struct WipeoutCommand {
mode: WipeoutMode,
first: Option<Vec3>,
poly_pts: Vec<Vec3>,
first: Option<DVec3>,
poly_pts: Vec<DVec3>,
}
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 {
WipeoutMode::Rectangular => {
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();
match &self.mode {
WipeoutMode::Rectangular => {
let p1 = self.first?;
let p1 = self.first?.as_vec3();
let min = p1.min(pt);
let max = p1.max(pt);
Some(WireModel {
@@ -144,10 +144,17 @@ impl CadCommand for WipeoutCommand {
if self.poly_pts.is_empty() {
return None;
}
let mut pts: Vec<[f32; 3]> =
self.poly_pts.iter().map(|p| [p.x, p.y, p.z]).collect();
let mut pts: Vec<[f32; 3]> = self
.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([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 {
text_verts: Vec::new(),
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).
let c1 = Vector3::new(p1.x.min(p2.x) as f64, p1.y.min(p2.y) as f64, p1.z as f64);
let c2 = Vector3::new(p1.x.max(p2.x) as f64, p1.y.max(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), p1.y.max(p2.y), p1.z);
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;
let z = pts[0].z as f64;
let verts: Vec<Vector2> = pts
.iter()
.map(|p| Vector2::new(p.x as f64, p.y as f64))
.collect();
let z = pts[0].z;
let verts: Vec<Vector2> = pts.iter().map(|p| Vector2::new(p.x, p.y)).collect();
EntityType::Wipeout(Wipeout::polygonal(&verts, z))
}
+14 -12
View File
@@ -10,17 +10,17 @@
// With 2 pairs: translate + rotate (+ optional uniform scale to fit)
use acadrust::Handle;
use glam::{DVec3, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, EntityTransform};
pub struct AlignCommand {
state: AlignState,
handles: Vec<Handle>,
src1: Option<Vec3>,
dst1: Option<Vec3>,
src2: Option<Vec3>,
dst2: Option<Vec3>,
src1: Option<DVec3>,
dst1: Option<DVec3>,
src2: Option<DVec3>,
dst2: Option<DVec3>,
}
#[derive(PartialEq)]
@@ -74,7 +74,7 @@ impl CadCommand for AlignCommand {
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 {
AlignState::Gathering => CmdResult::NeedPoint,
AlignState::Src1 => {
@@ -117,7 +117,7 @@ impl CadCommand for AlignCommand {
let delta = d - s;
CmdResult::TransformSelected(
self.handles.clone(),
EntityTransform::Translate(delta.as_dvec3()),
EntityTransform::Translate(delta),
)
}
_ => CmdResult::Cancel,
@@ -163,7 +163,7 @@ impl AlignCommand {
let delta = d1 - s1;
return CmdResult::TransformSelected(
self.handles.clone(),
EntityTransform::Translate(delta.as_dvec3()),
EntityTransform::Translate(delta),
);
}
@@ -182,10 +182,12 @@ impl AlignCommand {
// Compose via AlignTransform CmdResult
CmdResult::AlignSelected {
handles: self.handles.clone(),
src1: s1.as_dvec3(),
dst1: d1.as_dvec3(),
angle_rad: angle,
scale: scale_factor,
src1: s1,
dst1: d1,
// Reference points (src1/dst1) stay f64 exact; the derived
// similarity scalars feed the f32 EntityTransform, so downcast here.
angle_rad: angle as f32,
scale: scale_factor as f32,
}
}
}
+47 -44
View File
@@ -14,7 +14,7 @@
// 1. Center point → 2. Item count (text) → 3. Total angle in degrees (text)
use acadrust::Handle;
use glam::{DVec3, Mat4, Vec3};
use glam::{DVec3, Mat4};
use crate::command::{CadCommand, CmdResult, EntityTransform};
use crate::modules::draw::defaults;
@@ -246,8 +246,8 @@ impl CadCommand for ArrayRectCommand {
enum PolarStep {
Center,
Count { center: Vec3 },
Angle { center: Vec3, count: u32 },
Count { center: DVec3 },
Angle { center: DVec3, count: u32 },
}
pub struct ArrayPolarCommand {
@@ -331,7 +331,6 @@ impl CadCommand for ArrayPolarCommand {
v
};
let step_rad = total_deg.to_radians() / count as f32;
let center = center.as_dvec3();
let transforms = (1..count)
.map(|n| EntityTransform::Rotate {
center,
@@ -348,8 +347,12 @@ impl CadCommand for ArrayPolarCommand {
let pt = pt.as_vec3();
let (center, count, total_deg) = match &self.step {
PolarStep::Center => (pt, self.default_count, self.default_angle),
PolarStep::Count { center } => (*center, self.default_count, self.default_angle),
PolarStep::Angle { center, count } => (*center, *count, self.default_angle),
PolarStep::Count { center } => {
(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 mut out: Vec<WireModel> = (1..count)
@@ -373,7 +376,6 @@ impl CadCommand for ArrayPolarCommand {
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
let pt = pt.as_vec3();
if let PolarStep::Center = self.step {
self.step = PolarStep::Count { center: pt };
}
@@ -398,8 +400,8 @@ impl CadCommand for ArrayPolarCommand {
// → Returns BatchCopy with Translate transforms derived from path samples.
use acadrust::EntityType;
use std::f32::consts::PI as FPI;
use std::f32::consts::TAU as FTAU;
use std::f64::consts::PI as FPI;
use std::f64::consts::TAU as FTAU;
// ── 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
/// 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.
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::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 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);
Vec3::new(wx as f32, wy as f32, wz as f32)
DVec3::new(wx, wy, wz)
};
let n = verts.len();
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 {
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.
fn subsample_equidistant(pts: &[Vec3], count: usize) -> Vec<Vec3> {
fn subsample_equidistant(pts: &[DVec3], count: usize) -> Vec<DVec3> {
if count == 0 {
return vec![];
}
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() {
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;
for i in 0..count {
let target = if count > 1 {
total * i as f32 / (count - 1) as f32
total * i as f64 / (count - 1) as f64
} else {
0.0
};
@@ -512,7 +514,7 @@ pub struct ArrayPathCommand {
/// 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
/// drawn, so without this the array could run opposite to expectation).
pick_pt: Vec3,
pick_pt: DVec3,
}
impl ArrayPathCommand {
@@ -527,13 +529,13 @@ impl ArrayPathCommand {
all_entities,
step: PathStep::SelectPath,
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
/// 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);
if pts.len() >= 2 {
let d_first = self.pick_pt.distance_squared(pts[0]);
@@ -546,16 +548,16 @@ impl ArrayPathCommand {
}
/// 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 {
return vec![];
}
match entity {
EntityType::Line(l) => {
let p0 = Vec3::new(l.start.x as f32, l.start.y as f32, 0.0);
let p1 = Vec3::new(l.end.x as f32, l.end.y as f32, 0.0);
let d = (count - 1).max(1) as f32;
(0..count).map(|i| p0.lerp(p1, i as f32 / d)).collect()
let p0 = DVec3::new(l.start.x, l.start.y, 0.0);
let p1 = DVec3::new(l.end.x, l.end.y, 0.0);
let d = (count - 1).max(1) as f64;
(0..count).map(|i| p0.lerp(p1, i as f64 / d)).collect()
}
EntityType::Arc(a) => {
// Sample in WCS via the arc's OCS basis so flipped-normal arcs
@@ -576,10 +578,10 @@ impl ArrayPathCommand {
.map(|i| {
let ang = sa + span * (i as f64 / d);
let (c, s) = (ang.cos(), ang.sin());
Vec3::new(
(cx + r * c * ax.0 + r * s * ay.0) as f32,
(cy + r * c * ax.1 + r * s * ay.1) as f32,
(cz + r * c * ax.2 + r * s * ay.2) as f32,
DVec3::new(
cx + r * c * ax.0 + r * s * ay.0,
cy + r * c * ax.1 + r * s * ay.1,
cz + r * c * ax.2 + r * s * ay.2,
)
})
.collect()
@@ -595,25 +597,25 @@ impl ArrayPathCommand {
.map(|i| {
let ang = i as f64 / count as f64 * std::f64::consts::TAU;
let (cs, sn) = (ang.cos(), ang.sin());
Vec3::new(
(cx + r * cs * ax.0 + r * sn * ay.0) as f32,
(cy + r * cs * ax.1 + r * sn * ay.1) as f32,
(cz + r * cs * ax.2 + r * sn * ay.2) as f32,
DVec3::new(
cx + r * cs * ax.0 + r * sn * ay.0,
cy + r * cs * ax.1 + r * sn * ay.1,
cz + r * cs * ax.2 + r * sn * ay.2,
)
})
.collect()
}
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
/// differences and unwrapped so the sequence stays smooth across an arc
/// that turns more than ±π in total.
fn tangents(pts: &[Vec3]) -> Vec<f32> {
fn tangents(pts: &[DVec3]) -> Vec<f64> {
let n = pts.len();
let mut a = vec![0.0f32; n];
let mut a = vec![0.0f64; n];
for i in 0..n {
let prev = if i > 0 { 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).
/// On a straight path the tangent never changes, so every copy is a pure
/// `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 {
return vec![];
}
@@ -658,14 +660,14 @@ impl ArrayPathCommand {
.map(|(i, &p)| {
let dth = tans[i] - t0;
if dth.abs() < 1e-5 {
EntityTransform::Translate((p - p0).as_dvec3())
EntityTransform::Translate(p - p0)
} else {
// The aligned copy is the rigid motion x' = p + R(x - p0),
// which is a pure rotation by `dth` about its fixed point.
let center = Self::rigid_center(p0, p, dth);
EntityTransform::Rotate {
center: center.as_dvec3(),
angle_rad: dth,
center,
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
/// 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();
// b = pi - R·p0
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]]
let one_c = 1.0 - c;
let det = 2.0 * one_c;
Vec3::new(
DVec3::new(
(one_c * bx - s * by) / det,
(s * bx + one_c * by) / det,
p0.z,
@@ -724,7 +726,6 @@ impl CadCommand for ArrayPathCommand {
}
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
let pt = pt.as_vec3();
if handle.is_null() || self.handles.contains(&handle) {
return CmdResult::NeedPoint;
}
@@ -758,7 +759,9 @@ impl CadCommand for ArrayPathCommand {
if pts.len() >= 2 {
return vec![WireModel::solid(
"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,
false,
)];
+46 -50
View File
@@ -15,7 +15,7 @@ use acadrust::entities::{
};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use glam::{DVec3, Vec3};
use glam::DVec3;
use truck_modeling::base::{BoundedCurve, Cut};
use crate::command::{CadCommand, CmdResult};
@@ -38,7 +38,7 @@ pub fn tool() -> ToolDef {
/// Break `entity` between world-space points `p1` and `p2`.
/// 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 {
EntityType::Line(line) => Some(break_line(line, 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> {
let s = Vec3::new(
line.start.x as f32,
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);
fn break_line(line: &LineEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let s = DVec3::new(line.start.x, line.start.y, line.start.z);
let e = DVec3::new(line.end.x, line.end.y, line.end.z);
let dir = e - s;
let len2 = dir.length_squared();
if len2 < 1e-12 {
@@ -94,17 +90,17 @@ fn break_line(line: &LineEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
result
}
fn break_arc(arc: &ArcEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
let cx = arc.center.x as f32;
let cy = arc.center.y as f32;
let r = arc.radius as f32;
fn break_arc(arc: &ArcEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let cx = arc.center.x;
let cy = arc.center.y;
let r = arc.radius;
// Project p1 and p2 onto the arc (world XY plane)
let a1 = angle_on_arc(cx, cy, p1);
let a2 = angle_on_arc(cx, cy, p2);
let start = arc.start_angle as f32;
let end = arc.end_angle as f32;
let start = arc.start_angle;
let end = arc.end_angle;
// Normalize: clamp a1 to arc range, then remove CCW from a1 to a2
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 mut result = arc.clone();
result.common.handle = Handle::NULL;
result.start_angle = a2_on as f64;
result.end_angle = a1_on as f64;
result.start_angle = a2_on;
result.end_angle = a1_on;
vec![EntityType::Arc(result)]
}
fn break_circle(circle: &acadrust::entities::Circle, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
let cx = circle.center.x as f32;
let cy = circle.center.y as f32;
fn break_circle(circle: &acadrust::entities::Circle, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let cx = circle.center.x;
let cy = circle.center.y;
let a1 = angle_on_arc(cx, cy, p1);
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.radius = circle.radius;
arc.normal = circle.normal.clone();
arc.start_angle = a2 as f64;
arc.end_angle = a1 as f64;
arc.start_angle = a2;
arc.end_angle = a1;
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,
// then split into two polylines at those vertices.
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.
// World = DXF (XY drawing plane).
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;
// Project a point onto the ellipse parameter (eccentric anomaly)
let param_of = |pt: Vec3| -> f64 {
let rx = pt.x as f64 - cx;
let ry = pt.y as f64 - cy;
let param_of = |pt: DVec3| -> f64 {
let rx = pt.x - cx;
let ry = pt.y - cy;
let xl = rx * nx + ry * ny;
let yl = -rx * ny + ry * nx;
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 ────────────────────────────────────────────────────────
/// 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 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`).
fn clamp_to_arc(a: f32, start: f32, end: f32) -> f32 {
let span = (end - start).rem_euclid(std::f32::consts::TAU);
let rel = (a - start).rem_euclid(std::f32::consts::TAU);
fn clamp_to_arc(a: f64, start: f64, end: f64) -> f64 {
let span = (end - start).rem_euclid(std::f64::consts::TAU);
let rel = (a - start).rem_euclid(std::f64::consts::TAU);
if rel <= span {
a
} else if rel < span + (std::f32::consts::TAU - span) / 2.0 {
} else if rel < span + (std::f64::consts::TAU - span) / 2.0 {
end
} else {
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`.
fn nearest_pline_param(p: &LwPolyline, pt: Vec3) -> usize {
fn nearest_pline_param(p: &LwPolyline, pt: DVec3) -> usize {
p.vertices
.iter()
.enumerate()
.min_by_key(|(_, v)| {
let dx = v.location.x as f32 - pt.x;
let dy = v.location.y as f32 - pt.y;
let dx = v.location.x - pt.x;
let dy = v.location.y - pt.y;
((dx * dx + dy * dy) * 1e6) as i64
})
.map(|(i, _)| i)
.unwrap_or(0)
}
fn world_to_dxf(v: Vec3) -> Vec3 {
fn world_to_dxf(v: DVec3) -> DVec3 {
// 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 {
Vector3::new(v.x as f64, v.y as f64, v.z as f64)
fn vec3_to_v3(v: DVec3) -> Vector3 {
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).
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,
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,
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.
pub struct BreakInteractiveCommand {
target: Option<Handle>,
p1: Option<Vec3>,
p1: Option<DVec3>,
}
impl BreakInteractiveCommand {
@@ -382,7 +378,7 @@ impl CadCommand for BreakInteractiveCommand {
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() {
return CmdResult::NeedPoint;
}
@@ -391,7 +387,7 @@ impl CadCommand for BreakInteractiveCommand {
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 {
Some(h) => h,
None => return CmdResult::Cancel,
@@ -403,7 +399,7 @@ impl CadCommand for BreakInteractiveCommand {
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 {
@@ -452,12 +448,12 @@ impl CadCommand for BreakAtPointCommand {
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 {
Some(handle) => CmdResult::BreakEntity {
handle,
p1: pt.as_dvec3(),
p2: pt.as_dvec3(),
p1: pt,
p2: pt,
},
None => CmdResult::Cancel,
}
+21 -16
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> {
let s = Vec3::new(line.start.x as f32, line.start.y as f32, 0.0);
let e = Vec3::new(line.end.x as f32, line.end.y as f32, 0.0);
let p = Vec3::new(pick_pt.x, pick_pt.y, 0.0);
// Keep the line's own coordinates on the f64 grid; only pick_pt (screen-only,
// nearest-end selection) widens for the distance compare.
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 {
return None;
}
@@ -173,23 +178,28 @@ fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Entity
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?
let dist_to_start = (p - s).length();
let dist_to_end = (p - e).length();
let px = pick_pt.x as f64;
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();
result.common.handle = Handle::NULL;
if dist_to_end <= dist_to_start {
// Extend/trim the end
let new_end = s + dir * new_len as f32;
result.end = xz_to_v3(new_end, line.end.z);
let new_x = sx + ux * new_len;
let new_y = sy + uy * new_len;
result.end = Vector3::new(new_x, new_y, line.end.z);
} else {
// Extend/trim the start (move start backward along dir)
let new_start = e - dir * new_len as f32;
result.start = xz_to_v3(new_start, line.start.z);
let new_x = ex - ux * new_len;
let new_y = ey - uy * new_len;
result.start = Vector3::new(new_x, new_y, line.start.z);
}
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> {
let n = poly.vertices.len();
if n < 2 {
+19 -15
View File
@@ -16,7 +16,7 @@
// All others : move the whole entity if any point is inside.
use acadrust::Handle;
use glam::{DVec3, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
@@ -39,14 +39,14 @@ enum Step {
/// Waiting for the first crossing-window corner.
WindowCorner1,
/// Waiting for the second corner; `c1` is the first corner.
WindowCorner2(Vec3),
WindowCorner2(DVec3),
/// 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.
Target {
win_min: Vec3,
win_max: Vec3,
base: Vec3,
win_min: DVec3,
win_max: DVec3,
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 {
Step::WindowCorner1 => {
self.step = Step::WindowCorner2(pt);
@@ -115,9 +115,9 @@ impl CadCommand for StretchCommand {
let delta = pt - *base;
CmdResult::StretchEntities {
handles: self.handles.clone(),
win_min: win_min.as_dvec3(),
win_max: win_max.as_dvec3(),
delta: delta.as_dvec3(),
win_min: *win_min,
win_max: *win_max,
delta,
}
}
}
@@ -140,12 +140,12 @@ impl CadCommand for StretchCommand {
// 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.
match &self.step {
Step::WindowCorner2(c1) => Some(c1.as_dvec3()),
Step::WindowCorner2(c1) => Some(*c1),
_ => 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 {
// The crossing-window rectangle is drawn as a filled selection
// marquee by the host (via window_first_corner) so it matches a
@@ -158,15 +158,19 @@ impl CadCommand for StretchCommand {
} => {
let delta = pt - *base;
// 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
.wire_models
.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();
out.push(WireModel::solid(
"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,
false,
));
+7 -5
View File
@@ -6,7 +6,7 @@
// Step 2: If first point picked, pick second point to define angle vector.
use acadrust::Handle;
use glam::{DVec3, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, DynField};
use crate::scene::model::wire_model::WireModel;
@@ -16,7 +16,7 @@ use acadrust::EntityType;
enum Step {
AngleOrFirstPoint,
SecondPoint { first_point: Vec3 },
SecondPoint { first_point: DVec3 },
}
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 {
Step::AngleOrFirstPoint => {
self.step = Step::SecondPoint { first_point: pt };
@@ -131,7 +131,7 @@ impl CadCommand for TorientCommand {
Step::SecondPoint { first_point } => {
let dx = pt.x - first_point.x;
let dy = pt.y - first_point.y;
let angle = dy.atan2(dx) as f64;
let angle = dy.atan2(dx);
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 {
let first_point = first_point.as_vec3();
vec![WireModel::solid(
"rubber_band".into(),
vec![[first_point.x, first_point.y, first_point.z], [pt.x, pt.y, pt.z]],