wip(f64): convert command layer to DVec3 (in progress)

Migrate the command/snap/grip/cursor chain from f32 Vec3 to f64 DVec3 so
picked points, snaps and grip drags stay precise at UTM-scale coordinates
(f32 quantizes to ~0.5 m there).

Done so far:
- command/mod.rs: EntityTransform, CmdResult points, CadCommand point
  methods, TangentObject already DVec3 (radius f64).
- snap: SnapResult.world DVec3; world_to_screen/try_pt DVec3.
- GripApply -> DVec3; grip find/state (GripEdit) f64; camera
  pick_on_plane_f64 / pick_on_target_plane_f64; scene paper_to_model_f64.
- update.rs click + mouse-move + grip-drag pipelines route precise DVec3
  to on_point/on_preview_wires/on_entity_pick; ortho/polar cast at the
  UcsXform boundary only.
- cmd_result dispatch, transform helpers, dimension/arc/circle/multileader
  apply_grip, circle/arc/ellipse/shapes draw commands, array modify.

Tree does NOT compile yet (~236 errors remain across modify/annotate/
insert command files + entity apply_grip impls); committed at user request
to checkpoint progress.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-24 03:32:11 +03:00
commit 3da71698b1
18 changed files with 626 additions and 587 deletions

View file

@ -210,7 +210,7 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "BLOCK"); self.push_undo_snapshot(i, "BLOCK");
match self.tabs[i] match self.tabs[i]
.scene .scene
.create_block_from_entities(&handles, &name, base) .create_block_from_entities(&handles, &name, base.as_vec3())
{ {
Ok(insert_handle) => { Ok(insert_handle) => {
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
@ -611,12 +611,13 @@ impl OpenCADStudio {
// offset gap. Zero for a same-drawing paste. (#135) // offset gap. Zero for a same-drawing paste. (#135)
let src_wo = self.clipboard_world_offset; let src_wo = self.clipboard_world_offset;
let tgt_wo = [0.0_f64; 3]; let tgt_wo = [0.0_f64; 3];
let wo_corr = glam::Vec3::new( let wo_corr = glam::DVec3::new(
(tgt_wo[0] - src_wo[0]) as f32, tgt_wo[0] - src_wo[0],
(tgt_wo[1] - src_wo[1]) as f32, tgt_wo[1] - src_wo[1],
(tgt_wo[2] - src_wo[2]) as f32, tgt_wo[2] - src_wo[2],
); );
let delta = base_pt - self.clipboard_centroid + wo_corr; let delta =
base_pt - self.clipboard_centroid.as_dvec3() + wo_corr;
let translate = crate::command::EntityTransform::Translate(delta); let translate = crate::command::EntityTransform::Translate(delta);
self.push_undo_snapshot(i, "PASTECLIP"); self.push_undo_snapshot(i, "PASTECLIP");
// Recreate any layer / linetype / style the copied entities // Recreate any layer / linetype / style the copied entities
@ -783,7 +784,7 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
self.tabs[i].scene.zoom_to_window(p1, p2); self.tabs[i].scene.zoom_to_window(p1.as_vec3(), p2.as_vec3());
self.command_line.push_output("Zoom Window"); self.command_line.push_output("Zoom Window");
} }
CmdResult::Measurement(msg) => { CmdResult::Measurement(msg) => {
@ -818,7 +819,7 @@ impl OpenCADStudio {
self.tabs[i].scene.transform_entities( self.tabs[i].scene.transform_entities(
&handles, &handles,
&crate::command::EntityTransform::Scale { &crate::command::EntityTransform::Scale {
center: glam::Vec3::ZERO, center: glam::DVec3::ZERO,
factor: scale, factor: scale,
}, },
); );
@ -828,7 +829,7 @@ impl OpenCADStudio {
self.tabs[i].scene.transform_entities( self.tabs[i].scene.transform_entities(
&handles, &handles,
&crate::command::EntityTransform::Rotate { &crate::command::EntityTransform::Rotate {
center: glam::Vec3::ZERO, center: glam::DVec3::ZERO,
angle_rad, angle_rad,
}, },
); );
@ -861,7 +862,7 @@ impl OpenCADStudio {
.scene .scene
.document .document
.get_entity(handle) .get_entity(handle)
.and_then(|e| lengthen_entity(e, pick_pt, &mode)); .and_then(|e| lengthen_entity(e, pick_pt.as_vec3(), &mode));
match result { match result {
Some(new_entity) => { Some(new_entity) => {
let label = self.history_label_from_active_cmd(i, "LENGTHEN"); let label = self.history_label_from_active_cmd(i, "LENGTHEN");
@ -1001,7 +1002,7 @@ impl OpenCADStudio {
.scene .scene
.document .document
.get_entity(handle) .get_entity(handle)
.and_then(|e| break_entity(e, p1, p2)); .and_then(|e| break_entity(e, p1.as_vec3(), p2.as_vec3()));
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");
@ -1091,9 +1092,7 @@ impl OpenCADStudio {
// Helper: is DXF point (x, y) inside the world-space window? // Helper: is DXF point (x, y) inside the world-space window?
// Drawing plane is world XY (= DXF XY). // Drawing plane is world XY (= DXF XY).
let in_win = |x: f64, y: f64| -> bool { let in_win = |x: f64, y: f64| -> bool {
let wx = x as f32; x >= win_min.x && x <= win_max.x && y >= win_min.y && y <= win_max.y
let wy = y as f32;
wx >= win_min.x && wx <= win_max.x && wy >= win_min.y && wy <= win_max.y
}; };
let dx = delta.x as f64; let dx = delta.x as f64;
@ -1687,7 +1686,7 @@ impl OpenCADStudio {
} => { } => {
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.open_mtext_editor(pos, handle, &initial, height); self.open_mtext_editor(pos.as_vec3(), handle, &initial, height);
} }
CmdResult::OpenTextEditor { CmdResult::OpenTextEditor {
pos, pos,
@ -1698,7 +1697,7 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.open_text_inline( self.open_text_inline(
pos, pos.as_vec3(),
handle, handle,
&initial, &initial,
height, height,

View file

@ -1688,7 +1688,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)); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt.as_dvec3()));
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);
@ -1730,7 +1730,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(wcs_pt)); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt.as_dvec3()));
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
@ -2617,10 +2617,10 @@ impl OpenCADStudio {
height: vh, height: vh,
}; };
let cam = self.tabs[i].scene.camera.borrow(); let cam = self.tabs[i].scene.camera.borrow();
let raw_paper = cam.pick_on_target_plane(p, bounds); let raw_paper = cam.pick_on_target_plane_f64(p, bounds);
let view_rot = cam.view_proj_rte(bounds); let eye = cam.eye_f64(); let view_rot = cam.view_proj_rte(bounds); let eye = cam.eye_f64();
drop(cam); drop(cam);
let raw = self.tabs[i].scene.paper_to_model(raw_paper); let raw = self.tabs[i].scene.paper_to_model_f64(raw_paper);
// First move of this drag: hide the edited entity from the // First move of this drag: hide the edited entity from the
// base tessellation (one re-tess) so subsequent moves only // base tessellation (one re-tess) so subsequent moves only
@ -2648,7 +2648,7 @@ impl OpenCADStudio {
self.snapper.from_point = None; self.snapper.from_point = None;
let (go, gr) = self.tabs[i].ucs_grid_basis(); let (go, gr) = self.tabs[i].ucs_grid_basis();
let snap_hit = let snap_hit =
self.snapper.snap(raw, p, &all_wires[..], view_rot, eye, bounds, go, gr); self.snapper.snap(raw.as_vec3(), p, &all_wires[..], view_rot, eye, bounds, go, gr);
let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw); let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw);
self.tabs[i].snap_result = snap_hit; self.tabs[i].snap_result = snap_hit;
if let Some(s) = self.tabs[i].snap_result.as_mut() { if let Some(s) = self.tabs[i].snap_result.as_mut() {
@ -2657,13 +2657,13 @@ impl OpenCADStudio {
} }
if snap_hit.is_none() { if snap_hit.is_none() {
let base = grip.origin_world; let base = grip.origin_world.as_vec3();
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, base, &ucs_xf); snapped = ortho_constrain(snapped.as_vec3(), base, &ucs_xf).as_dvec3();
} else if self.polar_mode { } else if self.polar_mode {
snapped = polar_constrain_near( snapped = polar_constrain_near(
snapped, snapped.as_vec3(),
base, base,
self.polar_increment_deg, self.polar_increment_deg,
view_rot, view_rot,
@ -2671,7 +2671,8 @@ impl OpenCADStudio {
bounds, bounds,
self.snapper.osnap_radius_px, self.snapper.osnap_radius_px,
&ucs_xf, &ucs_xf,
); )
.as_dvec3();
} }
} }
@ -2679,9 +2680,9 @@ impl OpenCADStudio {
// Only add world_offset when converting local → DXF space in model space. // Only add world_offset when converting local → DXF space in model space.
let wo_vec = if self.tabs[i].scene.current_layout == "Model" { let wo_vec = if self.tabs[i].scene.current_layout == "Model" {
let wo = [0.0_f64; 3]; let wo = [0.0_f64; 3];
glam::Vec3::new(wo[0] as f32, wo[1] as f32, wo[2] as f32) glam::DVec3::new(wo[0], wo[1], wo[2])
} else { } else {
glam::Vec3::ZERO glam::DVec3::ZERO
}; };
let apply = if grip.is_translate { let apply = if grip.is_translate {
GripApply::Translate(snapped - grip.last_world) GripApply::Translate(snapped - grip.last_world)
@ -2765,23 +2766,19 @@ impl OpenCADStudio {
height: vh, height: vh,
}; };
let cursor_paper = if let Some(ref ucs) = self.tabs[i].active_ucs { let cursor_paper = if let Some(ref ucs) = self.tabs[i].active_ucs {
let origin = glam::Vec3::new( let origin = glam::DVec3::new(ucs.origin.x, ucs.origin.y, ucs.origin.z);
ucs.origin.x as f32,
ucs.origin.y as f32,
ucs.origin.z as f32,
);
let normal = ucs_z_axis(ucs); let normal = ucs_z_axis(ucs);
self.tabs[i] self.tabs[i]
.scene .scene
.camera .camera
.borrow() .borrow()
.pick_on_plane(p, bounds, normal, origin) .pick_on_plane_f64(p, bounds, normal, origin)
} else { } else {
self.tabs[i] self.tabs[i]
.scene .scene
.camera .camera
.borrow() .borrow()
.pick_on_target_plane(p, bounds) .pick_on_target_plane_f64(p, bounds)
}; };
let (view_rot, eye) = { let (view_rot, eye) = {
let cam = self.tabs[i].scene.camera.borrow(); let cam = self.tabs[i].scene.camera.borrow();
@ -2792,7 +2789,7 @@ impl OpenCADStudio {
crate::ui::overlay::compute_grid_step(view_rot, bounds); crate::ui::overlay::compute_grid_step(view_rot, bounds);
// In MSPACE, map paper-space cursor to model space so that // In MSPACE, map paper-space cursor to model space so that
// command previews and snapping work in the correct coordinate space. // command previews and snapping work in the correct coordinate space.
let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper); let cursor_world = self.tabs[i].scene.paper_to_model_f64(cursor_paper);
let all_wires = self.tabs[i].scene.hit_test_wires(); let all_wires = self.tabs[i].scene.hit_test_wires();
let needs_entity = self.tabs[i] let needs_entity = self.tabs[i]
@ -2819,7 +2816,7 @@ impl OpenCADStudio {
None None
} else if needs_tan { } else if needs_tan {
self.snapper.snap_tangent_only( self.snapper.snap_tangent_only(
cursor_world, cursor_world.as_vec3(),
p, p,
&all_wires[..], &all_wires[..],
view_rot, view_rot,
@ -2830,14 +2827,14 @@ impl OpenCADStudio {
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;
self.snapper self.snapper
.snap(cursor_world, p, &all_wires[..], view_rot, eye, bounds, go, gr) .snap(cursor_world.as_vec3(), p, &all_wires[..], view_rot, eye, bounds, go, gr)
}; };
// Object Snap Tracking: update dwell, then align the cursor // Object Snap Tracking: update dwell, then align the cursor
// to a tracking ray (and store the alignment so a typed // to a tracking ray (and store the alignment so a typed
// distance can place a point along it — issue #69). // distance can place a point along it — issue #69).
let otrack_hit = { let otrack_hit = {
let snap_world = self.tabs[i].snap_result.map(|s| s.world); let snap_world = self.tabs[i].snap_result.map(|s| s.world.as_vec3());
self.snapper.update_otrack_dwell( self.snapper.update_otrack_dwell(
snap_world, snap_world,
view_rot, view_rot,
@ -2853,7 +2850,7 @@ impl OpenCADStudio {
}; };
let ucs = self.tabs[i].scene.viewcube_ucs_mat(); let ucs = self.tabs[i].scene.viewcube_ucs_mat();
self.snapper.otrack_snap( self.snapper.otrack_snap(
cursor_world, cursor_world.as_vec3(),
view_rot, view_rot,
eye, eye,
bounds, bounds,
@ -2868,16 +2865,16 @@ impl OpenCADStudio {
self.otrack_active = otrack_hit.map(|h| (h.base, h.dir)); self.otrack_active = otrack_hit.map(|h| (h.base, h.dir));
let effective = { let effective = {
let mut pt = if let Some(h) = otrack_hit { let mut pt: glam::DVec3 = if let Some(h) = otrack_hit {
// Tracking alignment wins over the free cursor; // Tracking alignment wins over the free cursor;
// ortho/polar don't re-constrain it. // ortho/polar don't re-constrain it.
h.aligned h.aligned.as_dvec3()
} else { } else {
// snap.world is paper-space for viewport-projected // snap.world is paper-space for viewport-projected
// wires; convert to model-space for previews. // wires; convert to model-space for previews.
let mut pt = self.tabs[i] let mut pt = self.tabs[i]
.snap_result .snap_result
.map(|s| self.tabs[i].scene.paper_to_model(s.world)) .map(|s| self.tabs[i].scene.paper_to_model_f64(s.world))
.unwrap_or(cursor_world); .unwrap_or(cursor_world);
// Object snap wins over ortho/polar: a snapped point // Object snap wins over ortho/polar: a snapped point
// is taken as-is so it isn't pulled onto the ortho // is taken as-is so it isn't pulled onto the ortho
@ -2890,10 +2887,11 @@ impl OpenCADStudio {
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, base, &ucs_xf); pt = ortho_constrain(pt.as_vec3(), base, &ucs_xf)
.as_dvec3();
} else if self.polar_mode { } else if self.polar_mode {
pt = polar_constrain_near( pt = polar_constrain_near(
pt, pt.as_vec3(),
base, base,
self.polar_increment_deg, self.polar_increment_deg,
view_rot, view_rot,
@ -2901,7 +2899,8 @@ impl OpenCADStudio {
bounds, bounds,
self.snapper.osnap_radius_px, self.snapper.osnap_radius_px,
&ucs_xf, &ucs_xf,
); )
.as_dvec3();
} }
} }
} }
@ -2914,7 +2913,7 @@ impl OpenCADStudio {
} }
pt pt
}; };
self.tabs[i].last_cursor_world = effective; self.tabs[i].last_cursor_world = effective.as_vec3();
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
@ -2941,7 +2940,7 @@ impl OpenCADStudio {
} else { } else {
[0.0; 3] [0.0; 3]
}; };
let wo_f = glam::Vec3::new(wo_local[0] as f32, wo_local[1] as f32, wo_local[2] as f32); let wo_f = glam::DVec3::new(wo_local[0], wo_local[1], wo_local[2]);
let effective_wcs = effective + wo_f; let effective_wcs = effective + wo_f;
// Orange object snap (plugin commands implement resolve_object_pick). // Orange object snap (plugin commands implement resolve_object_pick).
@ -2955,8 +2954,8 @@ impl OpenCADStudio {
) )
}); });
if let Some(pick) = pick { if let Some(pick) = pick {
let world = glam::Vec3::new(pick.x as f32, pick.y as f32, effective.z); let world = glam::DVec3::new(pick.x, pick.y, effective.z);
let ndc = view_rot.project_point3((world.as_dvec3() - eye).as_vec3()); let ndc = view_rot.project_point3((world - eye).as_vec3());
let screen = iced::Point::new( let screen = iced::Point::new(
(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,
@ -3008,12 +3007,12 @@ impl OpenCADStudio {
// on_hover_entity returns WCS wires; shift to the // on_hover_entity returns WCS wires; shift to the
// offset-relative frame so the preview lands on the // offset-relative frame so the preview lands on the
// geometry on large-coordinate drawings. // geometry on large-coordinate drawings.
if wo_f != glam::Vec3::ZERO { if wo_f != glam::DVec3::ZERO {
for w in p.iter_mut() { for w in p.iter_mut() {
for pt in w.points.iter_mut() { for pt in w.points.iter_mut() {
pt[0] -= wo_f.x; pt[0] -= wo_f.x as f32;
pt[1] -= wo_f.y; pt[1] -= wo_f.y as f32;
pt[2] -= wo_f.z; pt[2] -= wo_f.z as f32;
} }
} }
} }
@ -3042,6 +3041,7 @@ impl OpenCADStudio {
// 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 {
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;
// Only show the guide while POLAR is actually engaged // Only show the guide while POLAR is actually engaged
@ -3428,26 +3428,22 @@ impl OpenCADStudio {
// Project screen point onto the active UCS XY plane (or world XY when // Project screen point onto the active UCS XY plane (or world XY when
// no UCS is active). // no UCS is active).
let raw_paper = if let Some(ref ucs) = self.tabs[i].active_ucs { let raw_paper = if let Some(ref ucs) = self.tabs[i].active_ucs {
let origin = glam::Vec3::new( let origin = glam::DVec3::new(ucs.origin.x, ucs.origin.y, ucs.origin.z);
ucs.origin.x as f32,
ucs.origin.y as f32,
ucs.origin.z as f32,
);
let normal = ucs_z_axis(ucs); let normal = ucs_z_axis(ucs);
self.tabs[i] self.tabs[i]
.scene .scene
.camera .camera
.borrow() .borrow()
.pick_on_plane(p, bounds, normal, origin) .pick_on_plane_f64(p, bounds, normal, origin)
} else { } else {
self.tabs[i] self.tabs[i]
.scene .scene
.camera .camera
.borrow() .borrow()
.pick_on_target_plane(p, bounds) .pick_on_target_plane_f64(p, bounds)
}; };
// Convert paper-space → model-space when inside a viewport. // Convert paper-space → model-space when inside a viewport.
let raw = self.tabs[i].scene.paper_to_model(raw_paper); let raw = self.tabs[i].scene.paper_to_model_f64(raw_paper);
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) }; let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
let all_wires = self.tabs[i].scene.hit_test_wires(); let all_wires = self.tabs[i].scene.hit_test_wires();
let needs_tan = self.tabs[i] let needs_tan = self.tabs[i]
@ -3464,16 +3460,16 @@ impl OpenCADStudio {
None None
} else if needs_tan { } else if needs_tan {
self.snapper self.snapper
.snap_tangent_only(raw, p, &all_wires[..], view_rot, eye, bounds) .snap_tangent_only(raw.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;
self.snapper.snap(raw, p, &all_wires[..], view_rot, eye, bounds, go, gr) self.snapper.snap(raw.as_vec3(), p, &all_wires[..], view_rot, eye, bounds, go, gr)
}; };
// snap.world is in paper-space (projected wire coords in MSPACE); // snap.world is in paper-space (projected wire coords in MSPACE);
// convert to model-space so commands receive consistent coordinates. // convert to model-space so commands receive consistent coordinates.
let mut pt = snap_hit let mut pt = snap_hit
.map(|s| self.tabs[i].scene.paper_to_model(s.world)) .map(|s| self.tabs[i].scene.paper_to_model_f64(s.world))
.unwrap_or(raw); .unwrap_or(raw);
// When no UCS is active clamp to world XY; with a UCS the point is // When no UCS is active clamp to world XY; with a UCS the point is
// already constrained to that plane by the ray–plane intersection. // already constrained to that plane by the ray–plane intersection.
@ -3490,12 +3486,12 @@ impl OpenCADStudio {
}; };
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, view_rot, eye, bounds, step, self.last_point, ucs) .otrack_snap(raw.as_vec3(), view_rot, eye, bounds, step, self.last_point, ucs)
} else { } else {
None None
}; };
if let Some(h) = otrack { if let Some(h) = otrack {
pt = h.aligned; pt = h.aligned.as_dvec3();
if self.tabs[i].active_ucs.is_none() { if self.tabs[i].active_ucs.is_none() {
pt.z = 0.0; pt.z = 0.0;
} }
@ -3507,10 +3503,10 @@ impl OpenCADStudio {
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, base, &ucs_xf); pt = ortho_constrain(pt.as_vec3(), base, &ucs_xf).as_dvec3();
} else if self.polar_mode { } else if self.polar_mode {
pt = polar_constrain_near( pt = polar_constrain_near(
pt, pt.as_vec3(),
base, base,
self.polar_increment_deg, self.polar_increment_deg,
view_rot, view_rot,
@ -3518,7 +3514,8 @@ impl OpenCADStudio {
bounds, bounds,
self.snapper.osnap_radius_px, self.snapper.osnap_radius_px,
&ucs_xf, &ucs_xf,
); )
.as_dvec3();
} }
} }
} }
@ -3538,7 +3535,7 @@ impl OpenCADStudio {
} else { } else {
[0.0; 3] [0.0; 3]
}; };
world_pt + glam::Vec3::new(wo[0] as f32, wo[1] as f32, wo[2] as f32) world_pt + glam::DVec3::new(wo[0], wo[1], wo[2])
}; };
let result = if self.tabs[i] let result = if self.tabs[i]
@ -3555,7 +3552,7 @@ impl OpenCADStudio {
) )
}); });
if let Some(pick) = pick { if let Some(pick) = pick {
let center = glam::Vec3::new(pick.x as f32, pick.y as f32, pick_wcs.z); let center = glam::DVec3::new(pick.x, pick.y, pick_wcs.z);
let result = self.tabs[i] let result = self.tabs[i]
.active_cmd .active_cmd
.as_mut() .as_mut()
@ -3701,7 +3698,7 @@ impl OpenCADStudio {
self.tabs[i].dyn_active = 0; self.tabs[i].dyn_active = 0;
} }
} }
self.last_point = Some(world_pt); self.last_point = Some(world_pt.as_vec3());
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();
@ -4510,8 +4507,8 @@ impl OpenCADStudio {
handle: popup.handle, handle: popup.handle,
grip_id, grip_id,
is_translate, is_translate,
origin_world: g.world.as_vec3(), origin_world: g.world,
last_world: g.world.as_vec3(), last_world: g.world,
}); });
} }
return Task::none(); return Task::none();
@ -4572,8 +4569,8 @@ impl OpenCADStudio {
handle: popup.handle, handle: popup.handle,
grip_id: new_gid, grip_id: new_gid,
is_translate: false, is_translate: false,
origin_world: g.world.as_vec3(), origin_world: g.world,
last_world: g.world.as_vec3(), last_world: g.world,
}); });
self.grip_add_provisional = Some((popup.handle, new_gid)); self.grip_add_provisional = Some((popup.handle, new_gid));
} }
@ -8215,9 +8212,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), crate::command::DynAnchor::Point(p) => Some(p.as_vec3()),
}; };
self.tabs[i].dyn_ref = spec.ref_point; self.tabs[i].dyn_ref = spec.ref_point.map(|v| v.as_vec3());
} }
/// Track cursor dwell over a selected entity's grip. Sets /// Track cursor dwell over a selected entity's grip. Sets
@ -8368,7 +8365,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)) .map(|c| c.on_preview_wires(cur.as_dvec3()))
.unwrap_or_default(); .unwrap_or_default();
self.tabs[i].scene.set_preview_wires(previews); self.tabs[i].scene.set_preview_wires(previews);
} }
@ -8647,7 +8644,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)); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt.as_dvec3()));
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);
@ -8706,7 +8703,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)); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt.as_dvec3()));
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;
} }

View file

@ -9,7 +9,7 @@ use crate::scene::model::hatch_model::HatchModel;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use crate::scene::Scene; use crate::scene::Scene;
use acadrust::{EntityType, Handle}; use acadrust::{EntityType, Handle};
use glam::Vec3; use glam::DVec3;
/// Domain object resolved under the cursor for ObjectPick snapping. /// Domain object resolved under the cursor for ObjectPick snapping.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -26,13 +26,13 @@ pub struct ObjectPickHit {
#[derive(Clone)] #[derive(Clone)]
pub enum EntityTransform { pub enum EntityTransform {
/// Move every point by the given world-space delta (world XY plane). /// Move every point by the given world-space delta (world XY plane).
Translate(Vec3), Translate(DVec3),
/// Rotate around `center` by `angle_rad` in the world XY plane. /// Rotate around `center` by `angle_rad` in the world XY plane.
Rotate { center: Vec3, angle_rad: f32 }, Rotate { center: DVec3, angle_rad: f32 },
/// Uniform scale from `center` by `factor`. /// Uniform scale from `center` by `factor`.
Scale { center: Vec3, factor: f32 }, Scale { center: DVec3, factor: f32 },
/// Mirror across the line through `p1`→`p2` in the world XY plane. /// Mirror across the line through `p1`→`p2` in the world XY plane.
Mirror { p1: Vec3, p2: Vec3 }, Mirror { p1: DVec3, p2: DVec3 },
} }
// ── Tangent object ───────────────────────────────────────────────────────── // ── Tangent object ─────────────────────────────────────────────────────────
@ -41,9 +41,9 @@ pub enum EntityTransform {
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum TangentObject { pub enum TangentObject {
/// Infinite line through two world-space XZ-plane points. /// Infinite line through two world-space XZ-plane points.
Line { p1: Vec3, p2: Vec3 }, Line { p1: DVec3, p2: DVec3 },
/// Circle in the world XY plane. /// Circle in the world XY plane.
Circle { center: Vec3, radius: f32 }, Circle { center: DVec3, radius: f64 },
} }
// ── Result token ────────────────────────────────────────────────────────── // ── Result token ──────────────────────────────────────────────────────────
@ -82,7 +82,7 @@ pub enum CmdResult {
CreateBlock { CreateBlock {
handles: Vec<Handle>, handles: Vec<Handle>,
name: String, name: String,
base: Vec3, base: DVec3,
}, },
/// Apply a transform to selected entities and end the command. /// Apply a transform to selected entities and end the command.
TransformSelected(Vec<Handle>, EntityTransform), TransformSelected(Vec<Handle>, EntityTransform),
@ -117,13 +117,13 @@ pub enum CmdResult {
thaw: Vec<String>, thaw: Vec<String>,
}, },
/// Paste clipboard entities translated so their centroid lands at `base_pt`; end command. /// Paste clipboard entities translated so their centroid lands at `base_pt`; end command.
PasteClipboard { base_pt: Vec3 }, PasteClipboard { base_pt: DVec3 },
/// Zoom the model-space camera to fit the given corner points; end command. /// Zoom the model-space camera to fit the given corner points; end command.
ZoomToWindow { p1: Vec3, p2: Vec3 }, ZoomToWindow { p1: DVec3, p2: DVec3 },
/// Print a measurement result to the command line and end the command. /// Print a measurement result to the command line and end the command.
Measurement(String), Measurement(String),
/// Break `handle` at points `p1` and `p2`; replace with computed fragments. /// Break `handle` at points `p1` and `p2`; replace with computed fragments.
BreakEntity { handle: Handle, p1: Vec3, p2: Vec3 }, BreakEntity { handle: Handle, p1: DVec3, p2: DVec3 },
/// Attempt to join the given entities into fewer merged entities. /// Attempt to join the given entities into fewer merged entities.
JoinEntities(Vec<Handle>), JoinEntities(Vec<Handle>),
/// Apply a polyline-edit operation to one entity; keep command active. /// Apply a polyline-edit operation to one entity; keep command active.
@ -138,19 +138,19 @@ pub enum CmdResult {
/// Extend/trim a Line or Arc by the given mode; end command. /// Extend/trim a Line or Arc by the given mode; end command.
LengthenEntity { LengthenEntity {
handle: Handle, handle: Handle,
pick_pt: Vec3, pick_pt: DVec3,
mode: crate::modules::draw::modify::lengthen::LenMode, mode: crate::modules::draw::modify::lengthen::LenMode,
}, },
/// Align selected entities: translate to dst1, rotate by angle_rad, optional scale. /// Align selected entities: translate to dst1, rotate by angle_rad, optional scale.
AlignSelected { AlignSelected {
handles: Vec<Handle>, handles: Vec<Handle>,
src1: Vec3, src1: DVec3,
dst1: Vec3, dst1: DVec3,
angle_rad: f32, angle_rad: f32,
scale: f32, scale: f32,
}, },
/// Set the plot window on the active layout's PlotSettings. /// Set the plot window on the active layout's PlotSettings.
SetPlotWindow { p1: Vec3, p2: Vec3 }, SetPlotWindow { p1: DVec3, p2: DVec3 },
/// Replace the text content of a Text/MText entity in-place. /// Replace the text content of a Text/MText entity in-place.
DdeditEntity { handle: Handle, new_text: String }, DdeditEntity { handle: Handle, new_text: String },
/// Open the in-place editor (plain box or rich MText editor, per type) for /// Open the in-place editor (plain box or rich MText editor, per type) for
@ -160,7 +160,7 @@ pub enum CmdResult {
/// area with live viewport preview). `handle` is `Some` when editing an /// area with live viewport preview). `handle` is `Some` when editing an
/// existing MText, `None` when creating a new one at `pos`. /// existing MText, `None` when creating a new one at `pos`.
OpenMTextEditor { OpenMTextEditor {
pos: Vec3, pos: DVec3,
handle: Option<Handle>, handle: Option<Handle>,
initial: String, initial: String,
height: f64, height: f64,
@ -169,7 +169,7 @@ pub enum CmdResult {
/// formatting toolbar). `handle` is `Some` when editing an existing Text, /// formatting toolbar). `handle` is `Some` when editing an existing Text,
/// `None` when creating a new one at `pos`. /// `None` when creating a new one at `pos`.
OpenTextEditor { OpenTextEditor {
pos: Vec3, pos: DVec3,
handle: Option<Handle>, handle: Option<Handle>,
initial: String, initial: String,
height: f64, height: f64,
@ -185,11 +185,11 @@ pub enum CmdResult {
StretchEntities { StretchEntities {
handles: Vec<Handle>, handles: Vec<Handle>,
/// Min corner of the crossing window in world XZ (= DXF XY). /// Min corner of the crossing window in world XZ (= DXF XY).
win_min: Vec3, win_min: DVec3,
/// Max corner of the crossing window in world XZ (= DXF XY). /// Max corner of the crossing window in world XZ (= DXF XY).
win_max: Vec3, win_max: DVec3,
/// Translation vector to apply to vertices inside the window. /// Translation vector to apply to vertices inside the window.
delta: Vec3, delta: DVec3,
}, },
/// Create a Solid3D placeholder entity + associated MeshModel. /// Create a Solid3D placeholder entity + associated MeshModel.
/// `mesh_fn` is called with the entity's handle string to build the mesh. /// `mesh_fn` is called with the entity's handle string to build the mesh.
@ -205,8 +205,8 @@ pub enum CmdResult {
/// Revolve the profile entity `handle` around the given axis by `angle_deg`. /// Revolve the profile entity `handle` around the given axis by `angle_deg`.
RevolveEntity { RevolveEntity {
handle: Handle, handle: Handle,
axis_start: glam::Vec3, axis_start: glam::DVec3,
axis_end: glam::Vec3, axis_end: glam::DVec3,
angle_deg: f32, angle_deg: f32,
color: [f32; 4], color: [f32; 4],
}, },
@ -377,7 +377,7 @@ pub enum DynAnchor {
#[allow(dead_code)] #[allow(dead_code)]
LastPoint, LastPoint,
/// An explicit world point. /// An explicit world point.
Point(Vec3), Point(DVec3),
} }
/// One value box in a [`DynSpec`]. /// One value box in a [`DynSpec`].
@ -403,7 +403,7 @@ pub struct DynSpec {
pub guide: DynGuide, pub guide: DynGuide,
/// Far end of a reference line through `anchor` (only used by /// Far end of a reference line through `anchor` (only used by
/// [`DynGuide::Perp`]); `None` otherwise. /// [`DynGuide::Perp`]); `None` otherwise.
pub ref_point: Option<Vec3>, pub ref_point: Option<DVec3>,
} }
// ── Trait ───────────────────────────────────────────────────────────────── // ── Trait ─────────────────────────────────────────────────────────────────
@ -424,7 +424,7 @@ pub trait CadCommand: Send {
fn set_ucs(&mut self, _ucs: glam::Mat4) {} fn set_ucs(&mut self, _ucs: glam::Mat4) {}
/// Called when the user left-clicks in the viewport (point pick). /// Called when the user left-clicks in the viewport (point pick).
fn on_point(&mut self, pt: Vec3) -> CmdResult; fn on_point(&mut self, pt: DVec3) -> CmdResult;
/// Called when the user presses Enter (finalize / next option). /// Called when the user presses Enter (finalize / next option).
fn on_enter(&mut self) -> CmdResult; fn on_enter(&mut self) -> CmdResult;
@ -447,7 +447,7 @@ pub trait CadCommand: Send {
/// Called when the user clicks and `needs_entity_pick()` is true. /// Called when the user clicks and `needs_entity_pick()` is true.
/// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found). /// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found).
fn on_entity_pick(&mut self, _handle: Handle, _pt: Vec3) -> CmdResult { fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
@ -467,7 +467,7 @@ pub trait CadCommand: Send {
} }
/// Preview wires while hovering during object-point pick. /// Preview wires while hovering during object-point pick.
fn object_pick_hover_previews(&self, _scene: &Scene, _cursor: Vec3) -> Vec<WireModel> { fn object_pick_hover_previews(&self, _scene: &Scene, _cursor: DVec3) -> Vec<WireModel> {
vec![] vec![]
} }
@ -477,7 +477,7 @@ pub trait CadCommand: Send {
} }
/// Called when `needs_structure_point_pick()` is true and a structure is found near the click. /// Called when `needs_structure_point_pick()` is true and a structure is found near the click.
fn on_structure_pick(&mut self, _handle: Handle, _pt: Vec3) -> CmdResult { fn on_structure_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
@ -502,19 +502,19 @@ pub trait CadCommand: Send {
/// Called on every mouse-move when `needs_entity_pick()` is true. /// Called on every mouse-move when `needs_entity_pick()` is true.
/// Return preview wires showing the operation result under the cursor. /// Return preview wires showing the operation result under the cursor.
/// Default: empty (no preview). /// Default: empty (no preview).
fn on_hover_entity(&mut self, _handle: Handle, _pt: Vec3) -> Vec<WireModel> { fn on_hover_entity(&mut self, _handle: Handle, _pt: DVec3) -> Vec<WireModel> {
vec![] vec![]
} }
/// Called on every mouse-move in the viewport. /// Called on every mouse-move in the viewport.
/// Return `Some(WireModel)` to update the rubber-band preview, `None` to skip. /// Return `Some(WireModel)` to update the rubber-band preview, `None` to skip.
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
None None
} }
/// Called on every mouse-move; return all preview wires to show (object ghosts + rubber-band). /// Called on every mouse-move; return all preview wires to show (object ghosts + rubber-band).
/// Default: forwards to `on_mouse_move` for backwards compatibility. /// Default: forwards to `on_mouse_move` for backwards compatibility.
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> { fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
self.on_mouse_move(pt).into_iter().collect() self.on_mouse_move(pt).into_iter().collect()
} }
@ -599,7 +599,7 @@ pub trait CadCommand: Send {
/// Called instead of `on_point` when the command needs a tangent pick /// Called instead of `on_point` when the command needs a tangent pick
/// and the snap system found a tangent object. /// and the snap system found a tangent object.
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult { fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult {
let _ = obj; let _ = obj;
self.on_point(hit) self.on_point(hit)
} }
@ -647,7 +647,7 @@ pub trait CadCommand: Send {
/// Returns `None` when the value can only be typed (a count, or a /// Returns `None` when the value can only be typed (a count, or a
/// distance with no reference yet). The string the host commits is this /// distance with no reference yet). The string the host commits is this
/// value formatted; the command's own `on_text_input` parses it back. /// value formatted; the command's own `on_text_input` parses it back.
fn dyn_live_value(&self, _cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, _cursor: DVec3) -> Option<f64> {
None None
} }
} }

View file

@ -157,37 +157,37 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) {
fn apply_grip(arc: &mut Arc, grip_id: usize, apply: GripApply) { fn apply_grip(arc: &mut Arc, grip_id: usize, apply: GripApply) {
match (grip_id, apply) { match (grip_id, apply) {
(0, GripApply::Translate(d)) => { (0, GripApply::Translate(d)) => {
arc.center.x += d.x as f64; arc.center.x += d.x;
arc.center.y += d.y as f64; arc.center.y += d.y;
arc.center.z += d.z as f64; arc.center.z += d.z;
} }
(0, GripApply::Absolute(p)) => { (0, GripApply::Absolute(p)) => {
arc.center.x = p.x as f64; arc.center.x = p.x;
arc.center.y = p.y as f64; arc.center.y = p.y;
arc.center.z = p.z as f64; arc.center.z = p.z;
} }
(1, GripApply::Absolute(p)) => { (1, GripApply::Absolute(p)) => {
let dx = p.x - arc.center.x as f32; let dx = p.x - arc.center.x;
let dy = p.y - arc.center.y as f32; let dy = p.y - arc.center.y;
arc.start_angle = (dy as f64).atan2(dx as f64); arc.start_angle = dy.atan2(dx);
} }
(2, GripApply::Absolute(p)) => { (2, GripApply::Absolute(p)) => {
let dx = p.x - arc.center.x as f32; let dx = p.x - arc.center.x;
let dy = p.y - arc.center.y as f32; let dy = p.y - arc.center.y;
arc.end_angle = (dy as f64).atan2(dx as f64); arc.end_angle = dy.atan2(dx);
} }
(3, GripApply::Translate(d)) => { (3, GripApply::Translate(d)) => {
let sa = arc.start_angle as f32; let sa = arc.start_angle;
let ea = arc.end_angle as f32; let ea = arc.end_angle;
let span = angle_span(sa, ea); let span = angle_span(sa as f32, ea as f32) as f64;
let mid_a = sa + span * 0.5; let mid_a = sa + span * 0.5;
let current_mid_x = arc.center.x as f32 + arc.radius as f32 * mid_a.cos(); let current_mid_x = arc.center.x + arc.radius * mid_a.cos();
let current_mid_y = arc.center.y as f32 + arc.radius as f32 * mid_a.sin(); let current_mid_y = arc.center.y + arc.radius * mid_a.sin();
let new_mid_x = current_mid_x + d.x; let new_mid_x = current_mid_x + d.x;
let new_mid_y = current_mid_y + d.y; let new_mid_y = current_mid_y + d.y;
let dx = new_mid_x - arc.center.x as f32; let dx = new_mid_x - arc.center.x;
let dy = new_mid_y - arc.center.y as f32; let dy = new_mid_y - arc.center.y;
let new_r = (dx * dx + dy * dy).sqrt() as f64; let new_r = (dx * dx + dy * dy).sqrt();
if new_r > 1e-6 { if new_r > 1e-6 {
arc.radius = new_r; arc.radius = new_r;
} }

View file

@ -166,11 +166,9 @@ fn apply_grip(circle: &mut Circle, grip_id: usize, apply: GripApply) {
circle.center.z += d.z as f64; circle.center.z += d.z as f64;
} }
(1..=4, GripApply::Absolute(p)) => { (1..=4, GripApply::Absolute(p)) => {
let cx = circle.center.x as f32; let dx = p.x - circle.center.x;
let cy = circle.center.y as f32; let dy = p.y - circle.center.y;
let dx = p.x - cx; circle.radius = (dx * dx + dy * dy).sqrt();
let dy = p.y - cy;
circle.radius = ((dx * dx + dy * dy) as f64).sqrt();
} }
_ => {} _ => {}
} }

View file

@ -3,7 +3,7 @@ use acadrust::entities::{
DimensionDiameter, DimensionLinear, DimensionOrdinate, DimensionRadius, DimensionDiameter, DimensionLinear, DimensionOrdinate, DimensionRadius,
}; };
use acadrust::Entity; use acadrust::Entity;
use glam::Vec3; use glam::{DVec3, Vec3};
use crate::command::EntityTransform; use crate::command::EntityTransform;
use crate::entities::common::{ use crate::entities::common::{
@ -540,21 +540,22 @@ where
} }
} }
fn rotate_point(p: &mut acadrust::types::Vector3, center: Vec3, angle_rad: f32) { fn rotate_point(p: &mut acadrust::types::Vector3, center: DVec3, angle_rad: f32) {
let dx = p.x as f32 - center.x; let dx = p.x - center.x;
let dy = p.y as f32 - center.y; let dy = p.y - center.y;
let (s, c) = angle_rad.sin_cos(); let (s, c) = (angle_rad as f64).sin_cos();
p.x = (center.x + dx * c - dy * s) as f64; p.x = center.x + dx * c - dy * s;
p.y = (center.y + dx * s + dy * c) as f64; p.y = center.y + dx * s + dy * c;
} }
fn scale_point(p: &mut acadrust::types::Vector3, center: Vec3, factor: f32) { fn scale_point(p: &mut acadrust::types::Vector3, center: DVec3, factor: f32) {
p.x = (center.x + (p.x as f32 - center.x) * factor) as f64; let f = factor as f64;
p.y = (center.y + (p.y as f32 - center.y) * factor) as f64; p.x = center.x + (p.x - center.x) * f;
p.z = (center.z + (p.z as f32 - center.z) * factor) as f64; p.y = center.y + (p.y - center.y) * f;
p.z = center.z + (p.z - center.z) * f;
} }
fn mirror_point(p: &mut acadrust::types::Vector3, p1: Vec3, p2: Vec3) { fn mirror_point(p: &mut acadrust::types::Vector3, p1: DVec3, p2: DVec3) {
crate::scene::view::transform::reflect_xy_point(&mut p.x, &mut p.y, p1, p2); crate::scene::view::transform::reflect_xy_point(&mut p.x, &mut p.y, p1, p2);
} }
@ -582,16 +583,16 @@ fn dv3(v: &acadrust::types::Vector3) -> glam::DVec3 {
glam::DVec3::new(v.x, v.y, v.z) glam::DVec3::new(v.x, v.y, v.z)
} }
fn set_v3(target: &mut acadrust::types::Vector3, p: Vec3) { fn set_v3(target: &mut acadrust::types::Vector3, p: DVec3) {
target.x = p.x as f64; target.x = p.x;
target.y = p.y as f64; target.y = p.y;
target.z = p.z as f64; target.z = p.z;
} }
fn translate_v3(target: &mut acadrust::types::Vector3, d: Vec3) { fn translate_v3(target: &mut acadrust::types::Vector3, d: DVec3) {
target.x += d.x as f64; target.x += d.x;
target.y += d.y as f64; target.y += d.y;
target.z += d.z as f64; target.z += d.z;
} }
fn apply_to_v3(target: &mut acadrust::types::Vector3, apply: &GripApply) { fn apply_to_v3(target: &mut acadrust::types::Vector3, apply: &GripApply) {

View file

@ -24,7 +24,7 @@ pub(crate) fn mleader_v_anchor(attach: TextAttachmentType) -> MTextVAnchor {
| TextAttachmentType::BottomOfTopLineUnderlineAll => MTextVAnchor::BottomOfTopLine, | TextAttachmentType::BottomOfTopLineUnderlineAll => MTextVAnchor::BottomOfTopLine,
} }
} }
use glam::Vec3; use glam::{DVec3, Vec3};
use crate::command::EntityTransform; use crate::command::EntityTransform;
use crate::entities::common::{ use crate::entities::common::{
@ -920,9 +920,9 @@ fn apply_transform(ml: &mut MultiLeader, t: &EntityTransform) {
/// Reflect a direction (not position) vector across the mirror line p1→p2. /// Reflect a direction (not position) vector across the mirror line p1→p2.
/// Reflecting a direction is the same as reflecting `p1 + dir` then subtracting /// Reflecting a direction is the same as reflecting `p1 + dir` then subtracting
/// the reflection of `p1`, which simplifies to reflecting around the origin. /// the reflection of `p1`, which simplifies to reflecting around the origin.
fn reflect_xy_direction(dx: &mut f64, dy: &mut f64, p1: Vec3, p2: Vec3) { fn reflect_xy_direction(dx: &mut f64, dy: &mut f64, p1: DVec3, p2: DVec3) {
let zero = Vec3::ZERO; let zero = DVec3::ZERO;
let p2_rel = Vec3::new(p2.x - p1.x, p2.y - p1.y, 0.0); let p2_rel = DVec3::new(p2.x - p1.x, p2.y - p1.y, 0.0);
let mut tip_x = *dx; let mut tip_x = *dx;
let mut tip_y = *dy; let mut tip_y = *dy;
crate::scene::view::transform::reflect_xy_point(&mut tip_x, &mut tip_y, zero, p2_rel); crate::scene::view::transform::reflect_xy_point(&mut tip_x, &mut tip_y, zero, p2_rel);

View file

@ -18,9 +18,9 @@ use acadrust::{Arc as CadArc, EntityType};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::Vec3; use glam::DVec3;
const TAU: f32 = std::f32::consts::TAU; const TAU: f64 = std::f64::consts::TAU;
// ── Per-method SVG icons ─────────────────────────────────────────────────── // ── Per-method SVG icons ───────────────────────────────────────────────────
@ -67,12 +67,12 @@ pub const ICON: IconKind = ICON_CSE;
// ── Shared math helpers ──────────────────────────────────────────────────── // ── Shared math helpers ────────────────────────────────────────────────────
/// Angle in radians from `center` to `pt`. /// Angle in radians from `center` to `pt`.
fn angle_xy(center: Vec3, pt: Vec3) -> f32 { fn angle_xy(center: DVec3, pt: DVec3) -> f64 {
(pt.y - center.y).atan2(pt.x - center.x) (pt.y - center.y).atan2(pt.x - center.x)
} }
/// Build a CCW arc polyline from `start_angle` to `end_angle` (radians). /// Build a CCW arc polyline from `start_angle` to `end_angle` (radians).
fn arc_preview(center: Vec3, radius: f32, start_angle: f32, end_angle: f32) -> WireModel { fn arc_preview(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> WireModel {
let mut ea = end_angle; let mut ea = end_angle;
while ea < start_angle { while ea < start_angle {
ea += TAU; ea += TAU;
@ -81,23 +81,23 @@ fn arc_preview(center: Vec3, radius: f32, start_angle: f32, end_angle: f32) -> W
let segs = ((span / TAU) * 64.0).ceil().max(4.0) as u32; let segs = ((span / TAU) * 64.0).ceil().max(4.0) as u32;
let pts: Vec<[f32; 3]> = (0..=segs) let pts: Vec<[f32; 3]> = (0..=segs)
.map(|i| { .map(|i| {
let a = start_angle + span * (i as f32 / segs as f32); let a = start_angle + span * (i as f64 / segs as f64);
[ [
center.x + radius * a.cos(), (center.x + radius * a.cos()) as f32,
center.y + radius * a.sin(), (center.y + radius * a.sin()) as f32,
center.z, center.z as f32,
] ]
}) })
.collect(); .collect();
WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false) WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false)
} }
fn make_arc(center: Vec3, radius: f32, start_angle: f32, end_angle: f32) -> EntityType { fn make_arc(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> EntityType {
EntityType::Arc(CadArc { EntityType::Arc(CadArc {
center: Vector3::new(center.x as f64, center.y as f64, center.z as f64), center: Vector3::new(center.x, center.y, center.z),
radius: radius as f64, radius,
start_angle: start_angle as f64, start_angle,
end_angle: end_angle as f64, end_angle,
..Default::default() ..Default::default()
}) })
} }
@ -105,7 +105,7 @@ fn make_arc(center: Vec3, radius: f32, start_angle: f32, end_angle: f32) -> Enti
/// Signed rotation from `prev` to `curr` around `center`. /// Signed rotation from `prev` to `curr` around `center`.
/// Positive = CCW, negative = CW. /// Positive = CCW, negative = CW.
/// Signed angle (radians) swept from `prev` to `curr` about `center`. /// Signed angle (radians) swept from `prev` to `curr` about `center`.
fn rot_delta(center: Vec3, prev: Vec3, curr: Vec3) -> f32 { fn rot_delta(center: DVec3, prev: DVec3, curr: DVec3) -> f64 {
let p = prev - center; let p = prev - center;
let c = curr - center; let c = curr - center;
(p.x * c.y - p.y * c.x).atan2(p.x * c.x + p.y * c.y) (p.x * c.y - p.y * c.x).atan2(p.x * c.x + p.y * c.y)
@ -113,19 +113,22 @@ fn rot_delta(center: Vec3, prev: Vec3, curr: Vec3) -> f32 {
/// Minimum swept angle before the previewed arc may flip CW/CCW. Filters the /// Minimum swept angle before the previewed arc may flip CW/CCW. Filters the
/// per-frame cursor jitter that otherwise reverses the sweep on tiny moves. /// per-frame cursor jitter that otherwise reverses the sweep on tiny moves.
const DIR_TOL: f32 = 0.1745; // ~10° const DIR_TOL: f64 = 0.1745; // ~10°
fn line_wire(a: Vec3, b: Vec3) -> WireModel { fn line_wire(a: DVec3, b: DVec3) -> WireModel {
WireModel::solid( WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
vec![[a.x, a.y, a.z], [b.x, b.y, b.z]], vec![
[a.x as f32, a.y as f32, a.z as f32],
[b.x as f32, b.y as f32, b.z as f32],
],
WireModel::CYAN, WireModel::CYAN,
false, false,
) )
} }
/// Circumscribed circle through three points. /// Circumscribed circle through three points.
fn circumcircle(a: Vec3, b: Vec3, c: Vec3) -> Option<(Vec3, f32)> { fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
let d = 2.0 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)); let d = 2.0 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
if d.abs() < 1e-9 { if d.abs() < 1e-9 {
return None; return None;
@ -138,12 +141,12 @@ fn circumcircle(a: Vec3, b: Vec3, c: Vec3) -> Option<(Vec3, f32)> {
+ (b.x * b.x + b.y * b.y) * (a.x - c.x) + (b.x * b.x + b.y * b.y) * (a.x - c.x)
+ (c.x * c.x + c.y * c.y) * (b.x - a.x)) + (c.x * c.x + c.y * c.y) * (b.x - a.x))
/ d; / d;
let center = Vec3::new(ux, uy, a.z); let center = DVec3::new(ux, uy, a.z);
Some((center, center.distance(a))) Some((center, center.distance(a)))
} }
/// True if `angle` lies inside the CCW arc [start..end] (radians). /// True if `angle` lies inside the CCW arc [start..end] (radians).
fn ccw_contains(angle: f32, start: f32, end: f32) -> bool { fn ccw_contains(angle: f64, start: f64, end: f64) -> bool {
let a = angle.rem_euclid(TAU); let a = angle.rem_euclid(TAU);
let s = start.rem_euclid(TAU); let s = start.rem_euclid(TAU);
let e = end.rem_euclid(TAU); let e = end.rem_euclid(TAU);
@ -155,14 +158,14 @@ fn ccw_contains(angle: f32, start: f32, end: f32) -> bool {
} }
/// Arc center+radius from two endpoints and a cursor (sagitta / bow-toward-cursor). /// Arc center+radius from two endpoints and a cursor (sagitta / bow-toward-cursor).
fn arc_from_sagitta(s: Vec3, e: Vec3, cursor: Vec3) -> Option<(Vec3, f32)> { fn arc_from_sagitta(s: DVec3, e: DVec3, cursor: DVec3) -> Option<(DVec3, f64)> {
let chord_vec = e - s; let chord_vec = e - s;
let chord_len = (chord_vec.x * chord_vec.x + chord_vec.y * chord_vec.y).sqrt(); let chord_len = (chord_vec.x * chord_vec.x + chord_vec.y * chord_vec.y).sqrt();
if chord_len < 1e-6 { if chord_len < 1e-6 {
return None; return None;
} }
let unit_chord = Vec3::new(chord_vec.x / chord_len, chord_vec.y / chord_len, 0.0); let unit_chord = DVec3::new(chord_vec.x / chord_len, chord_vec.y / chord_len, 0.0);
let perp = Vec3::new(-unit_chord.y, unit_chord.x, 0.0); let perp = DVec3::new(-unit_chord.y, unit_chord.x, 0.0);
let mid = (s + e) * 0.5; let mid = (s + e) * 0.5;
let h = (cursor - mid).dot(perp); // signed sagitta let h = (cursor - mid).dot(perp); // signed sagitta
if h.abs() < 1e-3 { if h.abs() < 1e-3 {
@ -176,14 +179,14 @@ fn arc_from_sagitta(s: Vec3, e: Vec3, cursor: Vec3) -> Option<(Vec3, f32)> {
} }
/// Arc center+radius from start, end, and a radius-magnitude point (dist = dist(pt, start)). /// Arc center+radius from start, end, and a radius-magnitude point (dist = dist(pt, start)).
fn arc_from_se_radius(s: Vec3, e: Vec3, radius_pt: Vec3) -> Option<(Vec3, f32)> { fn arc_from_se_radius(s: DVec3, e: DVec3, radius_pt: DVec3) -> Option<(DVec3, f64)> {
let r = s.distance(radius_pt).max(1e-3); let r = s.distance(radius_pt).max(1e-3);
let chord_len = s.distance(e); let chord_len = s.distance(e);
if r < chord_len * 0.5 { if r < chord_len * 0.5 {
return None; return None;
} }
let unit_chord = (e - s) / chord_len; let unit_chord = (e - s) / chord_len;
let perp = Vec3::new(-unit_chord.y, 0.0, unit_chord.x); let perp = DVec3::new(-unit_chord.y, 0.0, unit_chord.x);
let mid = (s + e) * 0.5; let mid = (s + e) * 0.5;
let h_sign = (radius_pt - mid).dot(perp).signum(); let h_sign = (radius_pt - mid).dot(perp).signum();
let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5)) let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5))
@ -193,12 +196,12 @@ fn arc_from_se_radius(s: Vec3, e: Vec3, radius_pt: Vec3) -> Option<(Vec3, f32)>
} }
/// Arc center+radius from start, end, and a tangent-direction point at start. /// Arc center+radius from start, end, and a tangent-direction point at start.
fn arc_from_direction(s: Vec3, e: Vec3, dir_pt: Vec3) -> Option<(Vec3, f32)> { fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)> {
let t = (dir_pt - s).normalize_or_zero(); let t = (dir_pt - s).normalize_or_zero();
if t.length_squared() < 1e-12 { if t.length_squared() < 1e-12 {
return None; return None;
} }
let perp_t = Vec3::new(-t.y, t.x, 0.0); // rotate tangent 90° CCW let perp_t = DVec3::new(-t.y, t.x, 0.0); // rotate tangent 90° CCW
let chord = e - s; let chord = e - s;
let denom = perp_t.x * chord.x + perp_t.y * chord.y; let denom = perp_t.x * chord.x + perp_t.y * chord.y;
if denom.abs() < 1e-9 { if denom.abs() < 1e-9 {
@ -211,7 +214,7 @@ fn arc_from_direction(s: Vec3, e: Vec3, dir_pt: Vec3) -> Option<(Vec3, f32)> {
/// Compute end_angle from a chord-length pick (SCL / CSL semantics). /// Compute end_angle from a chord-length pick (SCL / CSL semantics).
/// `chord_len` is clamped to [0, 2r]. /// `chord_len` is clamped to [0, 2r].
fn end_angle_from_chord_len(start_angle: f32, chord: f32, r: f32) -> f32 { fn end_angle_from_chord_len(start_angle: f64, chord: f64, r: f64) -> f64 {
let half = (chord.min(2.0 * r) / (2.0 * r)).asin(); let half = (chord.min(2.0 * r) / (2.0 * r)).asin();
start_angle + 2.0 * half start_angle + 2.0 * half
} }
@ -220,10 +223,10 @@ fn end_angle_from_chord_len(start_angle: f32, chord: f32, r: f32) -> f32 {
pub struct ArcCommand { pub struct ArcCommand {
step: u8, step: u8,
c: Vec3, c: DVec3,
r: f32, r: f64,
sa: f32, sa: f64,
prev_pt: Option<Vec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
} }
@ -231,7 +234,7 @@ impl ArcCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
c: Vec3::ZERO, c: DVec3::ZERO,
r: 0.0, r: 0.0,
sa: 0.0, sa: 0.0,
prev_pt: None, prev_pt: None,
@ -254,7 +257,7 @@ impl CadCommand for ArcCommand {
), ),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.c = pt; self.c = pt;
@ -284,7 +287,7 @@ impl CadCommand for ArcCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.c, pt)), 1 => Some(line_wire(self.c, pt)),
2 => { 2 => {
@ -315,7 +318,7 @@ impl CadCommand for ArcCommand {
// ── Command 2: 3-Point (ARC_3P) ────────────────────────────────────────── // ── Command 2: 3-Point (ARC_3P) ──────────────────────────────────────────
pub struct Arc3PCommand { pub struct Arc3PCommand {
pts: Vec<Vec3>, pts: Vec<DVec3>,
} }
impl Arc3PCommand { impl Arc3PCommand {
@ -335,7 +338,7 @@ impl CadCommand for Arc3PCommand {
_ => "ARC 3P Specify end point:".into(), _ => "ARC 3P Specify end point:".into(),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.pts.push(pt); self.pts.push(pt);
if self.pts.len() < 3 { if self.pts.len() < 3 {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
@ -366,7 +369,7 @@ impl CadCommand for Arc3PCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.pts.len() { match self.pts.len() {
0 => None, 0 => None,
1 => Some(line_wire(self.pts[0], pt)), 1 => Some(line_wire(self.pts[0], pt)),
@ -385,7 +388,11 @@ impl CadCommand for Arc3PCommand {
} else { } else {
Some(WireModel::solid( Some(WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
vec![[p1.x, p1.y, p1.z], [p2.x, p2.y, p2.z], [pt.x, pt.y, pt.z]], vec![
[p1.x as f32, p1.y as f32, p1.z as f32],
[p2.x as f32, p2.y as f32, p2.z as f32],
[pt.x as f32, pt.y as f32, pt.z as f32],
],
WireModel::CYAN, WireModel::CYAN,
false, false,
)) ))
@ -399,11 +406,11 @@ impl CadCommand for Arc3PCommand {
pub struct ArcSCECommand { pub struct ArcSCECommand {
step: u8, step: u8,
s: Vec3, s: DVec3,
c: Vec3, c: DVec3,
r: f32, r: f64,
sa: f32, sa: f64,
prev_pt: Option<Vec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
} }
@ -411,8 +418,8 @@ impl ArcSCECommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
s: Vec3::ZERO, s: DVec3::ZERO,
c: Vec3::ZERO, c: DVec3::ZERO,
r: 0.0, r: 0.0,
sa: 0.0, sa: 0.0,
prev_pt: None, prev_pt: None,
@ -432,7 +439,7 @@ impl CadCommand for ArcSCECommand {
_ => format!("ARC SCE Specify end point [r={:.3}]:", self.r), _ => format!("ARC SCE Specify end point [r={:.3}]:", self.r),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.s = pt; self.s = pt;
@ -463,7 +470,7 @@ impl CadCommand for ArcSCECommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
@ -496,11 +503,11 @@ impl CadCommand for ArcSCECommand {
pub struct ArcSCACommand { pub struct ArcSCACommand {
step: u8, step: u8,
s: Vec3, s: DVec3,
c: Vec3, c: DVec3,
r: f32, r: f64,
sa: f32, sa: f64,
prev_pt: Option<Vec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
} }
@ -508,8 +515,8 @@ impl ArcSCACommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
s: Vec3::ZERO, s: DVec3::ZERO,
c: Vec3::ZERO, c: DVec3::ZERO,
r: 0.0, r: 0.0,
sa: 0.0, sa: 0.0,
prev_pt: None, prev_pt: None,
@ -532,7 +539,7 @@ impl CadCommand for ArcSCACommand {
), ),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.s = pt; self.s = pt;
@ -565,7 +572,7 @@ impl CadCommand for ArcSCACommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.step == 2 { if self.step == 2 {
let span: f32 = text.trim().replace(',', ".").parse().ok()?; let span: f64 = text.trim().replace(',', ".").parse().ok()?;
// Negative span = CW; positive = CCW. // Negative span = CW; positive = CCW.
let ea = self.sa + span.to_radians(); let ea = self.sa + span.to_radians();
return Some(CmdResult::CommitAndExit(make_arc( return Some(CmdResult::CommitAndExit(make_arc(
@ -582,17 +589,18 @@ impl CadCommand for ArcSCACommand {
anchor: DynAnchor::Point(self.c), anchor: DynAnchor::Point(self.c),
fields: vec![DynFieldSpec::new(DynRole::Angle)], fields: vec![DynFieldSpec::new(DynRole::Angle)],
guide: DynGuide::Polar, guide: DynGuide::Polar,
ref_point: Some(self.c + Vec3::new(self.sa.cos(), self.sa.sin(), 0.0)), ref_point: Some(self.c + DVec3::new(self.sa.cos(), self.sa.sin(), 0.0)),
}) })
} }
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
self.step == 2 self.step == 2
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
(self.step == 2) (self.step == 2).then(|| {
.then(|| crate::command::dyn_display_angle_deg(angle_xy(self.c, cursor) - self.sa) as f64) crate::command::dyn_display_angle_deg((angle_xy(self.c, cursor) - self.sa) as f32) as f64
})
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
@ -626,18 +634,18 @@ impl CadCommand for ArcSCACommand {
pub struct ArcSCLCommand { pub struct ArcSCLCommand {
step: u8, step: u8,
s: Vec3, s: DVec3,
c: Vec3, c: DVec3,
r: f32, r: f64,
sa: f32, sa: f64,
} }
impl ArcSCLCommand { impl ArcSCLCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
s: Vec3::ZERO, s: DVec3::ZERO,
c: Vec3::ZERO, c: DVec3::ZERO,
r: 0.0, r: 0.0,
sa: 0.0, sa: 0.0,
} }
@ -658,7 +666,7 @@ impl CadCommand for ArcSCLCommand {
), ),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.s = pt; self.s = pt;
@ -687,7 +695,7 @@ impl CadCommand for ArcSCLCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.step == 2 { if self.step == 2 {
let chord: f32 = text.trim().replace(',', ".").parse().ok()?; let chord: f64 = text.trim().replace(',', ".").parse().ok()?;
if chord > 0.0 { if chord > 0.0 {
let ea = end_angle_from_chord_len(self.sa, chord, self.r); let ea = end_angle_from_chord_len(self.sa, chord, self.r);
return Some(CmdResult::CommitAndExit(make_arc( return Some(CmdResult::CommitAndExit(make_arc(
@ -710,10 +718,10 @@ impl CadCommand for ArcSCLCommand {
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
self.step == 2 self.step == 2
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
(self.step == 2).then(|| self.s.distance(cursor) as f64) (self.step == 2).then(|| self.s.distance(cursor))
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
@ -731,16 +739,16 @@ impl CadCommand for ArcSCLCommand {
pub struct ArcSEACommand { pub struct ArcSEACommand {
step: u8, step: u8,
s: Vec3, s: DVec3,
e: Vec3, e: DVec3,
} }
impl ArcSEACommand { impl ArcSEACommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
s: Vec3::ZERO, s: DVec3::ZERO,
e: Vec3::ZERO, e: DVec3::ZERO,
} }
} }
} }
@ -756,7 +764,7 @@ impl CadCommand for ArcSEACommand {
_ => "ARC SEA Specify angle (move cursor perpendicular to chord):".into(), _ => "ARC SEA Specify angle (move cursor perpendicular to chord):".into(),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.s = pt; self.s = pt;
@ -784,7 +792,7 @@ impl CadCommand for ArcSEACommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
@ -806,16 +814,16 @@ impl CadCommand for ArcSEACommand {
pub struct ArcSERCommand { pub struct ArcSERCommand {
step: u8, step: u8,
s: Vec3, s: DVec3,
e: Vec3, e: DVec3,
} }
impl ArcSERCommand { impl ArcSERCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
s: Vec3::ZERO, s: DVec3::ZERO,
e: Vec3::ZERO, e: DVec3::ZERO,
} }
} }
} }
@ -831,7 +839,7 @@ impl CadCommand for ArcSERCommand {
_ => "ARC SER Click radius point or type radius value:".into(), _ => "ARC SER Click radius point or type radius value:".into(),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.s = pt; self.s = pt;
@ -861,13 +869,13 @@ impl CadCommand for ArcSERCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.step == 2 { if self.step == 2 {
let r: f32 = text.trim().replace(',', ".").parse().ok()?; let r: f64 = text.trim().replace(',', ".").parse().ok()?;
let chord = self.s.distance(self.e); let chord = self.s.distance(self.e);
if r > 0.0 && r >= chord / 2.0 { if r > 0.0 && r >= chord / 2.0 {
let mid = (self.s + self.e) * 0.5; let mid = (self.s + self.e) * 0.5;
let perp = { let perp = {
let cv = (self.e - self.s) / chord; let cv = (self.e - self.s) / chord;
Vec3::new(-cv.y, cv.x, 0.0) DVec3::new(-cv.y, cv.x, 0.0)
}; };
let d = (r * r - (chord / 2.0) * (chord / 2.0)).max(0.0).sqrt(); let d = (r * r - (chord / 2.0) * (chord / 2.0)).max(0.0).sqrt();
let center = mid - perp * d; let center = mid - perp * d;
@ -891,13 +899,13 @@ impl CadCommand for ArcSERCommand {
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
self.step == 2 self.step == 2
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
if self.step != 2 { if self.step != 2 {
return None; return None;
} }
arc_from_se_radius(self.s, self.e, cursor).map(|(_, r)| r as f64) arc_from_se_radius(self.s, self.e, cursor).map(|(_, r)| r)
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
@ -919,16 +927,16 @@ impl CadCommand for ArcSERCommand {
pub struct ArcSEDCommand { pub struct ArcSEDCommand {
step: u8, step: u8,
s: Vec3, s: DVec3,
e: Vec3, e: DVec3,
} }
impl ArcSEDCommand { impl ArcSEDCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
s: Vec3::ZERO, s: DVec3::ZERO,
e: Vec3::ZERO, e: DVec3::ZERO,
} }
} }
} }
@ -944,7 +952,7 @@ impl CadCommand for ArcSEDCommand {
_ => "ARC SED Specify tangent direction at start:".into(), _ => "ARC SED Specify tangent direction at start:".into(),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.s = pt; self.s = pt;
@ -972,7 +980,7 @@ impl CadCommand for ArcSEDCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.s, pt)), 1 => Some(line_wire(self.s, pt)),
2 => { 2 => {
@ -994,10 +1002,10 @@ impl CadCommand for ArcSEDCommand {
pub struct ArcCSACommand { pub struct ArcCSACommand {
step: u8, step: u8,
c: Vec3, c: DVec3,
r: f32, r: f64,
sa: f32, sa: f64,
prev_pt: Option<Vec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
} }
@ -1005,7 +1013,7 @@ impl ArcCSACommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
c: Vec3::ZERO, c: DVec3::ZERO,
r: 0.0, r: 0.0,
sa: 0.0, sa: 0.0,
prev_pt: None, prev_pt: None,
@ -1028,7 +1036,7 @@ impl CadCommand for ArcCSACommand {
), ),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.c = pt; self.c = pt;
@ -1060,7 +1068,7 @@ impl CadCommand for ArcCSACommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.step == 2 { if self.step == 2 {
let span: f32 = text.trim().replace(',', ".").parse().ok()?; let span: f64 = text.trim().replace(',', ".").parse().ok()?;
let ea = self.sa + span.to_radians(); let ea = self.sa + span.to_radians();
return Some(CmdResult::CommitAndExit(make_arc( return Some(CmdResult::CommitAndExit(make_arc(
self.c, self.r, self.sa, ea, self.c, self.r, self.sa, ea,
@ -1074,17 +1082,18 @@ impl CadCommand for ArcCSACommand {
anchor: DynAnchor::Point(self.c), anchor: DynAnchor::Point(self.c),
fields: vec![DynFieldSpec::new(DynRole::Angle)], fields: vec![DynFieldSpec::new(DynRole::Angle)],
guide: DynGuide::Polar, guide: DynGuide::Polar,
ref_point: Some(self.c + Vec3::new(self.sa.cos(), self.sa.sin(), 0.0)), ref_point: Some(self.c + DVec3::new(self.sa.cos(), self.sa.sin(), 0.0)),
}) })
} }
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
self.step == 2 self.step == 2
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
(self.step == 2) (self.step == 2).then(|| {
.then(|| crate::command::dyn_display_angle_deg(angle_xy(self.c, cursor) - self.sa) as f64) crate::command::dyn_display_angle_deg((angle_xy(self.c, cursor) - self.sa) as f32) as f64
})
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.c, pt)), 1 => Some(line_wire(self.c, pt)),
2 => { 2 => {
@ -1117,18 +1126,18 @@ impl CadCommand for ArcCSACommand {
pub struct ArcCSLCommand { pub struct ArcCSLCommand {
step: u8, step: u8,
c: Vec3, c: DVec3,
s: Vec3, s: DVec3,
r: f32, r: f64,
sa: f32, sa: f64,
} }
impl ArcCSLCommand { impl ArcCSLCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
c: Vec3::ZERO, c: DVec3::ZERO,
s: Vec3::ZERO, s: DVec3::ZERO,
r: 0.0, r: 0.0,
sa: 0.0, sa: 0.0,
} }
@ -1149,7 +1158,7 @@ impl CadCommand for ArcCSLCommand {
), ),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.c = pt; self.c = pt;
@ -1178,7 +1187,7 @@ impl CadCommand for ArcCSLCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.step == 2 { if self.step == 2 {
let chord: f32 = text.trim().replace(',', ".").parse().ok()?; let chord: f64 = text.trim().replace(',', ".").parse().ok()?;
if chord > 0.0 { if chord > 0.0 {
let ea = end_angle_from_chord_len(self.sa, chord, self.r); let ea = end_angle_from_chord_len(self.sa, chord, self.r);
return Some(CmdResult::CommitAndExit(make_arc( return Some(CmdResult::CommitAndExit(make_arc(
@ -1201,10 +1210,10 @@ impl CadCommand for ArcCSLCommand {
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
self.step == 2 self.step == 2
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
(self.step == 2).then(|| self.s.distance(cursor) as f64) (self.step == 2).then(|| self.s.distance(cursor))
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(line_wire(self.c, pt)), 1 => Some(line_wire(self.c, pt)),
2 => { 2 => {

View file

@ -15,9 +15,9 @@ use crate::command::{CadCommand, CmdResult, DynField, TangentObject};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::Vec3; use glam::DVec3;
const TAU: f32 = std::f32::consts::TAU; const TAU: f64 = std::f64::consts::TAU;
// ── Per-method SVG icons ─────────────────────────────────────────────────── // ── Per-method SVG icons ───────────────────────────────────────────────────
@ -58,15 +58,15 @@ pub const ICON: IconKind = ICON_CR;
// ── Shared geometry ──────────────────────────────────────────────────────── // ── Shared geometry ────────────────────────────────────────────────────────
fn circle_wire(center: Vec3, radius: f32) -> WireModel { fn circle_wire(center: DVec3, radius: f64) -> WireModel {
let segs = 64u32; let segs = 64u32;
let mut pts: Vec<[f32; 3]> = (0..=segs) let mut pts: Vec<[f32; 3]> = (0..=segs)
.map(|i| { .map(|i| {
let a = (i as f32) * TAU / segs as f32; let a = (i as f64) * TAU / segs as f64;
[ [
center.x + radius * a.cos(), (center.x + radius * a.cos()) as f32,
center.y + radius * a.sin(), (center.y + radius * a.sin()) as f32,
center.z, center.z as f32,
] ]
}) })
.collect(); .collect();
@ -76,9 +76,9 @@ fn circle_wire(center: Vec3, radius: f32) -> WireModel {
WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false) WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false)
} }
fn make_circle(center: Vec3, radius: f64) -> EntityType { fn make_circle(center: DVec3, radius: f64) -> EntityType {
EntityType::Circle(Circle { EntityType::Circle(Circle {
center: Vector3::new(center.x as f64, center.y as f64, center.z as f64), center: Vector3::new(center.x, center.y, center.z),
radius, radius,
..Default::default() ..Default::default()
}) })
@ -86,7 +86,7 @@ fn make_circle(center: Vec3, radius: f64) -> EntityType {
/// Circumscribed circle through three points. /// Circumscribed circle through three points.
/// Returns `None` if the points are collinear. /// Returns `None` if the points are collinear.
fn circumcircle(a: Vec3, b: Vec3, c: Vec3) -> Option<(Vec3, f32)> { fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
let ax = a.x; let ax = a.x;
let ay = a.y; let ay = a.y;
let bx = b.x; let bx = b.x;
@ -105,7 +105,7 @@ fn circumcircle(a: Vec3, b: Vec3, c: Vec3) -> Option<(Vec3, f32)> {
+ (bx * bx + by * by) * (ax - cx) + (bx * bx + by * by) * (ax - cx)
+ (cx * cx + cy * cy) * (bx - ax)) + (cx * cx + cy * cy) * (bx - ax))
/ d; / d;
let center = Vec3::new(ux, uy, a.z); let center = DVec3::new(ux, uy, a.z);
Some((center, center.distance(a))) Some((center, center.distance(a)))
} }
@ -117,7 +117,7 @@ pub struct CircleCommand {
} }
enum StepCR { enum StepCR {
Center, Center,
Radius(Vec3), Radius(DVec3),
} }
impl CircleCommand { impl CircleCommand {
@ -142,7 +142,7 @@ impl CadCommand for CircleCommand {
), ),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step { match &self.step {
StepCR::Center => { StepCR::Center => {
self.step = StepCR::Radius(pt); self.step = StepCR::Radius(pt);
@ -150,8 +150,8 @@ impl CadCommand for CircleCommand {
} }
StepCR::Radius(c) => { StepCR::Radius(c) => {
let r = c.distance(pt); let r = c.distance(pt);
defaults::set_circle_radius(r); defaults::set_circle_radius(r as f32);
CmdResult::CommitAndExit(make_circle(*c, r as f64)) CmdResult::CommitAndExit(make_circle(*c, r))
} }
} }
} }
@ -166,7 +166,7 @@ impl CadCommand for CircleCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if let StepCR::Radius(c) = &self.step { if let StepCR::Radius(c) = &self.step {
Some(circle_wire(*c, c.distance(pt))) Some(circle_wire(*c, c.distance(pt)))
} else { } else {
@ -175,10 +175,10 @@ impl CadCommand for CircleCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let StepCR::Radius(c) = &self.step { if let StepCR::Radius(c) = &self.step {
let r: f32 = text.trim().replace(',', ".").parse().ok()?; let r: f64 = text.trim().replace(',', ".").parse().ok()?;
if r > 0.0 { if r > 0.0 {
defaults::set_circle_radius(r); defaults::set_circle_radius(r as f32);
return Some(CmdResult::CommitAndExit(make_circle(*c, r as f64))); return Some(CmdResult::CommitAndExit(make_circle(*c, r)));
} }
} }
None None
@ -236,7 +236,7 @@ impl CadCommand for CircleCDCommand {
), ),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step { match &self.step {
StepCR::Center => { StepCR::Center => {
self.step = StepCR::Radius(pt); self.step = StepCR::Radius(pt);
@ -244,8 +244,8 @@ impl CadCommand for CircleCDCommand {
} }
StepCR::Radius(c) => { StepCR::Radius(c) => {
let d = c.distance(pt) * 2.0; let d = c.distance(pt) * 2.0;
defaults::set_circle_diam(d); defaults::set_circle_diam(d as f32);
CmdResult::CommitAndExit(make_circle(*c, (d / 2.0) as f64)) CmdResult::CommitAndExit(make_circle(*c, d / 2.0))
} }
} }
} }
@ -260,7 +260,7 @@ impl CadCommand for CircleCDCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
// Preview radius = distance to cursor; on commit that distance becomes the radius (diameter = 2x). // Preview radius = distance to cursor; on commit that distance becomes the radius (diameter = 2x).
if let StepCR::Radius(c) = &self.step { if let StepCR::Radius(c) = &self.step {
Some(circle_wire(*c, c.distance(pt))) Some(circle_wire(*c, c.distance(pt)))
@ -270,10 +270,10 @@ impl CadCommand for CircleCDCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let StepCR::Radius(c) = &self.step { if let StepCR::Radius(c) = &self.step {
let d: f32 = text.trim().replace(',', ".").parse().ok()?; let d: f64 = text.trim().replace(',', ".").parse().ok()?;
if d > 0.0 { if d > 0.0 {
defaults::set_circle_diam(d); defaults::set_circle_diam(d as f32);
return Some(CmdResult::CommitAndExit(make_circle(*c, (d / 2.0) as f64))); return Some(CmdResult::CommitAndExit(make_circle(*c, d / 2.0)));
} }
} }
None None
@ -298,7 +298,7 @@ impl CadCommand for CircleCDCommand {
// ── Command: 2-Point ────────────────────────────────────────────────────── // ── Command: 2-Point ──────────────────────────────────────────────────────
pub struct Circle2PCommand { pub struct Circle2PCommand {
p1: Option<Vec3>, p1: Option<DVec3>,
} }
impl Circle2PCommand { impl Circle2PCommand {
@ -318,7 +318,7 @@ impl CadCommand for Circle2PCommand {
"CIRCLE 2P Specify second end of diameter:".into() "CIRCLE 2P Specify second end of diameter:".into()
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.p1 { match self.p1 {
None => { None => {
self.p1 = Some(pt); self.p1 = Some(pt);
@ -326,7 +326,7 @@ impl CadCommand for Circle2PCommand {
} }
Some(p1) => { Some(p1) => {
let center = (p1 + pt) * 0.5; let center = (p1 + pt) * 0.5;
let radius = p1.distance(pt) as f64 / 2.0; let radius = p1.distance(pt) / 2.0;
CmdResult::CommitAndExit(make_circle(center, radius)) CmdResult::CommitAndExit(make_circle(center, radius))
} }
} }
@ -337,7 +337,7 @@ impl CadCommand for Circle2PCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let p1 = self.p1?; let p1 = self.p1?;
let center = (p1 + pt) * 0.5; let center = (p1 + pt) * 0.5;
let radius = p1.distance(pt) / 2.0; let radius = p1.distance(pt) / 2.0;
@ -348,7 +348,7 @@ impl CadCommand for Circle2PCommand {
// ── Command: 3-Point ────────────────────────────────────────────────────── // ── Command: 3-Point ──────────────────────────────────────────────────────
pub struct Circle3PCommand { pub struct Circle3PCommand {
pts: Vec<Vec3>, pts: Vec<DVec3>,
} }
impl Circle3PCommand { impl Circle3PCommand {
@ -368,14 +368,14 @@ impl CadCommand for Circle3PCommand {
_ => "CIRCLE 3P Specify third point:".into(), _ => "CIRCLE 3P Specify third point:".into(),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.pts.push(pt); self.pts.push(pt);
if self.pts.len() < 3 { if self.pts.len() < 3 {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]); let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]);
match circumcircle(a, b, c) { match circumcircle(a, b, c) {
Some((center, radius)) => CmdResult::CommitAndExit(make_circle(center, radius as f64)), Some((center, radius)) => CmdResult::CommitAndExit(make_circle(center, radius)),
None => { None => {
self.pts.pop(); self.pts.pop();
CmdResult::NeedPoint CmdResult::NeedPoint
@ -388,7 +388,7 @@ impl CadCommand for Circle3PCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.pts.len() { match self.pts.len() {
0 => None, 0 => None,
1 => { 1 => {
@ -406,7 +406,11 @@ impl CadCommand for Circle3PCommand {
} else { } else {
Some(WireModel::solid( Some(WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
vec![[a.x, a.y, a.z], [b.x, b.y, b.z], [pt.x, pt.y, pt.z]], vec![
[a.x as f32, a.y as f32, a.z as f32],
[b.x as f32, b.y as f32, b.z as f32],
[pt.x as f32, pt.y as f32, pt.z as f32],
],
WireModel::CYAN, WireModel::CYAN,
false, false,
)) ))
@ -420,13 +424,13 @@ impl CadCommand for Circle3PCommand {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct Line2D { struct Line2D {
a: f32, a: f64,
b: f32, b: f64,
c: f32, c: f64,
} // ax + by + c = 0, a²+b²=1 } // ax + by + c = 0, a²+b²=1
impl Line2D { impl Line2D {
fn from_obj(p1: Vec3, p2: Vec3) -> Self { fn from_obj(p1: DVec3, p2: DVec3) -> Self {
let dx = p2.x - p1.x; let dx = p2.x - p1.x;
let dy = p2.y - p1.y; let dy = p2.y - p1.y;
let len = (dx * dx + dy * dy).sqrt(); let len = (dx * dx + dy * dy).sqrt();
@ -447,17 +451,17 @@ impl Line2D {
} }
} }
fn line_line_isect(l1: Line2D, l2: Line2D) -> Option<Vec3> { fn line_line_isect(l1: Line2D, l2: Line2D) -> Option<DVec3> {
let det = l1.a * l2.b - l2.a * l1.b; let det = l1.a * l2.b - l2.a * l1.b;
if det.abs() < 1e-9 { if det.abs() < 1e-9 {
return None; return None;
} }
let x = (-l1.c * l2.b + l2.c * l1.b) / det; let x = (-l1.c * l2.b + l2.c * l1.b) / det;
let y = (-l1.a * l2.c + l2.a * l1.c) / det; let y = (-l1.a * l2.c + l2.a * l1.c) / det;
Some(Vec3::new(x, y, 0.0)) Some(DVec3::new(x, y, 0.0))
} }
fn solve_quadratic(a: f32, b: f32, c: f32) -> Vec<f32> { fn solve_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
if a.abs() < 1e-9 { if a.abs() < 1e-9 {
if b.abs() < 1e-9 { if b.abs() < 1e-9 {
return vec![]; return vec![];
@ -472,7 +476,7 @@ fn solve_quadratic(a: f32, b: f32, c: f32) -> Vec<f32> {
vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)] vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
} }
fn best_of(candidates: &[Vec3], hint: Vec3) -> Option<Vec3> { fn best_of(candidates: &[DVec3], hint: DVec3) -> Option<DVec3> {
candidates.iter().copied().min_by(|a, b| { candidates.iter().copied().min_by(|a, b| {
a.distance(hint) a.distance(hint)
.partial_cmp(&b.distance(hint)) .partial_cmp(&b.distance(hint))
@ -480,7 +484,7 @@ fn best_of(candidates: &[Vec3], hint: Vec3) -> Option<Vec3> {
}) })
} }
fn best_circle_of(candidates: &[(Vec3, f32)], hint: Vec3) -> Option<(Vec3, f32)> { fn best_circle_of(candidates: &[(DVec3, f64)], hint: DVec3) -> Option<(DVec3, f64)> {
candidates candidates
.iter() .iter()
.copied() .copied()
@ -493,7 +497,7 @@ fn best_circle_of(candidates: &[(Vec3, f32)], hint: Vec3) -> Option<(Vec3, f32)>
} }
/// All candidate circle centers tangent to two objects with radius r. /// All candidate circle centers tangent to two objects with radius r.
fn ttr_candidates(obj1: TangentObject, obj2: TangentObject, r: f32) -> Vec<Vec3> { fn ttr_candidates(obj1: TangentObject, obj2: TangentObject, r: f64) -> Vec<DVec3> {
match (obj1, obj2) { match (obj1, obj2) {
(TangentObject::Line { p1: a, p2: b }, TangentObject::Line { p1: c, p2: d }) => { (TangentObject::Line { p1: a, p2: b }, TangentObject::Line { p1: c, p2: d }) => {
let l1 = Line2D::from_obj(a, b); let l1 = Line2D::from_obj(a, b);
@ -540,10 +544,10 @@ fn ttr_candidates(obj1: TangentObject, obj2: TangentObject, r: f32) -> Vec<Vec3>
} }
} }
fn ttr_lc_candidates(l: Line2D, cp: Vec3, cr: f32, r: f32) -> Vec<Vec3> { fn ttr_lc_candidates(l: Line2D, cp: DVec3, cr: f64, r: f64) -> Vec<DVec3> {
let mut out = Vec::new(); let mut out = Vec::new();
for s1 in [-1.0f32, 1.0] { for s1 in [-1.0f64, 1.0] {
for s2 in [-1.0f32, 1.0] { for s2 in [-1.0f64, 1.0] {
let rho = cr + s2 * r; let rho = cr + s2 * r;
if rho < 0.0 { if rho < 0.0 {
continue; continue;
@ -563,23 +567,23 @@ fn ttr_lc_candidates(l: Line2D, cp: Vec3, cr: f32, r: f32) -> Vec<Vec3> {
// (-l.b*t + u)^2 + (l.a*t + v)^2 = rho^2 // (-l.b*t + u)^2 + (l.a*t + v)^2 = rho^2
// (l.b^2 + l.a^2)*t^2 + 2*(-l.b*u + l.a*v)*t + (u^2 + v^2 - rho^2) = 0 // (l.b^2 + l.a^2)*t^2 + 2*(-l.b*u + l.a*v)*t + (u^2 + v^2 - rho^2) = 0
// Since a^2 + b^2 = 1: // Since a^2 + b^2 = 1:
let qa = 1.0f32; let qa = 1.0f64;
let qb = 2.0 * (-l.b * u + l.a * v); let qb = 2.0 * (-l.b * u + l.a * v);
let qc = u * u + v * v - rho * rho; let qc = u * u + v * v - rho * rho;
for t in solve_quadratic(qa, qb, qc) { for t in solve_quadratic(qa, qb, qc) {
let cx = -l.b * t - l.a * c_off; let cx = -l.b * t - l.a * c_off;
let cy = l.a * t - l.b * c_off; let cy = l.a * t - l.b * c_off;
out.push(Vec3::new(cx, cy, 0.0)); out.push(DVec3::new(cx, cy, 0.0));
} }
} }
} }
out out
} }
fn ttr_cc_candidates(c1: Vec3, r1: f32, c2: Vec3, r2: f32, r: f32) -> Vec<Vec3> { fn ttr_cc_candidates(c1: DVec3, r1: f64, c2: DVec3, r2: f64, r: f64) -> Vec<DVec3> {
let mut out = Vec::new(); let mut out = Vec::new();
for s1 in [-1.0f32, 1.0] { for s1 in [-1.0f64, 1.0] {
for s2 in [-1.0f32, 1.0] { for s2 in [-1.0f64, 1.0] {
let rho1 = r1 + s1 * r; let rho1 = r1 + s1 * r;
let rho2 = r2 + s2 * r; let rho2 = r2 + s2 * r;
if rho1 < 0.0 || rho2 < 0.0 { if rho1 < 0.0 || rho2 < 0.0 {
@ -605,10 +609,10 @@ fn ttr_cc_candidates(c1: Vec3, r1: f32, c2: Vec3, r2: f32, r: f32) -> Vec<Vec3>
if disc < 0.0 { if disc < 0.0 {
continue; continue;
} }
for sign in [-1.0f32, 1.0] { for sign in [-1.0f64, 1.0] {
let cx = (-qb + sign * disc.sqrt()) / (2.0 * qa); let cx = (-qb + sign * disc.sqrt()) / (2.0 * qa);
let cy = (k - ax * cx) * iy; let cy = (k - ax * cx) * iy;
out.push(Vec3::new(cx, cy, 0.0)); out.push(DVec3::new(cx, cy, 0.0));
} }
} else { } else {
let ix = 1.0 / ax; let ix = 1.0 / ax;
@ -620,10 +624,10 @@ fn ttr_cc_candidates(c1: Vec3, r1: f32, c2: Vec3, r2: f32, r: f32) -> Vec<Vec3>
if disc < 0.0 { if disc < 0.0 {
continue; continue;
} }
for sign in [-1.0f32, 1.0] { for sign in [-1.0f64, 1.0] {
let cy = (-qb + sign * disc.sqrt()) / (2.0 * qa); let cy = (-qb + sign * disc.sqrt()) / (2.0 * qa);
let cx = (k - ay * cy) * ix; let cx = (k - ay * cy) * ix;
out.push(Vec3::new(cx, cy, 0.0)); out.push(DVec3::new(cx, cy, 0.0));
} }
} }
} }
@ -636,10 +640,10 @@ fn ttt_candidates(
obj1: TangentObject, obj1: TangentObject,
obj2: TangentObject, obj2: TangentObject,
obj3: TangentObject, obj3: TangentObject,
) -> Vec<(Vec3, f32)> { ) -> Vec<(DVec3, f64)> {
let objs = [obj1, obj2, obj3]; let objs = [obj1, obj2, obj3];
let mut results = Vec::new(); let mut results = Vec::new();
let sign_combos: [[f32; 3]; 8] = [ let sign_combos: [[f64; 3]; 8] = [
[-1., -1., -1.], [-1., -1., -1.],
[-1., -1., 1.], [-1., -1., 1.],
[-1., 1., -1.], [-1., 1., -1.],
@ -661,13 +665,13 @@ fn ttt_candidates(
// LinEq: lx*cx + ly*cy + lr*r = k // LinEq: lx*cx + ly*cy + lr*r = k
struct LinEq { struct LinEq {
lx: f32, lx: f64,
ly: f32, ly: f64,
lr: f32, lr: f64,
k: f32, k: f64,
} }
fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f32; 3]) -> Vec<(Vec3, f32)> { fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64)> {
let mut lin_eqs: Vec<LinEq> = Vec::new(); let mut lin_eqs: Vec<LinEq> = Vec::new();
let mut circle_idx: Vec<usize> = Vec::new(); let mut circle_idx: Vec<usize> = Vec::new();
@ -728,7 +732,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f32; 3]) -> Vec<(Vec3, f32)>
let a_cy = (e0.lx * e1.k - e1.lx * e0.k) / det; let a_cy = (e0.lx * e1.k - e1.lx * e0.k) / det;
let b_cy = -(e0.lx * e1.lr - e1.lx * e0.lr) / det; let b_cy = -(e0.lx * e1.lr - e1.lx * e0.lr) / det;
let r_vals: Vec<f32> = if lin_eqs.len() >= 3 { let r_vals: Vec<f64> = if lin_eqs.len() >= 3 {
let e2 = &lin_eqs[2]; let e2 = &lin_eqs[2];
let r_coeff = b_cx * e2.lx + b_cy * e2.ly + e2.lr; let r_coeff = b_cx * e2.lx + b_cy * e2.ly + e2.lr;
let r_const = e2.k - a_cx * e2.lx - a_cy * e2.ly; let r_const = e2.k - a_cx * e2.lx - a_cy * e2.ly;
@ -766,7 +770,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f32; 3]) -> Vec<(Vec3, f32)>
} }
let cx = a_cx + b_cx * r; let cx = a_cx + b_cx * r;
let cy = a_cy + b_cy * r; let cy = a_cy + b_cy * r;
Some((Vec3::new(cx, cy, 0.0), r)) Some((DVec3::new(cx, cy, 0.0), r))
}) })
.collect() .collect()
} }
@ -781,13 +785,13 @@ enum StepTTR {
First, First,
Second { Second {
obj1: TangentObject, obj1: TangentObject,
hit1: Vec3, hit1: DVec3,
}, },
Radius { Radius {
obj1: TangentObject, obj1: TangentObject,
obj2: TangentObject, obj2: TangentObject,
hit1: Vec3, hit1: DVec3,
hit2: Vec3, hit2: DVec3,
}, },
} }
@ -828,11 +832,11 @@ impl CadCommand for CircleTTRCommand {
} }
} }
fn on_point(&mut self, _pt: Vec3) -> CmdResult { fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult { fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult {
match &self.step { match &self.step {
StepTTR::First => { StepTTR::First => {
self.step = StepTTR::Second { self.step = StepTTR::Second {
@ -856,7 +860,7 @@ impl CadCommand for CircleTTRCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let r: f32 = text.trim().parse().ok()?; let r: f64 = text.trim().parse().ok()?;
if r <= 0.0 { if r <= 0.0 {
return Some(CmdResult::Cancel); return Some(CmdResult::Cancel);
} }
@ -870,7 +874,7 @@ impl CadCommand for CircleTTRCommand {
let hint = (*hit1 + *hit2) * 0.5; let hint = (*hit1 + *hit2) * 0.5;
let candidates = ttr_candidates(*obj1, *obj2, r); let candidates = ttr_candidates(*obj1, *obj2, r);
if let Some(center) = best_of(&candidates, hint) { if let Some(center) = best_of(&candidates, hint) {
Some(CmdResult::CommitAndExit(make_circle(center, r as f64))) Some(CmdResult::CommitAndExit(make_circle(center, r)))
} else { } else {
Some(CmdResult::Cancel) Some(CmdResult::Cancel)
} }
@ -885,7 +889,7 @@ impl CadCommand for CircleTTRCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
None None
} }
} }
@ -894,7 +898,7 @@ impl CadCommand for CircleTTRCommand {
pub struct CircleTTTCommand { pub struct CircleTTTCommand {
objs: Vec<TangentObject>, objs: Vec<TangentObject>,
hits: Vec<Vec3>, hits: Vec<DVec3>,
} }
impl CircleTTTCommand { impl CircleTTTCommand {
@ -923,20 +927,20 @@ impl CadCommand for CircleTTTCommand {
} }
} }
fn on_point(&mut self, _pt: Vec3) -> CmdResult { fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult { fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult {
self.objs.push(obj); self.objs.push(obj);
self.hits.push(hit); self.hits.push(hit);
if self.objs.len() < 3 { if self.objs.len() < 3 {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
let hint = self.hits.iter().fold(Vec3::ZERO, |a, &b| a + b) / 3.0; let hint = self.hits.iter().fold(DVec3::ZERO, |a, &b| a + b) / 3.0;
let candidates = ttt_candidates(self.objs[0], self.objs[1], self.objs[2]); let candidates = ttt_candidates(self.objs[0], self.objs[1], self.objs[2]);
match best_circle_of(&candidates, hint) { match best_circle_of(&candidates, hint) {
Some((center, r)) => CmdResult::CommitAndExit(make_circle(center, r as f64)), Some((center, r)) => CmdResult::CommitAndExit(make_circle(center, r)),
None => { None => {
self.objs.pop(); self.objs.pop();
self.hits.pop(); self.hits.pop();
@ -951,7 +955,7 @@ impl CadCommand for CircleTTTCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
None None
} }
} }

View file

@ -11,17 +11,17 @@ use acadrust::{Ellipse, EntityType};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::Vec3; use glam::DVec3;
fn parse_f32(text: &str) -> Option<f32> { fn parse_num(text: &str) -> Option<f64> {
text.trim().replace(',', ".").parse().ok() text.trim().replace(',', ".").parse().ok()
} }
const TAU: f32 = std::f32::consts::TAU; const TAU: f64 = std::f64::consts::TAU;
/// Minimum swept angle before the previewed arc may flip CW/CCW — filters the /// Minimum swept angle before the previewed arc may flip CW/CCW — filters the
/// per-frame cursor jitter that otherwise reverses the sweep on tiny moves. /// per-frame cursor jitter that otherwise reverses the sweep on tiny moves.
const DIR_TOL: f32 = 0.1745; // ~10° const DIR_TOL: f64 = 0.1745; // ~10°
// ── Icons ───────────────────────────────────────────────────────────────── // ── Icons ─────────────────────────────────────────────────────────────────
@ -51,33 +51,33 @@ pub const ICON: IconKind = ICON_CTR;
/// Preview wire for a full or partial ellipse. /// Preview wire for a full or partial ellipse.
fn ellipse_wire( fn ellipse_wire(
center: Vec3, center: DVec3,
major: Vec3, // vector from center to major-axis endpoint major: DVec3, // vector from center to major-axis endpoint
ratio: f32, // minor/major ratio: f64, // minor/major
t_start: f32, t_start: f64,
t_end: f32, t_end: f64,
) -> WireModel { ) -> WireModel {
let r_major = major.length(); let r_major = major.length();
if r_major < 1e-9 { if r_major < 1e-9 {
return WireModel::solid("rubber_band".into(), vec![], WireModel::CYAN, false); return WireModel::solid("rubber_band".into(), vec![], WireModel::CYAN, false);
} }
let major_dir = major / r_major; let major_dir = major / r_major;
let v = Vec3::Z.cross(major_dir).normalize(); let v = DVec3::Z.cross(major_dir).normalize();
let segs = 64u32; let segs = 64u32;
// Unwrap t_end so the arc goes counter-clockwise. // Unwrap t_end so the arc goes counter-clockwise.
let t_e = if t_end <= t_start { t_end + TAU } else { t_end }; let t_e = if t_end <= t_start { t_end + TAU } else { t_end };
let pts: Vec<[f32; 3]> = (0..=segs) let pts: Vec<[f32; 3]> = (0..=segs)
.map(|i| { .map(|i| {
let t = t_start + (t_e - t_start) * (i as f32 / segs as f32); let t = t_start + (t_e - t_start) * (i as f64 / segs as f64);
let p = center + t.cos() * r_major * major_dir + t.sin() * r_major * ratio * v; let p = center + t.cos() * r_major * major_dir + t.sin() * r_major * ratio * v;
[p.x, p.y, p.z] [p.x as f32, p.y as f32, p.z as f32]
}) })
.collect(); .collect();
WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false) WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false)
} }
/// Convert a world point to the parametric angle on the ellipse. /// Convert a world point to the parametric angle on the ellipse.
fn param_angle(center: Vec3, major_dir: Vec3, v: Vec3, pt: Vec3, ratio: f32) -> f32 { fn param_angle(center: DVec3, major_dir: DVec3, v: DVec3, pt: DVec3, ratio: f64) -> f64 {
let d = pt - center; let d = pt - center;
let u_proj = d.dot(major_dir); let u_proj = d.dot(major_dir);
let v_proj = d.dot(v); let v_proj = d.dot(v);
@ -87,13 +87,13 @@ fn param_angle(center: Vec3, major_dir: Vec3, v: Vec3, pt: Vec3, ratio: f32) ->
} }
/// Build the final Ellipse entity. /// Build the final Ellipse entity.
fn make_ellipse(center: Vec3, major: Vec3, ratio: f32, t_start: f32, t_end: f32) -> Ellipse { fn make_ellipse(center: DVec3, major: DVec3, ratio: f64, t_start: f64, t_end: f64) -> Ellipse {
Ellipse { Ellipse {
center: Vector3::new(center.x as f64, center.y as f64, center.z as f64), center: Vector3::new(center.x, center.y, center.z),
major_axis: Vector3::new(major.x as f64, major.y as f64, major.z as f64), major_axis: Vector3::new(major.x, major.y, major.z),
minor_axis_ratio: ratio as f64, minor_axis_ratio: ratio,
start_parameter: t_start as f64, start_parameter: t_start,
end_parameter: t_end as f64, end_parameter: t_end,
..Default::default() ..Default::default()
} }
} }
@ -103,8 +103,8 @@ fn make_ellipse(center: Vec3, major: Vec3, ratio: f32, t_start: f32, t_end: f32)
enum CtrStep { enum CtrStep {
Center, Center,
MajorAxis { center: Vec3 }, MajorAxis { center: DVec3 },
MinorRatio { center: Vec3, major: Vec3 }, MinorRatio { center: DVec3, major: DVec3 },
} }
pub struct EllipseCommand { pub struct EllipseCommand {
@ -135,7 +135,7 @@ impl CadCommand for EllipseCommand {
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step { match &self.step {
CtrStep::Center => { CtrStep::Center => {
self.step = CtrStep::MajorAxis { center: pt }; self.step = CtrStep::MajorAxis { center: pt };
@ -165,7 +165,7 @@ impl CadCommand for EllipseCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let CtrStep::MinorRatio { center, major } = &self.step { if let CtrStep::MinorRatio { center, major } = &self.step {
let r_minor = parse_f32(text)?; let r_minor = parse_num(text)?;
if r_minor > 0.0 { if r_minor > 0.0 {
let ratio = (r_minor / major.length()).clamp(1e-6, 1.0); let ratio = (r_minor / major.length()).clamp(1e-6, 1.0);
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
@ -176,7 +176,7 @@ impl CadCommand for EllipseCommand {
None None
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match &self.step { match &self.step {
CtrStep::MajorAxis { center } => Some(line_wire(*center, pt)), CtrStep::MajorAxis { center } => Some(line_wire(*center, pt)),
CtrStep::MinorRatio { center, major } => { CtrStep::MinorRatio { center, major } => {
@ -204,9 +204,9 @@ impl CadCommand for EllipseCommand {
} }
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
if let CtrStep::MinorRatio { center, major } = &self.step { if let CtrStep::MinorRatio { center, major } = &self.step {
Some((minor_ratio(*center, *major, cursor) * major.length()) as f64) Some(minor_ratio(*center, *major, cursor) * major.length())
} else { } else {
None None
} }
@ -218,8 +218,8 @@ impl CadCommand for EllipseCommand {
enum AxisStep { enum AxisStep {
Pt1, Pt1,
Pt2 { p1: Vec3 }, Pt2 { p1: DVec3 },
MinorRatio { center: Vec3, major: Vec3 }, MinorRatio { center: DVec3, major: DVec3 },
} }
pub struct EllipseAxisCommand { pub struct EllipseAxisCommand {
@ -250,7 +250,7 @@ impl CadCommand for EllipseAxisCommand {
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step { match &self.step {
AxisStep::Pt1 => { AxisStep::Pt1 => {
self.step = AxisStep::Pt2 { p1: pt }; self.step = AxisStep::Pt2 { p1: pt };
@ -278,7 +278,7 @@ impl CadCommand for EllipseAxisCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let AxisStep::MinorRatio { center, major } = &self.step { if let AxisStep::MinorRatio { center, major } = &self.step {
let r_minor = parse_f32(text)?; let r_minor = parse_num(text)?;
if r_minor > 0.0 { if r_minor > 0.0 {
let ratio = (r_minor / major.length()).clamp(1e-6, 1.0); let ratio = (r_minor / major.length()).clamp(1e-6, 1.0);
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
@ -289,7 +289,7 @@ impl CadCommand for EllipseAxisCommand {
None None
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match &self.step { match &self.step {
AxisStep::Pt1 => None, AxisStep::Pt1 => None,
AxisStep::Pt2 { p1 } => Some(line_wire(*p1, pt)), AxisStep::Pt2 { p1 } => Some(line_wire(*p1, pt)),
@ -316,9 +316,9 @@ impl CadCommand for EllipseAxisCommand {
} }
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
if let AxisStep::MinorRatio { center, major } = &self.step { if let AxisStep::MinorRatio { center, major } = &self.step {
Some((minor_ratio(*center, *major, cursor) * major.length()) as f64) Some(minor_ratio(*center, *major, cursor) * major.length())
} else { } else {
None None
} }
@ -331,28 +331,28 @@ impl CadCommand for EllipseAxisCommand {
enum ArcStep { enum ArcStep {
Center, Center,
MajorAxis { MajorAxis {
center: Vec3, center: DVec3,
}, },
MinorRatio { MinorRatio {
center: Vec3, center: DVec3,
major: Vec3, major: DVec3,
}, },
StartAngle { StartAngle {
center: Vec3, center: DVec3,
major: Vec3, major: DVec3,
ratio: f32, ratio: f64,
}, },
EndAngle { EndAngle {
center: Vec3, center: DVec3,
major: Vec3, major: DVec3,
ratio: f32, ratio: f64,
t_start: f32, t_start: f64,
}, },
} }
pub struct EllipseArcCommand { pub struct EllipseArcCommand {
step: ArcStep, step: ArcStep,
prev_pt: Option<Vec3>, prev_pt: Option<DVec3>,
cw: bool, cw: bool,
} }
@ -389,7 +389,7 @@ impl CadCommand for EllipseArcCommand {
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step { match &self.step {
ArcStep::Center => { ArcStep::Center => {
self.step = ArcStep::MajorAxis { center: pt }; self.step = ArcStep::MajorAxis { center: pt };
@ -452,7 +452,7 @@ impl CadCommand for EllipseArcCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let val = parse_f32(text)?; let val = parse_num(text)?;
match &self.step { match &self.step {
ArcStep::MinorRatio { center, major } => { ArcStep::MinorRatio { center, major } => {
if val > 0.0 { if val > 0.0 {
@ -499,7 +499,7 @@ impl CadCommand for EllipseArcCommand {
None None
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match &self.step { match &self.step {
ArcStep::MajorAxis { center } => Some(line_wire(*center, pt)), ArcStep::MajorAxis { center } => Some(line_wire(*center, pt)),
ArcStep::MinorRatio { center, major } => { ArcStep::MinorRatio { center, major } => {
@ -528,10 +528,10 @@ impl CadCommand for EllipseArcCommand {
let t_prev = angle_from_point(*center, *major, *ratio, prev); let t_prev = angle_from_point(*center, *major, *ratio, prev);
let t_cur = angle_from_point(*center, *major, *ratio, pt); let t_cur = angle_from_point(*center, *major, *ratio, pt);
let mut d = t_cur - t_prev; let mut d = t_cur - t_prev;
while d > std::f32::consts::PI { while d > std::f64::consts::PI {
d -= TAU; d -= TAU;
} }
while d <= -std::f32::consts::PI { while d <= -std::f64::consts::PI {
d += TAU; d += TAU;
} }
if d.abs() > DIR_TOL { if d.abs() > DIR_TOL {
@ -576,9 +576,9 @@ impl CadCommand for EllipseArcCommand {
} }
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
if let ArcStep::MinorRatio { center, major } = &self.step { if let ArcStep::MinorRatio { center, major } = &self.step {
Some((minor_ratio(*center, *major, cursor) * major.length()) as f64) Some(minor_ratio(*center, *major, cursor) * major.length())
} else { } else {
None None
} }
@ -587,7 +587,7 @@ impl CadCommand for EllipseArcCommand {
// ── Internal helpers ────────────────────────────────────────────────────── // ── Internal helpers ──────────────────────────────────────────────────────
fn minor_ratio(center: Vec3, major: Vec3, pt: Vec3) -> f32 { fn minor_ratio(center: DVec3, major: DVec3, pt: DVec3) -> f64 {
let r_major = major.length(); let r_major = major.length();
if r_major < 1e-9 { if r_major < 1e-9 {
return 0.5; return 0.5;
@ -599,20 +599,23 @@ fn minor_ratio(center: Vec3, major: Vec3, pt: Vec3) -> f32 {
(r_minor / r_major).clamp(1e-6, 1.0) (r_minor / r_major).clamp(1e-6, 1.0)
} }
fn angle_from_point(center: Vec3, major: Vec3, ratio: f32, pt: Vec3) -> f32 { fn angle_from_point(center: DVec3, major: DVec3, ratio: f64, pt: DVec3) -> f64 {
let r_major = major.length(); let r_major = major.length();
if r_major < 1e-9 { if r_major < 1e-9 {
return 0.0; return 0.0;
} }
let major_dir = major / r_major; let major_dir = major / r_major;
let v = Vec3::Z.cross(major_dir).normalize(); let v = DVec3::Z.cross(major_dir).normalize();
param_angle(center, major_dir, v, pt, ratio) param_angle(center, major_dir, v, pt, ratio)
} }
fn line_wire(from: Vec3, to: Vec3) -> WireModel { fn line_wire(from: DVec3, to: DVec3) -> WireModel {
WireModel { WireModel {
name: "rubber_band".into(), name: "rubber_band".into(),
points: vec![[from.x, from.y, from.z], [to.x, to.y, to.z]], points: vec![
[from.x as f32, from.y as f32, from.z as f32],
[to.x as f32, to.y as f32, to.z as f32],
],
points_low: Vec::new(), points_low: Vec::new(),
color: WireModel::CYAN, color: WireModel::CYAN,
selected: false, selected: false,

View file

@ -17,35 +17,37 @@ use acadrust::{EntityType, LwPolyline};
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::IconKind; use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
use glam::{Mat4, Vec3}; use glam::{DVec3, Mat4};
/// Build the four corners of an axis-aligned box between opposite corners `a` /// Build the four corners of an axis-aligned box between opposite corners `a`
/// and `b`, axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = /// and `b`, axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity =
/// world). The two given corners stay put; the other two are placed square to /// world). The two given corners stay put; the other two are placed square to
/// the UCS axes instead of the world axes. /// the UCS axes instead of the world axes.
fn ucs_box_corners(a: Vec3, b: Vec3, ucs: Mat4) -> [Vec3; 4] { fn ucs_box_corners(a: DVec3, b: DVec3, ucs: Mat4) -> [DVec3; 4] {
let ucs = ucs.as_dmat4();
let inv = ucs.inverse(); let inv = ucs.inverse();
let au = inv.transform_point3(a); let au = inv.transform_point3(a);
let bu = inv.transform_point3(b); let bu = inv.transform_point3(b);
[ [
ucs.transform_point3(Vec3::new(au.x, au.y, au.z)), ucs.transform_point3(DVec3::new(au.x, au.y, au.z)),
ucs.transform_point3(Vec3::new(bu.x, au.y, au.z)), ucs.transform_point3(DVec3::new(bu.x, au.y, au.z)),
ucs.transform_point3(Vec3::new(bu.x, bu.y, au.z)), ucs.transform_point3(DVec3::new(bu.x, bu.y, au.z)),
ucs.transform_point3(Vec3::new(au.x, bu.y, au.z)), ucs.transform_point3(DVec3::new(au.x, bu.y, au.z)),
] ]
} }
/// Four corners of a box centred at `c` with half-extents taken from `corner`, /// Four corners of a box centred at `c` with half-extents taken from `corner`,
/// axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = world). /// axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = world).
fn ucs_box_around_center(c: Vec3, corner: Vec3, ucs: Mat4) -> [Vec3; 4] { fn ucs_box_around_center(c: DVec3, corner: DVec3, ucs: Mat4) -> [DVec3; 4] {
let ucs = ucs.as_dmat4();
let d = ucs.inverse().transform_vector3(corner - c); let d = ucs.inverse().transform_vector3(corner - c);
let rx = ucs.transform_vector3(Vec3::new(d.x.abs(), 0.0, 0.0)); let rx = ucs.transform_vector3(DVec3::new(d.x.abs(), 0.0, 0.0));
let ry = ucs.transform_vector3(Vec3::new(0.0, d.y.abs(), 0.0)); let ry = ucs.transform_vector3(DVec3::new(0.0, d.y.abs(), 0.0));
[c - rx - ry, c + rx - ry, c + rx + ry, c - rx + ry] [c - rx - ry, c + rx - ry, c + rx + ry, c - rx + ry]
} }
const TAU: f32 = std::f32::consts::TAU; const TAU: f64 = std::f64::consts::TAU;
const PI: f32 = std::f32::consts::PI; const PI: f64 = std::f64::consts::PI;
// ── Icons ────────────────────────────────────────────────────────────────── // ── Icons ──────────────────────────────────────────────────────────────────
@ -100,10 +102,13 @@ fn wire_loop(pts: Vec<[f32; 3]>) -> WireModel {
WireModel::solid("rubber_band".into(), p, WireModel::CYAN, false) WireModel::solid("rubber_band".into(), p, WireModel::CYAN, false)
} }
fn wire_seg(a: Vec3, b: Vec3) -> WireModel { fn wire_seg(a: DVec3, b: DVec3) -> WireModel {
WireModel::solid( WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
vec![[a.x, a.y, a.z], [b.x, b.y, b.z]], vec![
[a.x as f32, a.y as f32, a.z as f32],
[b.x as f32, b.y as f32, b.z as f32],
],
WireModel::CYAN, WireModel::CYAN,
false, false,
) )
@ -111,40 +116,40 @@ fn wire_seg(a: Vec3, b: Vec3) -> WireModel {
// ── Polygon geometry ─────────────────────────────────────────────────────── // ── Polygon geometry ───────────────────────────────────────────────────────
fn poly_verts_xy(center: Vec3, vertex_r: f32, sides: u32, start_angle: f32) -> Vec<[f64; 2]> { fn poly_verts_xy(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> Vec<[f64; 2]> {
(0..sides) (0..sides)
.map(|i| { .map(|i| {
let a = start_angle + (i as f32) * TAU / sides as f32; let a = start_angle + (i as f64) * TAU / sides as f64;
[ [
(center.x + vertex_r * a.cos()) as f64, center.x + vertex_r * a.cos(),
(center.y + vertex_r * a.sin()) as f64, center.y + vertex_r * a.sin(),
] ]
}) })
.collect() .collect()
} }
fn poly_wire(center: Vec3, vertex_r: f32, sides: u32, start_angle: f32) -> WireModel { fn poly_wire(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> WireModel {
let pts: Vec<[f32; 3]> = (0..sides) let pts: Vec<[f32; 3]> = (0..sides)
.map(|i| { .map(|i| {
let a = start_angle + (i as f32) * TAU / sides as f32; let a = start_angle + (i as f64) * TAU / sides as f64;
[ [
center.x + vertex_r * a.cos(), (center.x + vertex_r * a.cos()) as f32,
center.y + vertex_r * a.sin(), (center.y + vertex_r * a.sin()) as f32,
center.z, center.z as f32,
] ]
}) })
.collect(); .collect();
wire_loop(pts) wire_loop(pts)
} }
fn angle_xy(from: Vec3, to: Vec3) -> f32 { fn angle_xy(from: DVec3, to: DVec3) -> f64 {
(to.y - from.y).atan2(to.x - from.x) (to.y - from.y).atan2(to.x - from.x)
} }
// ── Command: Rectangle — Two Corners (RECT) ────────────────────────────── // ── Command: Rectangle — Two Corners (RECT) ──────────────────────────────
pub struct RectCommand { pub struct RectCommand {
a: Option<Vec3>, a: Option<DVec3>,
ucs: Mat4, ucs: Mat4,
} }
@ -168,7 +173,7 @@ impl CadCommand for RectCommand {
"RECT Specify opposite corner:".into() "RECT Specify opposite corner:".into()
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.a { match self.a {
None => { None => {
self.a = Some(pt); self.a = Some(pt);
@ -177,10 +182,10 @@ impl CadCommand for RectCommand {
Some(a) => { Some(a) => {
let c = ucs_box_corners(a, pt, self.ucs); let c = ucs_box_corners(a, pt, self.ucs);
CmdResult::CommitAndExit(make_pline(&[ CmdResult::CommitAndExit(make_pline(&[
[c[0].x as f64, c[0].y as f64], [c[0].x, c[0].y],
[c[1].x as f64, c[1].y as f64], [c[1].x, c[1].y],
[c[2].x as f64, c[2].y as f64], [c[2].x, c[2].y],
[c[3].x as f64, c[3].y as f64], [c[3].x, c[3].y],
])) ]))
} }
} }
@ -191,14 +196,14 @@ impl CadCommand for RectCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let a = self.a?; let a = self.a?;
let c = ucs_box_corners(a, pt, self.ucs); let c = ucs_box_corners(a, pt, self.ucs);
Some(wire_loop(vec![ Some(wire_loop(vec![
[c[0].x, c[0].y, c[0].z], [c[0].x as f32, c[0].y as f32, c[0].z as f32],
[c[1].x, c[1].y, c[1].z], [c[1].x as f32, c[1].y as f32, c[1].z as f32],
[c[2].x, c[2].y, c[2].z], [c[2].x as f32, c[2].y as f32, c[2].z as f32],
[c[3].x, c[3].y, c[3].z], [c[3].x as f32, c[3].y as f32, c[3].z as f32],
])) ]))
} }
fn dyn_spec(&self) -> Option<crate::command::DynSpec> { fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
@ -225,16 +230,16 @@ impl CadCommand for RectCommand {
pub struct RectRotCommand { pub struct RectRotCommand {
step: u8, step: u8,
a: Vec3, a: DVec3,
b: Vec3, b: DVec3,
} }
impl RectRotCommand { impl RectRotCommand {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
step: 0, step: 0,
a: Vec3::ZERO, a: DVec3::ZERO,
b: Vec3::ZERO, b: DVec3::ZERO,
} }
} }
} }
@ -250,7 +255,7 @@ impl CadCommand for RectRotCommand {
_ => "RECT ROT Specify height (perpendicular pick):".into(), _ => "RECT ROT Specify height (perpendicular pick):".into(),
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.a = pt; self.a = pt;
@ -265,15 +270,15 @@ impl CadCommand for RectRotCommand {
_ => { _ => {
let (a, b) = (self.a, self.b); let (a, b) = (self.a, self.b);
let dir = (b - a).normalize_or_zero(); let dir = (b - a).normalize_or_zero();
let perp = Vec3::new(-dir.y, dir.x, 0.0); let perp = DVec3::new(-dir.y, dir.x, 0.0);
let h = (pt - b).dot(perp); // signed height let h = (pt - b).dot(perp); // signed height
let c = b + perp * h; let c = b + perp * h;
let d = a + perp * h; let d = a + perp * h;
CmdResult::CommitAndExit(make_pline(&[ CmdResult::CommitAndExit(make_pline(&[
[a.x as f64, a.y as f64], [a.x, a.y],
[b.x as f64, b.y as f64], [b.x, b.y],
[c.x as f64, c.y as f64], [c.x, c.y],
[d.x as f64, d.y as f64], [d.x, d.y],
])) ]))
} }
} }
@ -284,21 +289,21 @@ impl CadCommand for RectRotCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step { match self.step {
1 => Some(wire_seg(self.a, pt)), 1 => Some(wire_seg(self.a, pt)),
2 => { 2 => {
let (a, b) = (self.a, self.b); let (a, b) = (self.a, self.b);
let dir = (b - a).normalize_or_zero(); let dir = (b - a).normalize_or_zero();
let perp = Vec3::new(-dir.y, dir.x, 0.0); let perp = DVec3::new(-dir.y, dir.x, 0.0);
let h = (pt - b).dot(perp); let h = (pt - b).dot(perp);
let c = b + perp * h; let c = b + perp * h;
let d = a + perp * h; let d = a + perp * h;
Some(wire_loop(vec![ Some(wire_loop(vec![
[a.x, a.y, a.z], [a.x as f32, a.y as f32, a.z as f32],
[b.x, b.y, b.z], [b.x as f32, b.y as f32, b.z as f32],
[c.x, c.y, c.z], [c.x as f32, c.y as f32, c.z as f32],
[d.x, d.y, d.z], [d.x as f32, d.y as f32, d.z as f32],
])) ]))
} }
_ => None, _ => None,
@ -319,12 +324,12 @@ impl CadCommand for RectRotCommand {
}) })
} }
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
// Live height = perpendicular distance from the cursor to the base edge. // Live height = perpendicular distance from the cursor to the base edge.
(self.step == 2).then(|| { (self.step == 2).then(|| {
let dir = (self.b - self.a).normalize_or_zero(); let dir = (self.b - self.a).normalize_or_zero();
let perp = Vec3::new(-dir.y, dir.x, 0.0); let perp = DVec3::new(-dir.y, dir.x, 0.0);
(cursor - self.b).dot(perp).abs() as f64 (cursor - self.b).dot(perp).abs()
}) })
} }
} }
@ -334,7 +339,7 @@ impl CadCommand for RectRotCommand {
// Step 1: pick any corner (half-width = |cx|, half-height = |cy|) // Step 1: pick any corner (half-width = |cx|, half-height = |cy|)
pub struct RectCenCommand { pub struct RectCenCommand {
center: Option<Vec3>, center: Option<DVec3>,
ucs: Mat4, ucs: Mat4,
} }
@ -358,7 +363,7 @@ impl CadCommand for RectCenCommand {
"RECT CEN Specify corner point:".into() "RECT CEN Specify corner point:".into()
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.center { match self.center {
None => { None => {
self.center = Some(pt); self.center = Some(pt);
@ -367,10 +372,10 @@ impl CadCommand for RectCenCommand {
Some(c) => { Some(c) => {
let q = ucs_box_around_center(c, pt, self.ucs); let q = ucs_box_around_center(c, pt, self.ucs);
CmdResult::CommitAndExit(make_pline(&[ CmdResult::CommitAndExit(make_pline(&[
[q[0].x as f64, q[0].y as f64], [q[0].x, q[0].y],
[q[1].x as f64, q[1].y as f64], [q[1].x, q[1].y],
[q[2].x as f64, q[2].y as f64], [q[2].x, q[2].y],
[q[3].x as f64, q[3].y as f64], [q[3].x, q[3].y],
])) ]))
} }
} }
@ -381,14 +386,14 @@ impl CadCommand for RectCenCommand {
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let c = self.center?; let c = self.center?;
let q = ucs_box_around_center(c, pt, self.ucs); let q = ucs_box_around_center(c, pt, self.ucs);
Some(wire_loop(vec![ Some(wire_loop(vec![
[q[0].x, q[0].y, q[0].z], [q[0].x as f32, q[0].y as f32, q[0].z as f32],
[q[1].x, q[1].y, q[1].z], [q[1].x as f32, q[1].y as f32, q[1].z as f32],
[q[2].x, q[2].y, q[2].z], [q[2].x as f32, q[2].y as f32, q[2].z as f32],
[q[3].x, q[3].y, q[3].z], [q[3].x as f32, q[3].y as f32, q[3].z as f32],
])) ]))
} }
fn dyn_spec(&self) -> Option<crate::command::DynSpec> { fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
@ -414,7 +419,7 @@ impl CadCommand for RectCenCommand {
pub struct PolyCommand { pub struct PolyCommand {
sides: u32, sides: u32,
step: u8, step: u8,
center: Vec3, center: DVec3,
} }
impl PolyCommand { impl PolyCommand {
@ -422,7 +427,7 @@ impl PolyCommand {
Self { Self {
sides: 6, sides: 6,
step: 0, step: 0,
center: Vec3::ZERO, center: DVec3::ZERO,
} }
} }
} }
@ -479,7 +484,7 @@ impl CadCommand for PolyCommand {
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
// User clicked without typing sides: use default, treat click as center. // User clicked without typing sides: use default, treat click as center.
@ -511,7 +516,7 @@ impl CadCommand for PolyCommand {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if self.step < 2 { if self.step < 2 {
return None; return None;
} }
@ -528,7 +533,7 @@ impl CadCommand for PolyCommand {
pub struct PolyCCommand { pub struct PolyCCommand {
sides: u32, sides: u32,
step: u8, step: u8,
center: Vec3, center: DVec3,
} }
impl PolyCCommand { impl PolyCCommand {
@ -536,7 +541,7 @@ impl PolyCCommand {
Self { Self {
sides: 6, sides: 6,
step: 0, step: 0,
center: Vec3::ZERO, center: DVec3::ZERO,
} }
} }
} }
@ -593,7 +598,7 @@ impl CadCommand for PolyCCommand {
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.center = pt; self.center = pt;
@ -607,11 +612,11 @@ impl CadCommand for PolyCCommand {
} }
_ => { _ => {
let inradius = self.center.distance(pt); let inradius = self.center.distance(pt);
let vr = inradius / (PI / self.sides as f32).cos(); let vr = inradius / (PI / self.sides as f64).cos();
// The picked pt is at the midpoint of an edge; the vertex is // The picked pt is at the midpoint of an edge; the vertex is
// offset by half a sector (π/N) from that direction. // offset by half a sector (π/N) from that direction.
let edge_angle = angle_xy(self.center, pt); let edge_angle = angle_xy(self.center, pt);
let sa = edge_angle + PI / self.sides as f32; let sa = edge_angle + PI / self.sides as f64;
CmdResult::CommitAndExit(make_pline(&poly_verts_xy( CmdResult::CommitAndExit(make_pline(&poly_verts_xy(
self.center, self.center,
vr, vr,
@ -633,13 +638,13 @@ impl CadCommand for PolyCCommand {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if self.step < 2 { if self.step < 2 {
return None; return None;
} }
let inradius = self.center.distance(pt); let inradius = self.center.distance(pt);
let vr = inradius / (PI / self.sides as f32).cos(); let vr = inradius / (PI / self.sides as f64).cos();
let sa = angle_xy(self.center, pt) + PI / self.sides as f32; let sa = angle_xy(self.center, pt) + PI / self.sides as f64;
Some(poly_wire(self.center, vr, self.sides, sa)) Some(poly_wire(self.center, vr, self.sides, sa))
} }
} }
@ -651,7 +656,7 @@ impl CadCommand for PolyCCommand {
pub struct PolyECommand { pub struct PolyECommand {
sides: u32, sides: u32,
step: u8, step: u8,
a: Vec3, a: DVec3,
} }
impl PolyECommand { impl PolyECommand {
@ -659,7 +664,7 @@ impl PolyECommand {
Self { Self {
sides: 6, sides: 6,
step: 0, step: 0,
a: Vec3::ZERO, a: DVec3::ZERO,
} }
} }
} }
@ -705,7 +710,7 @@ impl CadCommand for PolyECommand {
} }
} }
fn on_point(&mut self, pt: Vec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step { match self.step {
0 => { 0 => {
self.a = pt; self.a = pt;
@ -738,7 +743,7 @@ impl CadCommand for PolyECommand {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if self.step < 2 { if self.step < 2 {
return None; return None;
} }
@ -752,18 +757,18 @@ impl CadCommand for PolyECommand {
/// Compute polygon center, vertex-radius and start-angle from two edge endpoints. /// Compute polygon center, vertex-radius and start-angle from two edge endpoints.
/// The polygon is placed on the left side of A→B (CCW convention). /// The polygon is placed on the left side of A→B (CCW convention).
fn edge_poly_params(a: Vec3, b: Vec3, sides: u32) -> Option<(Vec3, f32, f32)> { fn edge_poly_params(a: DVec3, b: DVec3, sides: u32) -> Option<(DVec3, f64, f64)> {
let edge_len = a.distance(b); let edge_len = a.distance(b);
if edge_len < 1e-6 { if edge_len < 1e-6 {
return None; return None;
} }
// vertex_radius = edge_len / (2 * sin(π/N)) // vertex_radius = edge_len / (2 * sin(π/N))
let vr = edge_len / (2.0 * (PI / sides as f32).sin()); let vr = edge_len / (2.0 * (PI / sides as f64).sin());
// inradius = vr * cos(π/N) = edge_len / (2 * tan(π/N)) // inradius = vr * cos(π/N) = edge_len / (2 * tan(π/N))
let inradius = vr * (PI / sides as f32).cos(); let inradius = vr * (PI / sides as f64).cos();
// Center: on the left perpendicular bisector of A→B // Center: on the left perpendicular bisector of A→B
let dir = (b - a) / edge_len; let dir = (b - a) / edge_len;
let perp = Vec3::new(-dir.y, dir.x, 0.0); // CCW left let perp = DVec3::new(-dir.y, dir.x, 0.0); // CCW left
let mid = (a + b) * 0.5; let mid = (a + b) * 0.5;
let center = mid + perp * inradius; let center = mid + perp * inradius;
// First vertex = A // First vertex = A

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::{Mat4, Vec3}; use glam::{DVec3, Mat4, Vec3};
use crate::command::{CadCommand, CmdResult, EntityTransform}; use crate::command::{CadCommand, CmdResult, EntityTransform};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
@ -94,8 +94,8 @@ impl ArrayRectCommand {
if r == 0 && c == 0 { if r == 0 && c == 0 {
continue; continue;
} }
t.push(EntityTransform::Translate(ucs.transform_vector3( t.push(EntityTransform::Translate(ucs.as_dmat4().transform_vector3(
Vec3::new(col_sp * c as f32, row_sp * r as f32, 0.0), glam::DVec3::new((col_sp * c as f32) as f64, (row_sp * r as f32) as f64, 0.0),
))); )));
} }
} }
@ -194,7 +194,7 @@ impl CadCommand for ArrayRectCommand {
} }
} }
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> { fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
let (rows, cols, row_sp, col_sp) = match self.step { let (rows, cols, row_sp, col_sp) = match self.step {
RectStep::Rows => ( RectStep::Rows => (
self.default_rows, self.default_rows,
@ -219,7 +219,7 @@ impl CadCommand for ArrayRectCommand {
if let EntityTransform::Translate(delta) = t { if let EntityTransform::Translate(delta) = t {
self.wire_models self.wire_models
.iter() .iter()
.map(|w| w.translated(*delta)) .map(|w| w.translated(delta.as_vec3()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
} else { } else {
vec![] vec![]
@ -228,7 +228,7 @@ impl CadCommand for ArrayRectCommand {
.collect() .collect()
} }
fn on_point(&mut self, _pt: Vec3) -> CmdResult { fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -331,6 +331,7 @@ 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,
@ -343,7 +344,8 @@ impl CadCommand for ArrayPolarCommand {
} }
} }
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> { fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
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 } => (*center, self.default_count, self.default_angle),
@ -370,7 +372,8 @@ impl CadCommand for ArrayPolarCommand {
out out
} }
fn on_point(&mut self, pt: Vec3) -> 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 };
} }
@ -655,13 +658,13 @@ 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) EntityTransform::Translate((p - p0).as_dvec3())
} 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: center.as_dvec3(),
angle_rad: dth, angle_rad: dth,
} }
} }
@ -720,7 +723,8 @@ impl CadCommand for ArrayPathCommand {
} }
} }
fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> 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;
} }
@ -738,7 +742,7 @@ impl CadCommand for ArrayPathCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
fn on_hover_entity(&mut self, handle: Handle, _pt: Vec3) -> Vec<WireModel> { fn on_hover_entity(&mut self, handle: Handle, _pt: DVec3) -> Vec<WireModel> {
if handle.is_null() || self.handles.contains(&handle) { if handle.is_null() || self.handles.contains(&handle) {
return vec![]; return vec![];
} }
@ -784,7 +788,7 @@ impl CadCommand for ArrayPathCommand {
Some(CmdResult::BatchCopy(self.handles.clone(), transforms)) Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
} }
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> { fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
let PathStep::Count { path_entity } = &self.step else { let PathStep::Count { path_entity } = &self.step else {
return vec![]; return vec![];
}; };
@ -796,19 +800,19 @@ impl CadCommand for ArrayPathCommand {
EntityTransform::Translate(delta) => self EntityTransform::Translate(delta) => self
.wire_models .wire_models
.iter() .iter()
.map(|w| w.translated(*delta)) .map(|w| w.translated(delta.as_vec3()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
EntityTransform::Rotate { center, angle_rad } => self EntityTransform::Rotate { center, angle_rad } => self
.wire_models .wire_models
.iter() .iter()
.map(|w| w.rotated(*center, *angle_rad)) .map(|w| w.rotated(center.as_vec3(), *angle_rad))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
_ => vec![], _ => vec![],
}) })
.collect() .collect()
} }
fn on_point(&mut self, _pt: Vec3) -> CmdResult { fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -885,10 +889,10 @@ impl Array3DCommand {
} }
// Drawing plane is world XY: X = col dir, Y = row dir, // Drawing plane is world XY: X = col dir, Y = row dir,
// Z = level (elevation). // Z = level (elevation).
t.push(EntityTransform::Translate(Vec3::new( t.push(EntityTransform::Translate(DVec3::new(
col_sp * c as f32, (col_sp * c as f32) as f64,
row_sp * r as f32, (row_sp * r as f32) as f64,
lvl_sp * l as f32, (lvl_sp * l as f32) as f64,
))); )));
} }
} }
@ -1025,7 +1029,7 @@ impl CadCommand for Array3DCommand {
} }
} }
fn on_point(&mut self, _pt: Vec3) -> CmdResult { fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@ -1037,7 +1041,7 @@ impl CadCommand for Array3DCommand {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> { fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
vec![] vec![]
} }
} }

View file

@ -3594,6 +3594,11 @@ impl Scene {
/// geometry of the currently active viewport. Returns the input unchanged /// geometry of the currently active viewport. Returns the input unchanged
/// when there is no active viewport. /// when there is no active viewport.
pub fn paper_to_model(&self, paper_pt: glam::Vec3) -> glam::Vec3 { pub fn paper_to_model(&self, paper_pt: glam::Vec3) -> glam::Vec3 {
self.paper_to_model_f64(paper_pt.as_dvec3()).as_vec3()
}
/// f64 variant of [`paper_to_model`] — preserves UTM-scale precision.
pub fn paper_to_model_f64(&self, paper_pt: glam::DVec3) -> glam::DVec3 {
let vp_handle = match self.active_viewport { let vp_handle = match self.active_viewport {
Some(h) => h, Some(h) => h,
None => return paper_pt, None => return paper_pt,
@ -3602,16 +3607,15 @@ impl Scene {
Some(acadrust::EntityType::Viewport(vp)) => vp, Some(acadrust::EntityType::Viewport(vp)) => vp,
_ => return paper_pt, _ => return paper_pt,
}; };
let scale = let scale = vp_effective_scale(vp.custom_scale, vp.view_height, vp.height);
vp_effective_scale(vp.custom_scale, vp.view_height, vp.height) as f32;
if scale.abs() < 1e-9 { if scale.abs() < 1e-9 {
return paper_pt; return paper_pt;
} }
let tx = vp.view_target.x as f32; let tx = vp.view_target.x;
let ty = vp.view_target.y as f32; let ty = vp.view_target.y;
let pcx = vp.center.x as f32; let pcx = vp.center.x;
let pcy = vp.center.y as f32; let pcy = vp.center.y;
glam::Vec3::new( glam::DVec3::new(
(paper_pt.x - pcx) / scale + tx, (paper_pt.x - pcx) / scale + tx,
(paper_pt.y - pcy) / scale + ty, (paper_pt.y - pcy) / scale + ty,
paper_pt.z, paper_pt.z,

View file

@ -1,7 +1,7 @@
// Shared value types used by the dispatch and grip systems. // Shared value types used by the dispatch and grip systems.
use acadrust::types::{Color as AcadColor, LineWeight}; use acadrust::types::{Color as AcadColor, LineWeight};
use glam::Vec3; use glam::{DVec3, Vec3};
/// The kind of value held by a property row. /// The kind of value held by a property row.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -96,9 +96,9 @@ pub struct GripDef {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum GripApply { pub enum GripApply {
/// Move a specific vertex to this absolute world position. /// Move a specific vertex to this absolute world position.
Absolute(Vec3), Absolute(DVec3),
/// Translate the whole object by this delta vector. /// Translate the whole object by this delta vector.
Translate(Vec3), Translate(DVec3),
} }
/// One entry in the hover-popup menu that opens when the cursor dwells /// One entry in the hover-popup menu that opens when the cursor dwells

View file

@ -1,7 +1,7 @@
//! OpenCADStudio-style grip editing. //! OpenCADStudio-style grip editing.
use acadrust::Handle; use acadrust::Handle;
use glam::Vec3; use glam::{DVec3, Vec3};
use iced::{Point, Rectangle}; use iced::{Point, Rectangle};
use crate::scene::model::object::{GripDef, GripShape}; use crate::scene::model::object::{GripDef, GripShape};
@ -23,9 +23,9 @@ pub struct GripEdit {
/// `true` → midpoint / translate grip; `false` → endpoint / absolute grip. /// `true` → midpoint / translate grip; `false` → endpoint / absolute grip.
pub is_translate: bool, pub is_translate: bool,
/// World-space position of the grip when the drag started (ortho/polar base). /// World-space position of the grip when the drag started (ortho/polar base).
pub origin_world: Vec3, pub origin_world: DVec3,
/// Last world-space cursor position (needed for incremental delta on translate drags). /// Last world-space cursor position (needed for incremental delta on translate drags).
pub last_world: Vec3, pub last_world: DVec3,
} }
// ── Screen-space helpers ─────────────────────────────────────────────────── // ── Screen-space helpers ───────────────────────────────────────────────────
@ -83,9 +83,9 @@ pub fn find_hit_grip_paper(
half_w: f32, half_w: f32,
half_h: f32, half_h: f32,
bounds: Rectangle, bounds: Rectangle,
) -> Option<(usize, bool, Vec3)> { ) -> Option<(usize, bool, DVec3)> {
let mut best_dist = GRIP_THRESHOLD_PX; let mut best_dist = GRIP_THRESHOLD_PX;
let mut best: Option<(usize, bool, Vec3)> = None; let mut best: Option<(usize, bool, DVec3)> = None;
for g in grips { for g in grips {
let screen = Point::new( let screen = Point::new(
@ -97,7 +97,7 @@ pub fn find_hit_grip_paper(
let d = (dx * dx + dy * dy).sqrt(); let d = (dx * dx + dy * dy).sqrt();
if d < best_dist { if d < best_dist {
best_dist = d; best_dist = d;
best = Some((g.id, g.is_midpoint, g.world.as_vec3())); best = Some((g.id, g.is_midpoint, g.world));
} }
} }
best best
@ -110,9 +110,9 @@ pub fn find_hit_grip(
grips: &[GripDef], grips: &[GripDef],
camera: &crate::scene::view::camera::Camera, camera: &crate::scene::view::camera::Camera,
bounds: Rectangle, bounds: Rectangle,
) -> Option<(usize, bool, Vec3)> { ) -> Option<(usize, bool, DVec3)> {
let mut best_dist = GRIP_THRESHOLD_PX; let mut best_dist = GRIP_THRESHOLD_PX;
let mut best: Option<(usize, bool, Vec3)> = None; let mut best: Option<(usize, bool, DVec3)> = None;
for g in grips { for g in grips {
let Some(screen) = camera.project_f64(g.world, bounds) else { let Some(screen) = camera.project_f64(g.world, bounds) else {
@ -124,7 +124,7 @@ pub fn find_hit_grip(
let d = (dx * dx + dy * dy).sqrt(); let d = (dx * dx + dy * dy).sqrt();
if d < best_dist { if d < best_dist {
best_dist = d; best_dist = d;
best = Some((g.id, g.is_midpoint, g.world.as_vec3())); best = Some((g.id, g.is_midpoint, g.world));
} }
} }
best best

View file

@ -233,9 +233,24 @@ impl Camera {
} }
pub fn pick_on_target_plane(&self, screen: Point, bounds: Rectangle) -> Vec3 { pub fn pick_on_target_plane(&self, screen: Point, bounds: Rectangle) -> Vec3 {
self.pick_on_target_plane_f64(screen, bounds).as_vec3()
}
/// f64 variant of [`pick_on_plane`] — keeps the cursor precise at UTM-scale.
pub fn pick_on_plane_f64(
&self,
screen: Point,
bounds: Rectangle,
plane_normal: Vec3,
plane_point: glam::DVec3,
) -> glam::DVec3 {
self.unproject_on_plane_f64(screen, bounds, plane_normal, plane_point)
}
/// f64 variant of [`pick_on_target_plane`].
pub fn pick_on_target_plane_f64(&self, screen: Point, bounds: Rectangle) -> glam::DVec3 {
let forward = (self.target.as_vec3() - self.eye()).normalize_or(Vec3::NEG_Z); let forward = (self.target.as_vec3() - self.eye()).normalize_or(Vec3::NEG_Z);
self.unproject_on_plane_f64(screen, bounds, forward, self.target) self.unproject_on_plane_f64(screen, bounds, forward, self.target)
.as_vec3()
} }

View file

@ -1,14 +1,14 @@
use acadrust::types::{Transform, Vector3}; use acadrust::types::{Transform, Vector3};
use glam::Vec3; use glam::DVec3;
use crate::command::EntityTransform; use crate::command::EntityTransform;
#[inline] #[inline]
fn to_v3(v: Vec3) -> Vector3 { fn 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)
} }
pub fn apply_standard_transform<T>(entity: &mut T, center: Vec3, angle_rad: f32) pub fn apply_standard_transform<T>(entity: &mut T, center: DVec3, angle_rad: f32)
where where
T: acadrust::Entity, T: acadrust::Entity,
{ {
@ -19,7 +19,7 @@ where
entity.apply_transform(&t); entity.apply_transform(&t);
} }
pub fn apply_standard_scale<T>(entity: &mut T, center: Vec3, factor: f32) pub fn apply_standard_scale<T>(entity: &mut T, center: DVec3, factor: f32)
where where
T: acadrust::Entity, T: acadrust::Entity,
{ {
@ -31,7 +31,7 @@ where
pub fn apply_standard_entity_transform<T, F>(entity: &mut T, t: &EntityTransform, mirror: F) pub fn apply_standard_entity_transform<T, F>(entity: &mut T, t: &EntityTransform, mirror: F)
where where
T: acadrust::Entity, T: acadrust::Entity,
F: FnOnce(&mut T, Vec3, Vec3), F: FnOnce(&mut T, DVec3, DVec3),
{ {
match t { match t {
EntityTransform::Translate(d) => entity.translate(to_v3(*d)), EntityTransform::Translate(d) => entity.translate(to_v3(*d)),
@ -47,7 +47,7 @@ where
/// `Transform`, for delegating MIRROR to acadrust's entity-aware /// `Transform`, for delegating MIRROR to acadrust's entity-aware
/// `apply_transform` paths (which handle direction flags, stored-angle /// `apply_transform` paths (which handle direction flags, stored-angle
/// conventions and bulges themselves). Degenerate line → identity. /// conventions and bulges themselves). Degenerate line → identity.
pub fn reflection_about_xy_line(p1: Vec3, p2: Vec3) -> acadrust::types::Transform { pub fn reflection_about_xy_line(p1: DVec3, p2: DVec3) -> acadrust::types::Transform {
use acadrust::types::{Matrix4, Transform}; use acadrust::types::{Matrix4, Transform};
let dx = (p2.x - p1.x) as f64; let dx = (p2.x - p1.x) as f64;
let dy = (p2.y - p1.y) as f64; let dy = (p2.y - p1.y) as f64;
@ -63,7 +63,7 @@ pub fn reflection_about_xy_line(p1: Vec3, p2: Vec3) -> acadrust::types::Transfor
Transform::from_matrix(m) Transform::from_matrix(m)
} }
pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: Vec3, p2: Vec3) { pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) {
let ax = (p2.x - p1.x) as f64; let ax = (p2.x - p1.x) as f64;
let ay = (p2.y - p1.y) as f64; let ay = (p2.y - p1.y) as f64;
let len2 = ax * ax + ay * ay; let len2 = ax * ax + ay * ay;
@ -79,7 +79,7 @@ pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: Vec3, p2: Vec3) {
*y = p1.y as f64 + my; *y = p1.y as f64 + my;
} }
pub fn mirror_xy_line(line: &mut acadrust::entities::Line, p1: Vec3, p2: Vec3) { pub fn mirror_xy_line(line: &mut acadrust::entities::Line, p1: DVec3, p2: DVec3) {
reflect_xy_point(&mut line.start.x, &mut line.start.y, p1, p2); reflect_xy_point(&mut line.start.x, &mut line.start.y, p1, p2);
reflect_xy_point(&mut line.end.x, &mut line.end.y, p1, p2); reflect_xy_point(&mut line.end.x, &mut line.end.y, p1, p2);
} }
@ -171,8 +171,8 @@ mod mirror_delegation_tests {
// Mirror across the vertical line x = 5. // Mirror across the vertical line x = 5.
h.apply_transform(&EntityTransform::Mirror { h.apply_transform(&EntityTransform::Mirror {
p1: Vec3::new(5.0, 0.0, 0.0), p1: DVec3::new(5.0, 0.0, 0.0),
p2: Vec3::new(5.0, 1.0, 0.0), p2: DVec3::new(5.0, 1.0, 0.0),
}); });
let edges = &h.paths[0].edges; let edges = &h.paths[0].edges;

View file

@ -57,7 +57,7 @@ pub const ALL_SNAP_MODES: &[(SnapType, &str, &str)] = &[
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct SnapResult { pub struct SnapResult {
pub world: Vec3, pub world: glam::DVec3,
pub screen: Point, pub screen: Point,
pub snap_type: SnapType, pub snap_type: SnapType,
/// Set when `snap_type == Tangent`; provides entity geometry for TTR/TTT. /// Set when `snap_type == Tangent`; provides entity geometry for TTR/TTT.
@ -203,8 +203,8 @@ impl Snapper {
Some(p) => { Some(p) => {
// Convert to screen to measure pixel distance. // Convert to screen to measure pixel distance.
let is_same = if let Some(prev) = self.last_snap_world { let is_same = if let Some(prev) = self.last_snap_world {
let dp = world_to_screen(p, view_rot, eye, bounds); let dp = world_to_screen(p.as_dvec3(), view_rot, eye, bounds);
let dp2 = world_to_screen(prev, view_rot, eye, bounds); let dp2 = world_to_screen(prev.as_dvec3(), view_rot, eye, bounds);
let dx = dp.x - dp2.x; let dx = dp.x - dp2.x;
let dy = dp.y - dp2.y; let dy = dp.y - dp2.y;
(dx * dx + dy * dy).sqrt() < DWELL_PX (dx * dx + dy * dy).sqrt() < DWELL_PX
@ -273,11 +273,11 @@ impl Snapper {
return None; return None;
} }
let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds); let cursor_screen = world_to_screen(cursor_world.as_dvec3(), view_rot, eye, bounds);
// Use the same aperture as OSNAP so the catch distance is uniform. // Use the same aperture as OSNAP so the catch distance is uniform.
let r = self.osnap_radius_px; let r = self.osnap_radius_px;
let screen_dist = |w: Vec3| { let screen_dist = |w: Vec3| {
let s = world_to_screen(w, view_rot, eye, bounds); let s = world_to_screen(w.as_dvec3(), view_rot, eye, bounds);
((s.x - cursor_screen.x).powi(2) + (s.y - cursor_screen.y).powi(2)).sqrt() ((s.x - cursor_screen.x).powi(2) + (s.y - cursor_screen.y).powi(2)).sqrt()
}; };
@ -499,7 +499,7 @@ impl Snapper {
&& cursor_world.y - r <= wire.aabb[3] && cursor_world.y - r <= wire.aabb[3]
}; };
let mut try_pt = |world: Vec3, snap_type: SnapType| { let mut try_pt = |world: glam::DVec3, snap_type: SnapType| {
let screen = world_to_screen(world, view_rot, eye, bounds); let screen = world_to_screen(world, view_rot, eye, bounds);
let d2 = dist2(screen, cursor_screen); let d2 = dist2(screen, cursor_screen);
// `!(d2 < radius2)` (not `d2 >= radius2`) so a NaN distance from // `!(d2 < radius2)` (not `d2 >= radius2`) so a NaN distance from
@ -533,7 +533,7 @@ impl Snapper {
SnapHint::Midpoint => SnapType::Midpoint, SnapHint::Midpoint => SnapType::Midpoint,
}; };
if self.is_on(snap_type) { if self.is_on(snap_type) {
try_pt(world.as_vec3(), snap_type); try_pt(world, snap_type);
} }
} }
} }
@ -548,18 +548,18 @@ impl Snapper {
// Use explicit vertices (Line, LwPolyline): every vertex is an endpoint. // Use explicit vertices (Line, LwPolyline): every vertex is an endpoint.
for &p in &wire.key_vertices { for &p in &wire.key_vertices {
try_pt( try_pt(
Vec3::new(p[0] as f32, p[1] as f32, p[2] as f32), glam::DVec3::new(p[0], p[1], p[2]),
SnapType::Endpoint, SnapType::Endpoint,
); );
} }
} else { } else {
// Tessellated curves (Circle, Arc, Ellipse): only arc endpoints. // Tessellated curves (Circle, Arc, Ellipse): only arc endpoints.
if let Some(&p) = wire.points.first() { if let Some(&p) = wire.points.first() {
try_pt(Vec3::from(p), SnapType::Endpoint); try_pt(glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), SnapType::Endpoint);
} }
if wire.points.len() > 1 { if wire.points.len() > 1 {
if let Some(&p) = wire.points.last() { if let Some(&p) = wire.points.last() {
try_pt(Vec3::from(p), SnapType::Endpoint); try_pt(glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), SnapType::Endpoint);
} }
} }
} }
@ -582,7 +582,7 @@ impl Snapper {
let a = Vec3::new(seg[0][0] as f32, seg[0][1] as f32, seg[0][2] as f32); let a = Vec3::new(seg[0][0] as f32, seg[0][1] as f32, seg[0][2] as f32);
let b = Vec3::new(seg[1][0] as f32, seg[1][1] as f32, seg[1][2] as f32); let b = Vec3::new(seg[1][0] as f32, seg[1][1] as f32, seg[1][2] as f32);
if a.distance_squared(b) > 1e-12 { if a.distance_squared(b) > 1e-12 {
try_pt((a + b) * 0.5, SnapType::Midpoint); try_pt(((a + b) * 0.5).as_dvec3(), SnapType::Midpoint);
} }
} }
} }
@ -598,7 +598,7 @@ impl Snapper {
for seg in wire.points.windows(2) { for seg in wire.points.windows(2) {
let p = let p =
nearest_on_segment(cursor_world, Vec3::from(seg[0]), Vec3::from(seg[1])); nearest_on_segment(cursor_world, Vec3::from(seg[0]), Vec3::from(seg[1]));
try_pt(p, SnapType::Nearest); try_pt(p.as_dvec3(), SnapType::Nearest);
} }
} }
} }
@ -618,7 +618,7 @@ impl Snapper {
} }
for seg in wire.points.windows(2) { for seg in wire.points.windows(2) {
if let Some(foot) = perp_foot(q, Vec3::from(seg[0]), Vec3::from(seg[1])) { if let Some(foot) = perp_foot(q, Vec3::from(seg[0]), Vec3::from(seg[1])) {
try_pt(foot, SnapType::Perpendicular); try_pt(foot.as_dvec3(), SnapType::Perpendicular);
} }
} }
} }
@ -654,7 +654,7 @@ impl Snapper {
continue; continue;
} }
if let Some(pt) = seg_intersect_xy(a0, a1, b0, b1) { if let Some(pt) = seg_intersect_xy(a0, a1, b0, b1) {
try_pt(pt, SnapType::Intersection); try_pt(pt.as_dvec3(), SnapType::Intersection);
} }
} }
} }
@ -682,7 +682,7 @@ impl Snapper {
bounds, bounds,
self.osnap_radius_px, self.osnap_radius_px,
) { ) {
try_pt(ext, SnapType::Extension); try_pt(ext.as_dvec3(), SnapType::Extension);
} }
} }
// Extend beyond the last point. // Extend beyond the last point.
@ -698,7 +698,7 @@ impl Snapper {
bounds, bounds,
self.osnap_radius_px, self.osnap_radius_px,
) { ) {
try_pt(ext, SnapType::Extension); try_pt(ext.as_dvec3(), SnapType::Extension);
} }
} }
} }
@ -716,7 +716,7 @@ impl Snapper {
Some( Some(
w.points w.points
.iter() .iter()
.map(|&p| world_to_screen(Vec3::from(p), view_rot, eye, bounds)) .map(|&p| world_to_screen(glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), view_rot, eye, bounds))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
) )
}) })
@ -739,7 +739,7 @@ impl Snapper {
if let Some((ta, _)) = seg_intersect_2d(sa0, sa1, sb0, sb1) { if let Some((ta, _)) = seg_intersect_2d(sa0, sa1, sb0, sb1) {
let wa0 = Vec3::from(seg_a[0]); let wa0 = Vec3::from(seg_a[0]);
let wa1 = Vec3::from(seg_a[1]); let wa1 = Vec3::from(seg_a[1]);
try_pt(wa0 + ta * (wa1 - wa0), SnapType::ApparentIntersection); try_pt((wa0 + ta * (wa1 - wa0)).as_dvec3(), SnapType::ApparentIntersection);
} }
} }
} }
@ -758,7 +758,7 @@ impl Snapper {
let ux = (rel.dot(ax) / s).round() * s; let ux = (rel.dot(ax) / s).round() * s;
let uy = (rel.dot(ay) / s).round() * s; let uy = (rel.dot(ay) / s).round() * s;
let uz = (rel.dot(az) / s).round() * s; let uz = (rel.dot(az) / s).round() * s;
try_pt(grid_origin + ax * ux + ay * uy + az * uz, SnapType::Grid); try_pt((grid_origin + ax * ux + ay * uy + az * uz).as_dvec3(), SnapType::Grid);
} }
// ── Tangent ──────────────────────────────────────────────────────── // ── Tangent ────────────────────────────────────────────────────────
@ -769,8 +769,8 @@ impl Snapper {
for tg in &wire.tangent_geoms { for tg in &wire.tangent_geoms {
let (world_pt, d2) = match tg { let (world_pt, d2) = match tg {
TangentGeom::Line { p1, p2 } => { TangentGeom::Line { p1, p2 } => {
let sp0 = world_to_screen(Vec3::from(*p1), view_rot, eye, bounds); let sp0 = world_to_screen(glam::DVec3::new(p1[0] as f64, p1[1] as f64, p1[2] as f64), view_rot, eye, bounds);
let sp1 = world_to_screen(Vec3::from(*p2), view_rot, eye, bounds); let sp1 = world_to_screen(glam::DVec3::new(p2[0] as f64, p2[1] as f64, p2[2] as f64), view_rot, eye, bounds);
let d2 = dist2_to_segment(cursor_screen, sp0, sp1); let d2 = dist2_to_segment(cursor_screen, sp0, sp1);
let t = t_on_segment(cursor_screen, sp0, sp1); let t = t_on_segment(cursor_screen, sp0, sp1);
let w = Vec3::from(*p1) + t * (Vec3::from(*p2) - Vec3::from(*p1)); let w = Vec3::from(*p1) + t * (Vec3::from(*p2) - Vec3::from(*p1));
@ -778,9 +778,9 @@ impl Snapper {
} }
TangentGeom::Circle { center, radius } => { TangentGeom::Circle { center, radius } => {
let cv = Vec3::from(*center); let cv = Vec3::from(*center);
let sc = world_to_screen(cv, view_rot, eye, bounds); let sc = world_to_screen(cv.as_dvec3(), view_rot, eye, bounds);
let rim = world_to_screen( let rim = world_to_screen(
Vec3::new(cv.x + radius, cv.y, cv.z), glam::DVec3::new((cv.x + radius) as f64, cv.y as f64, cv.z as f64),
view_rot, view_rot,
eye, eye,
bounds, bounds,
@ -805,19 +805,19 @@ impl Snapper {
if d2 < radius2 && (rank < best_rank || (rank == best_rank && d2 < best_d2)) { if d2 < radius2 && (rank < best_rank || (rank == best_rank && d2 < best_d2)) {
best_rank = rank; best_rank = rank;
best_d2 = d2; best_d2 = d2;
let screen_pt = world_to_screen(world_pt, view_rot, eye, bounds); let screen_pt = world_to_screen(world_pt.as_dvec3(), view_rot, eye, bounds);
let tangent_obj = match tg { let tangent_obj = match tg {
TangentGeom::Line { p1, p2 } => TangentObject::Line { TangentGeom::Line { p1, p2 } => TangentObject::Line {
p1: Vec3::from(*p1), p1: glam::DVec3::new(p1[0] as f64, p1[1] as f64, p1[2] as f64),
p2: Vec3::from(*p2), p2: glam::DVec3::new(p2[0] as f64, p2[1] as f64, p2[2] as f64),
}, },
TangentGeom::Circle { center, radius } => TangentObject::Circle { TangentGeom::Circle { center, radius } => TangentObject::Circle {
center: Vec3::from(*center), center: glam::DVec3::new(center[0] as f64, center[1] as f64, center[2] as f64),
radius: *radius, radius: *radius as f64,
}, },
}; };
best = Some(SnapResult { best = Some(SnapResult {
world: world_pt, world: world_pt.as_dvec3(),
screen: screen_pt, screen: screen_pt,
snap_type: SnapType::Tangent, snap_type: SnapType::Tangent,
tangent_obj: Some(tangent_obj), tangent_obj: Some(tangent_obj),
@ -962,8 +962,8 @@ fn extension_snap(
return None; return None;
} // only beyond the endpoint } // only beyond the endpoint
let world_pt = Vec3::new(origin.x + t * dir.x, origin.y + t * dir.y, origin.z); let world_pt = Vec3::new(origin.x + t * dir.x, origin.y + t * dir.y, origin.z);
let screen_pt = world_to_screen(world_pt, view_rot, eye, bounds); let screen_pt = world_to_screen(world_pt.as_dvec3(), view_rot, eye, bounds);
let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds); let cursor_screen = world_to_screen(cursor_world.as_dvec3(), view_rot, eye, bounds);
if dist2(screen_pt, cursor_screen) > radius_px * radius_px { if dist2(screen_pt, cursor_screen) > radius_px * radius_px {
return None; return None;
} }
@ -976,8 +976,8 @@ fn extension_snap(
/// so the result is precise at UTM-scale absolute coordinates (a full /// so the result is precise at UTM-scale absolute coordinates (a full
/// view-projection with a ~1e7 translation cancels catastrophically in f32). /// view-projection with a ~1e7 translation cancels catastrophically in f32).
/// `view_rot` is the rotation-only view-projection (Camera::view_proj_rte). /// `view_rot` is the rotation-only view-projection (Camera::view_proj_rte).
fn world_to_screen(world: Vec3, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Point { fn world_to_screen(world: glam::DVec3, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Point {
let rel = (world.as_dvec3() - eye).as_vec3(); let rel = (world - eye).as_vec3();
let ndc = view_rot.project_point3(rel); let ndc = view_rot.project_point3(rel);
Point::new( Point::new(
(ndc.x + 1.0) * 0.5 * bounds.width, (ndc.x + 1.0) * 0.5 * bounds.width,