forked from 1b-agent/opencadstudio-upstream
feat(ucs): integrate coordinate systems
Route coordinate input, geometry creation, transforms, properties,\nviews, and viewport persistence through a shared working plane.\n\nRefs #484
This commit is contained in:
1 parent
b1d55c6a7e
commit
657d023fb7
61 files changed
+2218
-747
No files matched your search
+283
-124
@@ -17,7 +17,7 @@ use acadrust::types::Vector3;
|
||||
use acadrust::{Arc as CadArc, EntityType};
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
@@ -72,12 +72,18 @@ pub const ICON: IconKind = ICON_3P;
|
||||
// ── Shared math helpers ────────────────────────────────────────────────────
|
||||
|
||||
/// Angle in radians from `center` to `pt`.
|
||||
fn angle_xy(center: DVec3, pt: DVec3) -> f64 {
|
||||
(pt.y - center.y).atan2(pt.x - center.x)
|
||||
fn angle_xy(center: DVec3, pt: DVec3, plane: WorkingPlane) -> f64 {
|
||||
plane.angle(center, pt).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Build a CCW arc polyline from `start_angle` to `end_angle` (radians).
|
||||
fn arc_preview(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> WireModel {
|
||||
fn arc_preview(
|
||||
center: DVec3,
|
||||
radius: f64,
|
||||
start_angle: f64,
|
||||
end_angle: f64,
|
||||
plane: WorkingPlane,
|
||||
) -> WireModel {
|
||||
let mut ea = end_angle;
|
||||
while ea < start_angle {
|
||||
ea += TAU;
|
||||
@@ -87,32 +93,36 @@ fn arc_preview(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) ->
|
||||
let pts: Vec<[f64; 3]> = (0..=segs)
|
||||
.map(|i| {
|
||||
let a = start_angle + span * (i as f64 / segs as f64);
|
||||
[
|
||||
center.x + radius * a.cos(),
|
||||
center.y + radius * a.sin(),
|
||||
center.z,
|
||||
]
|
||||
let point = center + plane.x * (radius * a.cos()) + plane.y * (radius * a.sin());
|
||||
[point.x, point.y, point.z]
|
||||
})
|
||||
.collect();
|
||||
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
|
||||
}
|
||||
|
||||
fn make_arc(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> EntityType {
|
||||
EntityType::Arc(CadArc {
|
||||
fn make_arc(
|
||||
center: DVec3,
|
||||
radius: f64,
|
||||
start_angle: f64,
|
||||
end_angle: f64,
|
||||
plane: WorkingPlane,
|
||||
) -> EntityType {
|
||||
let center = plane.to_local(center);
|
||||
plane.place_entity(EntityType::Arc(CadArc {
|
||||
center: Vector3::new(center.x, center.y, center.z),
|
||||
radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Signed rotation from `prev` to `curr` around `center`.
|
||||
/// Positive = CCW, negative = CW.
|
||||
/// Signed angle (radians) swept from `prev` to `curr` about `center`.
|
||||
fn rot_delta(center: DVec3, prev: DVec3, curr: DVec3) -> f64 {
|
||||
let p = prev - center;
|
||||
let c = curr - center;
|
||||
fn rot_delta(center: DVec3, prev: DVec3, curr: DVec3, plane: WorkingPlane) -> f64 {
|
||||
let p = plane.vector_to_local(prev - center);
|
||||
let c = plane.vector_to_local(curr - center);
|
||||
(p.x * c.y - p.y * c.x).atan2(p.x * c.x + p.y * c.y)
|
||||
}
|
||||
|
||||
@@ -130,7 +140,13 @@ fn line_wire(a: DVec3, b: DVec3) -> WireModel {
|
||||
}
|
||||
|
||||
/// Circumscribed circle through three points.
|
||||
fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
|
||||
fn circumcircle(
|
||||
a: DVec3,
|
||||
b: DVec3,
|
||||
c: DVec3,
|
||||
plane: WorkingPlane,
|
||||
) -> Option<(DVec3, f64)> {
|
||||
let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c));
|
||||
let d = 2.0 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
|
||||
if d.abs() < 1e-9 {
|
||||
return None;
|
||||
@@ -144,7 +160,7 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
|
||||
+ (c.x * c.x + c.y * c.y) * (b.x - a.x))
|
||||
/ d;
|
||||
let center = DVec3::new(ux, uy, a.z);
|
||||
Some((center, center.distance(a)))
|
||||
Some((plane.to_world(center), center.distance(a)))
|
||||
}
|
||||
|
||||
/// True if `angle` lies inside the CCW arc [start..end] (radians).
|
||||
@@ -160,7 +176,13 @@ fn ccw_contains(angle: f64, start: f64, end: f64) -> bool {
|
||||
}
|
||||
|
||||
/// Arc center+radius from two endpoints and a cursor (sagitta / bow-toward-cursor).
|
||||
fn arc_from_sagitta(s: DVec3, e: DVec3, cursor: DVec3) -> Option<(DVec3, f64)> {
|
||||
fn arc_from_sagitta(
|
||||
s: DVec3,
|
||||
e: DVec3,
|
||||
cursor: DVec3,
|
||||
plane: WorkingPlane,
|
||||
) -> Option<(DVec3, f64)> {
|
||||
let (s, e, cursor) = (plane.to_local(s), plane.to_local(e), plane.to_local(cursor));
|
||||
let chord_vec = e - s;
|
||||
let chord_len = (chord_vec.x * chord_vec.x + chord_vec.y * chord_vec.y).sqrt();
|
||||
if chord_len < 1e-6 {
|
||||
@@ -177,28 +199,48 @@ fn arc_from_sagitta(s: DVec3, e: DVec3, cursor: DVec3) -> Option<(DVec3, f64)> {
|
||||
let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5))
|
||||
.max(0.0)
|
||||
.sqrt();
|
||||
Some((mid - perp * h.signum() * d, r))
|
||||
Some((plane.to_world(mid - perp * h.signum() * d), r))
|
||||
}
|
||||
|
||||
/// Arc center+radius from start, end, and a radius-magnitude point (dist = dist(pt, start)).
|
||||
fn arc_from_se_radius(s: DVec3, e: DVec3, radius_pt: DVec3) -> Option<(DVec3, f64)> {
|
||||
fn arc_from_se_radius(
|
||||
s: DVec3,
|
||||
e: DVec3,
|
||||
radius_pt: DVec3,
|
||||
plane: WorkingPlane,
|
||||
) -> Option<(DVec3, f64)> {
|
||||
let (s, e, radius_pt) = (
|
||||
plane.to_local(s),
|
||||
plane.to_local(e),
|
||||
plane.to_local(radius_pt),
|
||||
);
|
||||
let r = s.distance(radius_pt).max(1e-3);
|
||||
let chord_len = s.distance(e);
|
||||
if r < chord_len * 0.5 {
|
||||
return None;
|
||||
}
|
||||
let unit_chord = (e - s) / chord_len;
|
||||
let perp = DVec3::new(-unit_chord.y, 0.0, unit_chord.x);
|
||||
let perp = DVec3::new(-unit_chord.y, unit_chord.x, 0.0);
|
||||
let mid = (s + e) * 0.5;
|
||||
let h_sign = (radius_pt - mid).dot(perp).signum();
|
||||
let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5))
|
||||
.max(0.0)
|
||||
.sqrt();
|
||||
Some((mid - perp * h_sign * d, r))
|
||||
Some((plane.to_world(mid - perp * h_sign * d), r))
|
||||
}
|
||||
|
||||
/// Arc center+radius from start, end, and a tangent-direction point at start.
|
||||
fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)> {
|
||||
fn arc_from_direction(
|
||||
s: DVec3,
|
||||
e: DVec3,
|
||||
dir_pt: DVec3,
|
||||
plane: WorkingPlane,
|
||||
) -> Option<(DVec3, f64)> {
|
||||
let (s, e, dir_pt) = (
|
||||
plane.to_local(s),
|
||||
plane.to_local(e),
|
||||
plane.to_local(dir_pt),
|
||||
);
|
||||
let t = (dir_pt - s).normalize_or_zero();
|
||||
if t.length_squared() < 1e-12 {
|
||||
return None;
|
||||
@@ -211,7 +253,7 @@ fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)>
|
||||
}
|
||||
let lambda = chord.length_squared() * 0.5 / denom;
|
||||
let center = s + perp_t * lambda;
|
||||
Some((center, center.distance(s)))
|
||||
Some((plane.to_world(center), center.distance(s)))
|
||||
}
|
||||
|
||||
/// Tangent-continuing arc: starts at `s` leaving in unit direction `t` (forward,
|
||||
@@ -225,18 +267,25 @@ fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)>
|
||||
/// checking the circle's CCW travel direction at `s` and swapping the angles
|
||||
/// when it opposes `+t`. `flip` (Ctrl) selects the complementary arc — the other
|
||||
/// way around the same circle.
|
||||
fn arc_continue(s: DVec3, t: DVec3, e: DVec3, flip: bool) -> Option<(DVec3, f64, f64, f64)> {
|
||||
let (center, radius) = arc_from_direction(s, e, s + t)?;
|
||||
fn arc_continue(
|
||||
s: DVec3,
|
||||
t: DVec3,
|
||||
e: DVec3,
|
||||
flip: bool,
|
||||
plane: WorkingPlane,
|
||||
) -> Option<(DVec3, f64, f64, f64)> {
|
||||
let (center, radius) = arc_from_direction(s, e, s + t, plane)?;
|
||||
// CCW travel direction on this circle at s = (s - center) rotated +90°.
|
||||
let rs = s - center;
|
||||
let rs = plane.vector_to_local(s - center);
|
||||
let t = plane.vector_to_local(t);
|
||||
let travel_s = DVec3::new(-rs.y, rs.x, 0.0);
|
||||
let forward = travel_s.dot(t) >= 0.0;
|
||||
// Draw CCW s→e when the CCW-from-s arc leaves along +t; else CCW e→s. `flip`
|
||||
// inverts the choice to draw the complementary arc.
|
||||
let (sa, ea) = if forward ^ flip {
|
||||
(angle_xy(center, s), angle_xy(center, e))
|
||||
(angle_xy(center, s, plane), angle_xy(center, e, plane))
|
||||
} else {
|
||||
(angle_xy(center, e), angle_xy(center, s))
|
||||
(angle_xy(center, e, plane), angle_xy(center, s, plane))
|
||||
};
|
||||
Some((center, radius, sa, ea))
|
||||
}
|
||||
@@ -264,12 +313,22 @@ pub fn continue_anchor(entity: &EntityType, last: Option<DVec3>) -> Option<(DVec
|
||||
let ep_v = a.end_point();
|
||||
let sp = DVec3::new(sp_v.x, sp_v.y, sp_v.z);
|
||||
let ep = DVec3::new(ep_v.x, ep_v.y, ep_v.z);
|
||||
let normal = (a.normal.x, a.normal.y, a.normal.z);
|
||||
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
|
||||
let tangent = |angle: f64, ccw: bool| {
|
||||
let sign = if ccw { 1.0 } else { -1.0 };
|
||||
DVec3::new(
|
||||
sign * (-angle.sin() * ax.0 + angle.cos() * ay.0),
|
||||
sign * (-angle.sin() * ax.1 + angle.cos() * ay.1),
|
||||
sign * (-angle.sin() * ax.2 + angle.cos() * ay.2),
|
||||
)
|
||||
};
|
||||
if nearer_first(ep, sp) {
|
||||
// Ended at end_angle: outward tangent = CCW travel there.
|
||||
Some((ep, DVec3::new(-a.end_angle.sin(), a.end_angle.cos(), 0.0)))
|
||||
Some((ep, tangent(a.end_angle, true)))
|
||||
} else {
|
||||
// Ended at start_angle: outward tangent = CW travel there.
|
||||
Some((sp, DVec3::new(a.start_angle.sin(), -a.start_angle.cos(), 0.0)))
|
||||
Some((sp, tangent(a.start_angle, false)))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
@@ -292,6 +351,7 @@ pub struct ArcCommand {
|
||||
sa: f64,
|
||||
prev_pt: Option<DVec3>,
|
||||
cw: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcCommand {
|
||||
@@ -303,11 +363,16 @@ impl ArcCommand {
|
||||
sa: 0.0,
|
||||
prev_pt: None,
|
||||
cw: false,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC"
|
||||
}
|
||||
@@ -360,16 +425,16 @@ impl CadCommand for ArcCommand {
|
||||
}
|
||||
1 => {
|
||||
self.r = self.c.distance(pt);
|
||||
self.sa = angle_xy(self.c, pt);
|
||||
self.sa = angle_xy(self.c, pt, self.plane);
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let ea = angle_xy(self.c, pt);
|
||||
let ea = angle_xy(self.c, pt, self.plane);
|
||||
let e = if self.cw {
|
||||
make_arc(self.c, self.r, ea, self.sa)
|
||||
make_arc(self.c, self.r, ea, self.sa, self.plane)
|
||||
} else {
|
||||
make_arc(self.c, self.r, self.sa, ea)
|
||||
make_arc(self.c, self.r, self.sa, ea, self.plane)
|
||||
};
|
||||
CmdResult::CommitAndExit(e)
|
||||
}
|
||||
@@ -408,7 +473,7 @@ impl CadCommand for ArcCommand {
|
||||
// Only flip the sweep once the cursor has moved a clear
|
||||
// angular step; keep the reference point until then so slow
|
||||
// moves accumulate and jitter is ignored.
|
||||
let d = rot_delta(self.c, prev, pt);
|
||||
let d = rot_delta(self.c, prev, pt, self.plane);
|
||||
if d.abs() > DIR_TOL {
|
||||
self.cw = d < 0.0;
|
||||
self.prev_pt = Some(pt);
|
||||
@@ -416,11 +481,11 @@ impl CadCommand for ArcCommand {
|
||||
} else {
|
||||
self.prev_pt = Some(pt);
|
||||
}
|
||||
let ea = angle_xy(self.c, pt);
|
||||
let ea = angle_xy(self.c, pt, self.plane);
|
||||
Some(if self.cw {
|
||||
arc_preview(self.c, self.r, ea, self.sa)
|
||||
arc_preview(self.c, self.r, ea, self.sa, self.plane)
|
||||
} else {
|
||||
arc_preview(self.c, self.r, self.sa, ea)
|
||||
arc_preview(self.c, self.r, self.sa, ea, self.plane)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
@@ -432,15 +497,23 @@ impl CadCommand for ArcCommand {
|
||||
|
||||
pub struct Arc3PCommand {
|
||||
pts: Vec<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl Arc3PCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { pts: Vec::new() }
|
||||
Self {
|
||||
pts: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for Arc3PCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_3P"
|
||||
}
|
||||
@@ -457,22 +530,22 @@ impl CadCommand for Arc3PCommand {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let (p1, p2, p3) = (self.pts[0], self.pts[1], self.pts[2]);
|
||||
match circumcircle(p1, p2, p3) {
|
||||
match circumcircle(p1, p2, p3, self.plane) {
|
||||
None => {
|
||||
self.pts.pop();
|
||||
CmdResult::NeedPoint
|
||||
} // collinear — retry
|
||||
Some((center, radius)) => {
|
||||
let a1 = angle_xy(center, p1);
|
||||
let a2 = angle_xy(center, p2);
|
||||
let a3 = angle_xy(center, p3);
|
||||
let a1 = angle_xy(center, p1, self.plane);
|
||||
let a2 = angle_xy(center, p2, self.plane);
|
||||
let a3 = angle_xy(center, p3, self.plane);
|
||||
// Choose arc direction so that p2 lies on the arc from p1 to p3.
|
||||
let (sa, ea) = if ccw_contains(a2, a1, a3) {
|
||||
(a1, a3)
|
||||
} else {
|
||||
(a3, a1)
|
||||
};
|
||||
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea))
|
||||
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,16 +564,16 @@ impl CadCommand for Arc3PCommand {
|
||||
1 => Some(line_wire(self.pts[0], pt)),
|
||||
_ => {
|
||||
let (p1, p2) = (self.pts[0], self.pts[1]);
|
||||
if let Some((center, radius)) = circumcircle(p1, p2, pt) {
|
||||
let a1 = angle_xy(center, p1);
|
||||
let a2 = angle_xy(center, p2);
|
||||
let a3 = angle_xy(center, pt);
|
||||
if let Some((center, radius)) = circumcircle(p1, p2, pt, self.plane) {
|
||||
let a1 = angle_xy(center, p1, self.plane);
|
||||
let a2 = angle_xy(center, p2, self.plane);
|
||||
let a3 = angle_xy(center, pt, self.plane);
|
||||
let (sa, ea) = if ccw_contains(a2, a1, a3) {
|
||||
(a1, a3)
|
||||
} else {
|
||||
(a3, a1)
|
||||
};
|
||||
Some(arc_preview(center, radius, sa, ea))
|
||||
Some(arc_preview(center, radius, sa, ea, self.plane))
|
||||
} else {
|
||||
Some(WireModel::solid_f64(
|
||||
"rubber_band".into(),
|
||||
@@ -524,6 +597,7 @@ pub struct ArcSCECommand {
|
||||
sa: f64,
|
||||
prev_pt: Option<DVec3>,
|
||||
cw: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcSCECommand {
|
||||
@@ -536,11 +610,16 @@ impl ArcSCECommand {
|
||||
sa: 0.0,
|
||||
prev_pt: None,
|
||||
cw: false,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcSCECommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_SCE"
|
||||
}
|
||||
@@ -564,16 +643,16 @@ impl CadCommand for ArcSCECommand {
|
||||
1 => {
|
||||
self.c = pt;
|
||||
self.r = pt.distance(self.s);
|
||||
self.sa = angle_xy(pt, self.s);
|
||||
self.sa = angle_xy(pt, self.s, self.plane);
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let ea = angle_xy(self.c, pt);
|
||||
let ea = angle_xy(self.c, pt, self.plane);
|
||||
let e = if self.cw {
|
||||
make_arc(self.c, self.r, ea, self.sa)
|
||||
make_arc(self.c, self.r, ea, self.sa, self.plane)
|
||||
} else {
|
||||
make_arc(self.c, self.r, self.sa, ea)
|
||||
make_arc(self.c, self.r, self.sa, ea, self.plane)
|
||||
};
|
||||
CmdResult::CommitAndExit(e)
|
||||
}
|
||||
@@ -596,7 +675,7 @@ impl CadCommand for ArcSCECommand {
|
||||
// Only flip the sweep once the cursor has moved a clear
|
||||
// angular step; keep the reference point until then so slow
|
||||
// moves accumulate and jitter is ignored.
|
||||
let d = rot_delta(self.c, prev, pt);
|
||||
let d = rot_delta(self.c, prev, pt, self.plane);
|
||||
if d.abs() > DIR_TOL {
|
||||
self.cw = d < 0.0;
|
||||
self.prev_pt = Some(pt);
|
||||
@@ -604,11 +683,11 @@ impl CadCommand for ArcSCECommand {
|
||||
} else {
|
||||
self.prev_pt = Some(pt);
|
||||
}
|
||||
let ea = angle_xy(self.c, pt);
|
||||
let ea = angle_xy(self.c, pt, self.plane);
|
||||
Some(if self.cw {
|
||||
arc_preview(self.c, self.r, ea, self.sa)
|
||||
arc_preview(self.c, self.r, ea, self.sa, self.plane)
|
||||
} else {
|
||||
arc_preview(self.c, self.r, self.sa, ea)
|
||||
arc_preview(self.c, self.r, self.sa, ea, self.plane)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
@@ -627,6 +706,7 @@ pub struct ArcSCACommand {
|
||||
sa: f64,
|
||||
prev_pt: Option<DVec3>,
|
||||
cw: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcSCACommand {
|
||||
@@ -639,11 +719,16 @@ impl ArcSCACommand {
|
||||
sa: 0.0,
|
||||
prev_pt: None,
|
||||
cw: false,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcSCACommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_SCA"
|
||||
}
|
||||
@@ -671,16 +756,16 @@ impl CadCommand for ArcSCACommand {
|
||||
1 => {
|
||||
self.c = pt;
|
||||
self.r = pt.distance(self.s);
|
||||
self.sa = angle_xy(pt, self.s);
|
||||
self.sa = angle_xy(pt, self.s, self.plane);
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let ea = angle_xy(self.c, pt);
|
||||
let ea = angle_xy(self.c, pt, self.plane);
|
||||
let e = if self.cw {
|
||||
make_arc(self.c, self.r, ea, self.sa)
|
||||
make_arc(self.c, self.r, ea, self.sa, self.plane)
|
||||
} else {
|
||||
make_arc(self.c, self.r, self.sa, ea)
|
||||
make_arc(self.c, self.r, self.sa, ea, self.plane)
|
||||
};
|
||||
CmdResult::CommitAndExit(e)
|
||||
}
|
||||
@@ -701,7 +786,7 @@ impl CadCommand for ArcSCACommand {
|
||||
// Negative span = CW; positive = CCW.
|
||||
let ea = self.sa + span.to_radians();
|
||||
return Some(CmdResult::CommitAndExit(make_arc(
|
||||
self.c, self.r, self.sa, ea,
|
||||
self.c, self.r, self.sa, ea, self.plane,
|
||||
)));
|
||||
}
|
||||
None
|
||||
@@ -714,7 +799,9 @@ impl CadCommand for ArcSCACommand {
|
||||
anchor: DynAnchor::Point(self.c),
|
||||
fields: vec![DynFieldSpec::new(DynRole::Angle)],
|
||||
guide: DynGuide::Polar,
|
||||
ref_point: Some(self.c + DVec3::new(self.sa.cos(), self.sa.sin(), 0.0)),
|
||||
ref_point: Some(
|
||||
self.c + self.plane.x * self.sa.cos() + self.plane.y * self.sa.sin(),
|
||||
),
|
||||
})
|
||||
}
|
||||
fn dyn_commit_as_text(&self) -> bool {
|
||||
@@ -722,7 +809,9 @@ impl CadCommand for ArcSCACommand {
|
||||
}
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
(self.step == 2).then(|| {
|
||||
crate::command::dyn_display_angle_deg((angle_xy(self.c, cursor) - self.sa) as f32) as f64
|
||||
crate::command::dyn_display_angle_deg(
|
||||
(angle_xy(self.c, cursor, self.plane) - self.sa) as f32,
|
||||
) as f64
|
||||
})
|
||||
}
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
@@ -733,7 +822,7 @@ impl CadCommand for ArcSCACommand {
|
||||
// Only flip the sweep once the cursor has moved a clear
|
||||
// angular step; keep the reference point until then so slow
|
||||
// moves accumulate and jitter is ignored.
|
||||
let d = rot_delta(self.c, prev, pt);
|
||||
let d = rot_delta(self.c, prev, pt, self.plane);
|
||||
if d.abs() > DIR_TOL {
|
||||
self.cw = d < 0.0;
|
||||
self.prev_pt = Some(pt);
|
||||
@@ -741,11 +830,11 @@ impl CadCommand for ArcSCACommand {
|
||||
} else {
|
||||
self.prev_pt = Some(pt);
|
||||
}
|
||||
let ea = angle_xy(self.c, pt);
|
||||
let ea = angle_xy(self.c, pt, self.plane);
|
||||
Some(if self.cw {
|
||||
arc_preview(self.c, self.r, ea, self.sa)
|
||||
arc_preview(self.c, self.r, ea, self.sa, self.plane)
|
||||
} else {
|
||||
arc_preview(self.c, self.r, self.sa, ea)
|
||||
arc_preview(self.c, self.r, self.sa, ea, self.plane)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
@@ -763,6 +852,7 @@ pub struct ArcSCLCommand {
|
||||
c: DVec3,
|
||||
r: f64,
|
||||
sa: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcSCLCommand {
|
||||
@@ -773,11 +863,16 @@ impl ArcSCLCommand {
|
||||
c: DVec3::ZERO,
|
||||
r: 0.0,
|
||||
sa: 0.0,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcSCLCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_SCL"
|
||||
}
|
||||
@@ -805,14 +900,20 @@ impl CadCommand for ArcSCLCommand {
|
||||
1 => {
|
||||
self.c = pt;
|
||||
self.r = pt.distance(self.s);
|
||||
self.sa = angle_xy(pt, self.s);
|
||||
self.sa = angle_xy(pt, self.s, self.plane);
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let chord = self.s.distance(pt);
|
||||
let ea = end_angle_from_chord_len(self.sa, chord, self.r);
|
||||
CmdResult::CommitAndExit(make_arc(self.c, self.r, self.sa, ea))
|
||||
CmdResult::CommitAndExit(make_arc(
|
||||
self.c,
|
||||
self.r,
|
||||
self.sa,
|
||||
ea,
|
||||
self.plane,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -831,7 +932,7 @@ impl CadCommand for ArcSCLCommand {
|
||||
if chord > 0.0 {
|
||||
let ea = end_angle_from_chord_len(self.sa, chord, self.r);
|
||||
return Some(CmdResult::CommitAndExit(make_arc(
|
||||
self.c, self.r, self.sa, ea,
|
||||
self.c, self.r, self.sa, ea, self.plane,
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -859,7 +960,7 @@ impl CadCommand for ArcSCLCommand {
|
||||
2 => {
|
||||
let chord = self.s.distance(pt);
|
||||
let ea = end_angle_from_chord_len(self.sa, chord, self.r);
|
||||
Some(arc_preview(self.c, self.r, self.sa, ea))
|
||||
Some(arc_preview(self.c, self.r, self.sa, ea, self.plane))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -873,6 +974,7 @@ pub struct ArcSEACommand {
|
||||
step: u8,
|
||||
s: DVec3,
|
||||
e: DVec3,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcSEACommand {
|
||||
@@ -881,11 +983,16 @@ impl ArcSEACommand {
|
||||
step: 0,
|
||||
s: DVec3::ZERO,
|
||||
e: DVec3::ZERO,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcSEACommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_SEA"
|
||||
}
|
||||
@@ -908,11 +1015,11 @@ impl CadCommand for ArcSEACommand {
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => match arc_from_sagitta(self.s, self.e, pt) {
|
||||
_ => match arc_from_sagitta(self.s, self.e, pt, self.plane) {
|
||||
Some((center, radius)) => {
|
||||
let sa = angle_xy(center, self.s);
|
||||
let ea = angle_xy(center, self.e);
|
||||
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea))
|
||||
let sa = angle_xy(center, self.s, self.plane);
|
||||
let ea = angle_xy(center, self.e, self.plane);
|
||||
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane))
|
||||
}
|
||||
None => CmdResult::Cancel,
|
||||
},
|
||||
@@ -931,10 +1038,12 @@ impl CadCommand for ArcSEACommand {
|
||||
match self.step {
|
||||
1 => Some(line_wire(self.s, pt)),
|
||||
2 => {
|
||||
if let Some((center, radius)) = arc_from_sagitta(self.s, self.e, pt) {
|
||||
let sa = angle_xy(center, self.s);
|
||||
let ea = angle_xy(center, self.e);
|
||||
Some(arc_preview(center, radius, sa, ea))
|
||||
if let Some((center, radius)) =
|
||||
arc_from_sagitta(self.s, self.e, pt, self.plane)
|
||||
{
|
||||
let sa = angle_xy(center, self.s, self.plane);
|
||||
let ea = angle_xy(center, self.e, self.plane);
|
||||
Some(arc_preview(center, radius, sa, ea, self.plane))
|
||||
} else {
|
||||
Some(line_wire(self.s, self.e))
|
||||
}
|
||||
@@ -951,6 +1060,7 @@ pub struct ArcSERCommand {
|
||||
step: u8,
|
||||
s: DVec3,
|
||||
e: DVec3,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcSERCommand {
|
||||
@@ -959,11 +1069,16 @@ impl ArcSERCommand {
|
||||
step: 0,
|
||||
s: DVec3::ZERO,
|
||||
e: DVec3::ZERO,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcSERCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_SER"
|
||||
}
|
||||
@@ -986,11 +1101,11 @@ impl CadCommand for ArcSERCommand {
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => match arc_from_se_radius(self.s, self.e, pt) {
|
||||
_ => match arc_from_se_radius(self.s, self.e, pt, self.plane) {
|
||||
Some((center, radius)) => {
|
||||
let sa = angle_xy(center, self.s);
|
||||
let ea = angle_xy(center, self.e);
|
||||
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea))
|
||||
let sa = angle_xy(center, self.s, self.plane);
|
||||
let ea = angle_xy(center, self.e, self.plane);
|
||||
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane))
|
||||
}
|
||||
None => CmdResult::Cancel,
|
||||
},
|
||||
@@ -1008,18 +1123,22 @@ impl CadCommand for ArcSERCommand {
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.step == 2 {
|
||||
let r: f64 = text.trim().replace(',', ".").parse().ok()?;
|
||||
let chord = self.s.distance(self.e);
|
||||
let s = self.plane.to_local(self.s);
|
||||
let e = self.plane.to_local(self.e);
|
||||
let chord = s.distance(e);
|
||||
if r > 0.0 && r >= chord / 2.0 {
|
||||
let mid = (self.s + self.e) * 0.5;
|
||||
let mid = (s + e) * 0.5;
|
||||
let perp = {
|
||||
let cv = (self.e - self.s) / chord;
|
||||
let cv = (e - s) / chord;
|
||||
DVec3::new(-cv.y, cv.x, 0.0)
|
||||
};
|
||||
let d = (r * r - (chord / 2.0) * (chord / 2.0)).max(0.0).sqrt();
|
||||
let center = mid - perp * d;
|
||||
let sa = angle_xy(center, self.s);
|
||||
let ea = angle_xy(center, self.e);
|
||||
return Some(CmdResult::CommitAndExit(make_arc(center, r, sa, ea)));
|
||||
let center = self.plane.to_world(mid - perp * d);
|
||||
let sa = angle_xy(center, self.s, self.plane);
|
||||
let ea = angle_xy(center, self.e, self.plane);
|
||||
return Some(CmdResult::CommitAndExit(make_arc(
|
||||
center, r, sa, ea, self.plane,
|
||||
)));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -1041,16 +1160,18 @@ impl CadCommand for ArcSERCommand {
|
||||
if self.step != 2 {
|
||||
return None;
|
||||
}
|
||||
arc_from_se_radius(self.s, self.e, cursor).map(|(_, r)| r)
|
||||
arc_from_se_radius(self.s, self.e, cursor, self.plane).map(|(_, r)| r)
|
||||
}
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
match self.step {
|
||||
1 => Some(line_wire(self.s, pt)),
|
||||
2 => {
|
||||
if let Some((center, radius)) = arc_from_se_radius(self.s, self.e, pt) {
|
||||
let sa = angle_xy(center, self.s);
|
||||
let ea = angle_xy(center, self.e);
|
||||
Some(arc_preview(center, radius, sa, ea))
|
||||
if let Some((center, radius)) =
|
||||
arc_from_se_radius(self.s, self.e, pt, self.plane)
|
||||
{
|
||||
let sa = angle_xy(center, self.s, self.plane);
|
||||
let ea = angle_xy(center, self.e, self.plane);
|
||||
Some(arc_preview(center, radius, sa, ea, self.plane))
|
||||
} else {
|
||||
Some(line_wire(self.s, self.e))
|
||||
}
|
||||
@@ -1067,6 +1188,7 @@ pub struct ArcSEDCommand {
|
||||
step: u8,
|
||||
s: DVec3,
|
||||
e: DVec3,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcSEDCommand {
|
||||
@@ -1075,11 +1197,16 @@ impl ArcSEDCommand {
|
||||
step: 0,
|
||||
s: DVec3::ZERO,
|
||||
e: DVec3::ZERO,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcSEDCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_SED"
|
||||
}
|
||||
@@ -1102,11 +1229,11 @@ impl CadCommand for ArcSEDCommand {
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => match arc_from_direction(self.s, self.e, pt) {
|
||||
_ => match arc_from_direction(self.s, self.e, pt, self.plane) {
|
||||
Some((center, radius)) => {
|
||||
let sa = angle_xy(center, self.s);
|
||||
let ea = angle_xy(center, self.e);
|
||||
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea))
|
||||
let sa = angle_xy(center, self.s, self.plane);
|
||||
let ea = angle_xy(center, self.e, self.plane);
|
||||
CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane))
|
||||
}
|
||||
None => CmdResult::Cancel,
|
||||
},
|
||||
@@ -1125,10 +1252,12 @@ impl CadCommand for ArcSEDCommand {
|
||||
match self.step {
|
||||
1 => Some(line_wire(self.s, pt)),
|
||||
2 => {
|
||||
if let Some((center, radius)) = arc_from_direction(self.s, self.e, pt) {
|
||||
let sa = angle_xy(center, self.s);
|
||||
let ea = angle_xy(center, self.e);
|
||||
Some(arc_preview(center, radius, sa, ea))
|
||||
if let Some((center, radius)) =
|
||||
arc_from_direction(self.s, self.e, pt, self.plane)
|
||||
{
|
||||
let sa = angle_xy(center, self.s, self.plane);
|
||||
let ea = angle_xy(center, self.e, self.plane);
|
||||
Some(arc_preview(center, radius, sa, ea, self.plane))
|
||||
} else {
|
||||
Some(line_wire(self.s, self.e))
|
||||
}
|
||||
@@ -1148,6 +1277,7 @@ pub struct ArcCSACommand {
|
||||
sa: f64,
|
||||
prev_pt: Option<DVec3>,
|
||||
cw: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcCSACommand {
|
||||
@@ -1159,11 +1289,16 @@ impl ArcCSACommand {
|
||||
sa: 0.0,
|
||||
prev_pt: None,
|
||||
cw: false,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcCSACommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_CSA"
|
||||
}
|
||||
@@ -1190,16 +1325,16 @@ impl CadCommand for ArcCSACommand {
|
||||
}
|
||||
1 => {
|
||||
self.r = self.c.distance(pt);
|
||||
self.sa = angle_xy(self.c, pt);
|
||||
self.sa = angle_xy(self.c, pt, self.plane);
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let ea = angle_xy(self.c, pt);
|
||||
let ea = angle_xy(self.c, pt, self.plane);
|
||||
let e = if self.cw {
|
||||
make_arc(self.c, self.r, ea, self.sa)
|
||||
make_arc(self.c, self.r, ea, self.sa, self.plane)
|
||||
} else {
|
||||
make_arc(self.c, self.r, self.sa, ea)
|
||||
make_arc(self.c, self.r, self.sa, ea, self.plane)
|
||||
};
|
||||
CmdResult::CommitAndExit(e)
|
||||
}
|
||||
@@ -1219,7 +1354,7 @@ impl CadCommand for ArcCSACommand {
|
||||
let span: f64 = text.trim().replace(',', ".").parse().ok()?;
|
||||
let ea = self.sa + span.to_radians();
|
||||
return Some(CmdResult::CommitAndExit(make_arc(
|
||||
self.c, self.r, self.sa, ea,
|
||||
self.c, self.r, self.sa, ea, self.plane,
|
||||
)));
|
||||
}
|
||||
None
|
||||
@@ -1230,7 +1365,9 @@ impl CadCommand for ArcCSACommand {
|
||||
anchor: DynAnchor::Point(self.c),
|
||||
fields: vec![DynFieldSpec::new(DynRole::Angle)],
|
||||
guide: DynGuide::Polar,
|
||||
ref_point: Some(self.c + DVec3::new(self.sa.cos(), self.sa.sin(), 0.0)),
|
||||
ref_point: Some(
|
||||
self.c + self.plane.x * self.sa.cos() + self.plane.y * self.sa.sin(),
|
||||
),
|
||||
})
|
||||
}
|
||||
fn dyn_commit_as_text(&self) -> bool {
|
||||
@@ -1238,7 +1375,9 @@ impl CadCommand for ArcCSACommand {
|
||||
}
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
(self.step == 2).then(|| {
|
||||
crate::command::dyn_display_angle_deg((angle_xy(self.c, cursor) - self.sa) as f32) as f64
|
||||
crate::command::dyn_display_angle_deg(
|
||||
(angle_xy(self.c, cursor, self.plane) - self.sa) as f32,
|
||||
) as f64
|
||||
})
|
||||
}
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
@@ -1249,7 +1388,7 @@ impl CadCommand for ArcCSACommand {
|
||||
// Only flip the sweep once the cursor has moved a clear
|
||||
// angular step; keep the reference point until then so slow
|
||||
// moves accumulate and jitter is ignored.
|
||||
let d = rot_delta(self.c, prev, pt);
|
||||
let d = rot_delta(self.c, prev, pt, self.plane);
|
||||
if d.abs() > DIR_TOL {
|
||||
self.cw = d < 0.0;
|
||||
self.prev_pt = Some(pt);
|
||||
@@ -1257,11 +1396,11 @@ impl CadCommand for ArcCSACommand {
|
||||
} else {
|
||||
self.prev_pt = Some(pt);
|
||||
}
|
||||
let ea = angle_xy(self.c, pt);
|
||||
let ea = angle_xy(self.c, pt, self.plane);
|
||||
Some(if self.cw {
|
||||
arc_preview(self.c, self.r, ea, self.sa)
|
||||
arc_preview(self.c, self.r, ea, self.sa, self.plane)
|
||||
} else {
|
||||
arc_preview(self.c, self.r, self.sa, ea)
|
||||
arc_preview(self.c, self.r, self.sa, ea, self.plane)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
@@ -1278,6 +1417,7 @@ pub struct ArcCSLCommand {
|
||||
s: DVec3,
|
||||
r: f64,
|
||||
sa: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcCSLCommand {
|
||||
@@ -1288,11 +1428,16 @@ impl ArcCSLCommand {
|
||||
s: DVec3::ZERO,
|
||||
r: 0.0,
|
||||
sa: 0.0,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcCSLCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_CSL"
|
||||
}
|
||||
@@ -1320,14 +1465,20 @@ impl CadCommand for ArcCSLCommand {
|
||||
1 => {
|
||||
self.s = pt;
|
||||
self.r = self.c.distance(pt);
|
||||
self.sa = angle_xy(self.c, pt);
|
||||
self.sa = angle_xy(self.c, pt, self.plane);
|
||||
self.step = 2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let chord = self.s.distance(pt);
|
||||
let ea = end_angle_from_chord_len(self.sa, chord, self.r);
|
||||
CmdResult::CommitAndExit(make_arc(self.c, self.r, self.sa, ea))
|
||||
CmdResult::CommitAndExit(make_arc(
|
||||
self.c,
|
||||
self.r,
|
||||
self.sa,
|
||||
ea,
|
||||
self.plane,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1346,7 +1497,7 @@ impl CadCommand for ArcCSLCommand {
|
||||
if chord > 0.0 {
|
||||
let ea = end_angle_from_chord_len(self.sa, chord, self.r);
|
||||
return Some(CmdResult::CommitAndExit(make_arc(
|
||||
self.c, self.r, self.sa, ea,
|
||||
self.c, self.r, self.sa, ea, self.plane,
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -1374,7 +1525,7 @@ impl CadCommand for ArcCSLCommand {
|
||||
2 => {
|
||||
let chord = self.s.distance(pt);
|
||||
let ea = end_angle_from_chord_len(self.sa, chord, self.r);
|
||||
Some(arc_preview(self.c, self.r, self.sa, ea))
|
||||
Some(arc_preview(self.c, self.r, self.sa, ea, self.plane))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -1396,6 +1547,7 @@ pub struct ArcContCommand {
|
||||
/// committed arc was drawn from, so undoing a segment resumes the run
|
||||
/// from the prior anchor instead of ending it.
|
||||
history: Vec<(DVec3, DVec3)>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcContCommand {
|
||||
@@ -1404,12 +1556,17 @@ impl ArcContCommand {
|
||||
s,
|
||||
tangent,
|
||||
ctrl: false,
|
||||
plane: WorkingPlane::default(),
|
||||
history: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcContCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC_CONT"
|
||||
}
|
||||
@@ -1420,9 +1577,9 @@ impl CadCommand for ArcContCommand {
|
||||
self.ctrl = ctrl;
|
||||
}
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match arc_continue(self.s, self.tangent, pt, self.ctrl) {
|
||||
match arc_continue(self.s, self.tangent, pt, self.ctrl, self.plane) {
|
||||
Some((center, radius, sa, ea)) => {
|
||||
let arc = make_arc(center, radius, sa, ea);
|
||||
let arc = make_arc(center, radius, sa, ea, self.plane);
|
||||
// Remember the anchor this arc was drawn from — mid-command
|
||||
// Ctrl+Z reverts the commit and resumes from here.
|
||||
self.history.push((self.s, self.tangent));
|
||||
@@ -1456,8 +1613,10 @@ impl CadCommand for ArcContCommand {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
match arc_continue(self.s, self.tangent, pt, self.ctrl) {
|
||||
Some((center, radius, sa, ea)) => Some(arc_preview(center, radius, sa, ea)),
|
||||
match arc_continue(self.s, self.tangent, pt, self.ctrl, self.plane) {
|
||||
Some((center, radius, sa, ea)) => {
|
||||
Some(arc_preview(center, radius, sa, ea, self.plane))
|
||||
}
|
||||
None => Some(line_wire(self.s, pt)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
enum Step {
|
||||
@@ -37,6 +37,7 @@ pub struct AttdefCommand {
|
||||
text_style: String,
|
||||
width_factor: f64,
|
||||
oblique_angle: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl AttdefCommand {
|
||||
@@ -52,11 +53,16 @@ impl AttdefCommand {
|
||||
text_style,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AttdefCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ATTDEF"
|
||||
}
|
||||
@@ -134,11 +140,12 @@ impl CadCommand for AttdefCommand {
|
||||
default,
|
||||
} = &self.step
|
||||
{
|
||||
let point = self.plane.to_local(pt);
|
||||
let mut attdef = AttributeDefinition {
|
||||
tag: tag.clone(),
|
||||
prompt: prompt.clone(),
|
||||
default_value: default.clone(),
|
||||
insertion_point: Vector3::new(pt.x, pt.y, pt.z),
|
||||
insertion_point: Vector3::new(point.x, point.y, point.z),
|
||||
height: self.height,
|
||||
text_style: self.text_style.clone(),
|
||||
width_factor: self.width_factor,
|
||||
@@ -146,7 +153,10 @@ impl CadCommand for AttdefCommand {
|
||||
..Default::default()
|
||||
};
|
||||
attdef.common.layer = "0".into();
|
||||
CmdResult::CommitAndExit(EntityType::AttributeDefinition(attdef))
|
||||
CmdResult::CommitAndExit(
|
||||
self.plane
|
||||
.place_entity(EntityType::AttributeDefinition(attdef)),
|
||||
)
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -177,12 +187,19 @@ impl CadCommand for AttdefCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if !matches!(self.step, Step::Insertion { .. }) {
|
||||
return None;
|
||||
}
|
||||
// Show a small cross at the insertion point.
|
||||
let d = 0.15_f32;
|
||||
let d = 0.15;
|
||||
let points = [
|
||||
pt - self.plane.x * d,
|
||||
pt + self.plane.x * d,
|
||||
DVec3::splat(f64::NAN),
|
||||
pt - self.plane.y * d,
|
||||
pt + self.plane.y * d,
|
||||
];
|
||||
Some(WireModel {
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
@@ -195,13 +212,10 @@ impl CadCommand for AttdefCommand {
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "attdef_preview".into(),
|
||||
points: vec![
|
||||
[pt.x - d, pt.y, pt.z],
|
||||
[pt.x + d, pt.y, pt.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[pt.x, pt.y, pt.z - d],
|
||||
[pt.x, pt.y, pt.z + d],
|
||||
],
|
||||
points: points
|
||||
.iter()
|
||||
.map(|point| point.as_vec3().to_array())
|
||||
.collect(),
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
|
||||
@@ -19,7 +19,7 @@ use acadrust::{EntityType, Handle, Line};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
// ── Ribbon definition ─────────────────────────────────────────────────────
|
||||
@@ -191,6 +191,7 @@ pub struct CenterLineCommand {
|
||||
/// The entity injected by the host before `on_entity_pick` runs; `None`
|
||||
/// until the host injects it.
|
||||
picked: Option<EntityType>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl CenterLineCommand {
|
||||
@@ -198,19 +199,29 @@ impl CenterLineCommand {
|
||||
Self {
|
||||
first: None,
|
||||
picked: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a `LineGeom` from a picked entity; `None` for anything else.
|
||||
fn as_line(entity: &EntityType) -> Option<LineGeom> {
|
||||
fn as_line(&self, entity: &EntityType) -> Option<LineGeom> {
|
||||
match entity {
|
||||
EntityType::Line(l) => Some(LineGeom::from_line(l)),
|
||||
EntityType::Line(line) => {
|
||||
let mut geom = LineGeom::from_line(line);
|
||||
geom.start = self.plane.to_local(geom.start);
|
||||
geom.end = self.plane.to_local(geom.end);
|
||||
Some(geom)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CenterLineCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CENTERLINE"
|
||||
}
|
||||
@@ -241,7 +252,7 @@ impl CadCommand for CenterLineCommand {
|
||||
}
|
||||
// Resolve the just-picked entity; ignore the pick if it isn't a Line.
|
||||
let picked = self.picked.take();
|
||||
let geom = match picked.as_ref().and_then(Self::as_line) {
|
||||
let geom = match picked.as_ref().and_then(|entity| self.as_line(entity)) {
|
||||
Some(g) => g,
|
||||
None => return CmdResult::NeedPoint,
|
||||
};
|
||||
@@ -257,7 +268,9 @@ impl CadCommand for CenterLineCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Some(first) => match compute_center_line(first, geom) {
|
||||
Some(line) => CmdResult::CommitAndExit(EntityType::Line(line)),
|
||||
Some(line) => CmdResult::CommitAndExit(
|
||||
self.plane.place_entity(EntityType::Line(line)),
|
||||
),
|
||||
// Degenerate combination (coincident / no usable result):
|
||||
// keep prompting for a usable second line.
|
||||
None => CmdResult::NeedPoint,
|
||||
|
||||
+117
-38
@@ -12,7 +12,7 @@ use acadrust::types::Vector3;
|
||||
use acadrust::{Circle, EntityType};
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, DynField, TangentObject};
|
||||
use crate::command::{CadCommand, CmdResult, DynField, TangentObject, WorkingPlane};
|
||||
use crate::modules::draw::defaults;
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
@@ -59,16 +59,18 @@ pub const ICON: IconKind = ICON_CR;
|
||||
|
||||
// ── Shared geometry ────────────────────────────────────────────────────────
|
||||
|
||||
fn circle_wire(center: DVec3, radius: f64) -> WireModel {
|
||||
fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel {
|
||||
let segs = 64u32;
|
||||
let mut pts: Vec<[f64; 3]> = (0..=segs)
|
||||
.map(|i| {
|
||||
let a = (i as f64) * TAU / segs as f64;
|
||||
[
|
||||
center.x + radius * a.cos(),
|
||||
center.y + radius * a.sin(),
|
||||
center.z,
|
||||
]
|
||||
let point = center
|
||||
+ plane.vector_to_world(DVec3::new(
|
||||
radius * a.cos(),
|
||||
radius * a.sin(),
|
||||
0.0,
|
||||
));
|
||||
[point.x, point.y, point.z]
|
||||
})
|
||||
.collect();
|
||||
if let Some(first) = pts.first().cloned() {
|
||||
@@ -77,17 +79,24 @@ fn circle_wire(center: DVec3, radius: f64) -> WireModel {
|
||||
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
|
||||
}
|
||||
|
||||
fn make_circle(center: DVec3, radius: f64) -> EntityType {
|
||||
EntityType::Circle(Circle {
|
||||
fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType {
|
||||
let center = plane.to_local(center);
|
||||
plane.place_entity(EntityType::Circle(Circle {
|
||||
center: Vector3::new(center.x, center.y, center.z),
|
||||
radius,
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// Circumscribed circle through three points.
|
||||
/// Returns `None` if the points are collinear.
|
||||
fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
|
||||
fn circumcircle(
|
||||
a: DVec3,
|
||||
b: DVec3,
|
||||
c: DVec3,
|
||||
plane: WorkingPlane,
|
||||
) -> Option<(DVec3, f64)> {
|
||||
let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c));
|
||||
let ax = a.x;
|
||||
let ay = a.y;
|
||||
let bx = b.x;
|
||||
@@ -107,7 +116,25 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
|
||||
+ (cx * cx + cy * cy) * (bx - ax))
|
||||
/ d;
|
||||
let center = DVec3::new(ux, uy, a.z);
|
||||
Some((center, center.distance(a)))
|
||||
Some((plane.to_world(center), center.distance(a)))
|
||||
}
|
||||
|
||||
fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 {
|
||||
let d = plane.vector_to_local(b - a);
|
||||
d.x.hypot(d.y)
|
||||
}
|
||||
|
||||
fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject {
|
||||
match object {
|
||||
TangentObject::Line { p1, p2 } => TangentObject::Line {
|
||||
p1: plane.to_local(p1),
|
||||
p2: plane.to_local(p2),
|
||||
},
|
||||
TangentObject::Circle { center, radius } => TangentObject::Circle {
|
||||
center: plane.to_local(center),
|
||||
radius,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command: Center, Radius ────────────────────────────────────────────────
|
||||
@@ -115,6 +142,7 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> {
|
||||
pub struct CircleCommand {
|
||||
step: StepCR,
|
||||
default_r: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
enum StepCR {
|
||||
Center,
|
||||
@@ -126,11 +154,16 @@ impl CircleCommand {
|
||||
Self {
|
||||
step: StepCR::Center,
|
||||
default_r: defaults::get_circle_radius(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CircleCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CIRCLE"
|
||||
}
|
||||
@@ -176,9 +209,9 @@ impl CadCommand for CircleCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
StepCR::Radius(c) => {
|
||||
let r = c.distance(pt);
|
||||
let r = plane_distance(*c, pt, self.plane);
|
||||
defaults::set_circle_radius(r);
|
||||
CmdResult::CommitAndExit(make_circle(*c, r))
|
||||
CmdResult::CommitAndExit(make_circle(*c, r, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,7 +219,7 @@ impl CadCommand for CircleCommand {
|
||||
if let StepCR::Radius(c) = &self.step {
|
||||
let c = *c;
|
||||
let r = self.default_r;
|
||||
return CmdResult::CommitAndExit(make_circle(c, r));
|
||||
return CmdResult::CommitAndExit(make_circle(c, r, self.plane));
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
@@ -195,7 +228,7 @@ impl CadCommand for CircleCommand {
|
||||
}
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if let StepCR::Radius(c) = &self.step {
|
||||
Some(circle_wire(*c, c.distance(pt)))
|
||||
Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -217,7 +250,7 @@ impl CadCommand for CircleCommand {
|
||||
let r: f64 = text.trim().replace(',', ".").parse().ok()?;
|
||||
if r > 0.0 {
|
||||
defaults::set_circle_radius(r);
|
||||
return Some(CmdResult::CommitAndExit(make_circle(*c, r)));
|
||||
return Some(CmdResult::CommitAndExit(make_circle(*c, r, self.plane)));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -251,6 +284,7 @@ impl CadCommand for CircleCommand {
|
||||
pub struct CircleCDCommand {
|
||||
step: StepCR,
|
||||
default_d: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl CircleCDCommand {
|
||||
@@ -258,11 +292,16 @@ impl CircleCDCommand {
|
||||
Self {
|
||||
step: StepCR::Center,
|
||||
default_d: defaults::get_circle_diam(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CircleCDCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CIRCLE_CD"
|
||||
}
|
||||
@@ -283,9 +322,9 @@ impl CadCommand for CircleCDCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
StepCR::Radius(c) => {
|
||||
let d = c.distance(pt) * 2.0;
|
||||
let d = plane_distance(*c, pt, self.plane) * 2.0;
|
||||
defaults::set_circle_diam(d);
|
||||
CmdResult::CommitAndExit(make_circle(*c, d / 2.0))
|
||||
CmdResult::CommitAndExit(make_circle(*c, d / 2.0, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,7 +332,7 @@ impl CadCommand for CircleCDCommand {
|
||||
if let StepCR::Radius(c) = &self.step {
|
||||
let c = *c;
|
||||
let d = self.default_d;
|
||||
return CmdResult::CommitAndExit(make_circle(c, (d / 2.0) as f64));
|
||||
return CmdResult::CommitAndExit(make_circle(c, d / 2.0, self.plane));
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
@@ -303,7 +342,7 @@ impl CadCommand for CircleCDCommand {
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
// Preview radius = distance to cursor; on commit that distance becomes the radius (diameter = 2x).
|
||||
if let StepCR::Radius(c) = &self.step {
|
||||
Some(circle_wire(*c, c.distance(pt)))
|
||||
Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -313,7 +352,7 @@ impl CadCommand for CircleCDCommand {
|
||||
let d: f64 = text.trim().replace(',', ".").parse().ok()?;
|
||||
if d > 0.0 {
|
||||
defaults::set_circle_diam(d);
|
||||
return Some(CmdResult::CommitAndExit(make_circle(*c, d / 2.0)));
|
||||
return Some(CmdResult::CommitAndExit(make_circle(*c, d / 2.0, self.plane)));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -339,15 +378,23 @@ impl CadCommand for CircleCDCommand {
|
||||
|
||||
pub struct Circle2PCommand {
|
||||
p1: Option<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl Circle2PCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { p1: None }
|
||||
Self {
|
||||
p1: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for Circle2PCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CIRCLE_2P"
|
||||
}
|
||||
@@ -366,8 +413,8 @@ impl CadCommand for Circle2PCommand {
|
||||
}
|
||||
Some(p1) => {
|
||||
let center = (p1 + pt) * 0.5;
|
||||
let radius = p1.distance(pt) / 2.0;
|
||||
CmdResult::CommitAndExit(make_circle(center, radius))
|
||||
let radius = plane_distance(p1, pt, self.plane) / 2.0;
|
||||
CmdResult::CommitAndExit(make_circle(center, radius, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,8 +427,8 @@ impl CadCommand for Circle2PCommand {
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
let p1 = self.p1?;
|
||||
let center = (p1 + pt) * 0.5;
|
||||
let radius = p1.distance(pt) / 2.0;
|
||||
Some(circle_wire(center, radius))
|
||||
let radius = plane_distance(p1, pt, self.plane) / 2.0;
|
||||
Some(circle_wire(center, radius, self.plane))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,15 +436,23 @@ impl CadCommand for Circle2PCommand {
|
||||
|
||||
pub struct Circle3PCommand {
|
||||
pts: Vec<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl Circle3PCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { pts: Vec::new() }
|
||||
Self {
|
||||
pts: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for Circle3PCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CIRCLE_3P"
|
||||
}
|
||||
@@ -414,8 +469,10 @@ impl CadCommand for Circle3PCommand {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]);
|
||||
match circumcircle(a, b, c) {
|
||||
Some((center, radius)) => CmdResult::CommitAndExit(make_circle(center, radius)),
|
||||
match circumcircle(a, b, c, self.plane) {
|
||||
Some((center, radius)) => {
|
||||
CmdResult::CommitAndExit(make_circle(center, radius, self.plane))
|
||||
}
|
||||
None => {
|
||||
self.pts.pop();
|
||||
CmdResult::NeedPoint
|
||||
@@ -435,14 +492,14 @@ impl CadCommand for Circle3PCommand {
|
||||
// Show circle preview with p1→cursor as diameter (same as 2P).
|
||||
let p1 = self.pts[0];
|
||||
let center = (p1 + pt) * 0.5;
|
||||
let radius = p1.distance(pt) / 2.0;
|
||||
Some(circle_wire(center, radius))
|
||||
let radius = plane_distance(p1, pt, self.plane) / 2.0;
|
||||
Some(circle_wire(center, radius, self.plane))
|
||||
}
|
||||
_ => {
|
||||
// Show circumcircle preview if non-collinear, else polyline.
|
||||
let (a, b) = (self.pts[0], self.pts[1]);
|
||||
if let Some((center, radius)) = circumcircle(a, b, pt) {
|
||||
Some(circle_wire(center, radius))
|
||||
if let Some((center, radius)) = circumcircle(a, b, pt, self.plane) {
|
||||
Some(circle_wire(center, radius, self.plane))
|
||||
} else {
|
||||
Some(WireModel::solid_f64(
|
||||
"rubber_band".into(),
|
||||
@@ -860,6 +917,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64)
|
||||
|
||||
pub struct CircleTTRCommand {
|
||||
step: StepTTR,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
enum StepTTR {
|
||||
@@ -880,11 +938,16 @@ impl CircleTTRCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: StepTTR::First,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CircleTTRCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CIRCLE_TTR"
|
||||
}
|
||||
@@ -918,6 +981,8 @@ impl CadCommand for CircleTTRCommand {
|
||||
}
|
||||
|
||||
fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult {
|
||||
let obj = tangent_object_local(obj, self.plane);
|
||||
let hit = self.plane.to_local(hit);
|
||||
match &self.step {
|
||||
StepTTR::First => {
|
||||
self.step = StepTTR::Second {
|
||||
@@ -955,7 +1020,11 @@ impl CadCommand for CircleTTRCommand {
|
||||
let hint = (*hit1 + *hit2) * 0.5;
|
||||
let candidates = ttr_candidates(*obj1, *obj2, r);
|
||||
if let Some(center) = best_of(&candidates, hint) {
|
||||
Some(CmdResult::CommitAndExit(make_circle(center, r)))
|
||||
Some(CmdResult::CommitAndExit(make_circle(
|
||||
self.plane.to_world(center),
|
||||
r,
|
||||
self.plane,
|
||||
)))
|
||||
} else {
|
||||
Some(CmdResult::Cancel)
|
||||
}
|
||||
@@ -980,6 +1049,7 @@ impl CadCommand for CircleTTRCommand {
|
||||
pub struct CircleTTTCommand {
|
||||
objs: Vec<TangentObject>,
|
||||
hits: Vec<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl CircleTTTCommand {
|
||||
@@ -987,11 +1057,16 @@ impl CircleTTTCommand {
|
||||
Self {
|
||||
objs: Vec::new(),
|
||||
hits: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CircleTTTCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CIRCLE_TTT"
|
||||
}
|
||||
@@ -1013,15 +1088,19 @@ impl CadCommand for CircleTTTCommand {
|
||||
}
|
||||
|
||||
fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult {
|
||||
self.objs.push(obj);
|
||||
self.hits.push(hit);
|
||||
self.objs.push(tangent_object_local(obj, self.plane));
|
||||
self.hits.push(self.plane.to_local(hit));
|
||||
if self.objs.len() < 3 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
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]);
|
||||
match best_circle_of(&candidates, hint) {
|
||||
Some((center, r)) => CmdResult::CommitAndExit(make_circle(center, r)),
|
||||
Some((center, r)) => CmdResult::CommitAndExit(make_circle(
|
||||
self.plane.to_world(center),
|
||||
r,
|
||||
self.plane,
|
||||
)),
|
||||
None => {
|
||||
self.objs.pop();
|
||||
self.hits.pop();
|
||||
|
||||
@@ -41,28 +41,46 @@ impl DimCenterCommand {
|
||||
}
|
||||
|
||||
/// Extract (center, radius) from a Circle or Arc; `None` for anything else.
|
||||
fn center_radius(entity: &EntityType) -> Option<(Vector3, f64)> {
|
||||
fn center_radius(entity: &EntityType) -> Option<(DVec3, f64, DVec3, DVec3)> {
|
||||
match entity {
|
||||
EntityType::Circle(c) => Some((c.center, c.radius)),
|
||||
EntityType::Arc(a) => Some((a.center, a.radius)),
|
||||
EntityType::Circle(circle) => {
|
||||
let normal = (circle.normal.x, circle.normal.y, circle.normal.z);
|
||||
let center = crate::scene::view::transform::ocs_point_to_wcs(
|
||||
(circle.center.x, circle.center.y, circle.center.z),
|
||||
normal,
|
||||
);
|
||||
let (x, y) = crate::scene::view::transform::ocs_axes(normal);
|
||||
Some((
|
||||
DVec3::new(center.0, center.1, center.2),
|
||||
circle.radius,
|
||||
DVec3::new(x.0, x.1, x.2),
|
||||
DVec3::new(y.0, y.1, y.2),
|
||||
))
|
||||
}
|
||||
EntityType::Arc(arc) => {
|
||||
let normal = (arc.normal.x, arc.normal.y, arc.normal.z);
|
||||
let center = crate::scene::view::transform::ocs_point_to_wcs(
|
||||
(arc.center.x, arc.center.y, arc.center.z),
|
||||
normal,
|
||||
);
|
||||
let (x, y) = crate::scene::view::transform::ocs_axes(normal);
|
||||
Some((
|
||||
DVec3::new(center.0, center.1, center.2),
|
||||
arc.radius,
|
||||
DVec3::new(x.0, x.1, x.2),
|
||||
DVec3::new(y.0, y.1, y.2),
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the two cross lines from a center and radius.
|
||||
fn build_cross(center: Vector3, radius: f64) -> Vec<EntityType> {
|
||||
fn build_cross(center: DVec3, radius: f64, x: DVec3, y: DVec3) -> Vec<EntityType> {
|
||||
let m = radius * 0.2;
|
||||
let cx = center.x;
|
||||
let cy = center.y;
|
||||
let cz = center.z;
|
||||
let horizontal = Line::from_points(
|
||||
Vector3::new(cx - m, cy, cz),
|
||||
Vector3::new(cx + m, cy, cz),
|
||||
);
|
||||
let vertical = Line::from_points(
|
||||
Vector3::new(cx, cy - m, cz),
|
||||
Vector3::new(cx, cy + m, cz),
|
||||
);
|
||||
let to_vector = |point: DVec3| Vector3::new(point.x, point.y, point.z);
|
||||
let horizontal = Line::from_points(to_vector(center - x * m), to_vector(center + x * m));
|
||||
let vertical = Line::from_points(to_vector(center - y * m), to_vector(center + y * m));
|
||||
vec![EntityType::Line(horizontal), EntityType::Line(vertical)]
|
||||
}
|
||||
}
|
||||
@@ -93,8 +111,8 @@ impl CadCommand for DimCenterCommand {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
match self.picked.as_ref().and_then(Self::center_radius) {
|
||||
Some((center, radius)) => {
|
||||
let lines = Self::build_cross(center, radius);
|
||||
Some((center, radius, x, y)) => {
|
||||
let lines = Self::build_cross(center, radius, x, y);
|
||||
CmdResult::ReplaceMany(vec![], lines)
|
||||
}
|
||||
// Picked something that is not a circle or arc — keep prompting.
|
||||
|
||||
@@ -15,12 +15,13 @@ use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
|
||||
pub struct DonutCommand {
|
||||
state: DonutState,
|
||||
inner_r: f64,
|
||||
outer_r: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
enum DonutState {
|
||||
@@ -35,11 +36,16 @@ impl DonutCommand {
|
||||
state: DonutState::AskInner,
|
||||
inner_r: 0.0,
|
||||
outer_r: 1.0,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DonutCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DONUT"
|
||||
}
|
||||
@@ -97,9 +103,10 @@ impl CadCommand for DonutCommand {
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match &self.state {
|
||||
DonutState::PlaceCenter => {
|
||||
let entity = make_donut(pt.x as f64, pt.y as f64, self.inner_r, self.outer_r);
|
||||
let center = self.plane.to_local(pt);
|
||||
let entity = make_donut(center.x, center.y, center.z, self.inner_r, self.outer_r);
|
||||
// Keep command active so user can place more donuts.
|
||||
CmdResult::CommitEntity(entity)
|
||||
CmdResult::CommitEntity(self.plane.place_entity(entity))
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
@@ -119,7 +126,7 @@ impl CadCommand for DonutCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType {
|
||||
fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType {
|
||||
use acadrust::types::Vector2;
|
||||
let r_avg = (inner_r + outer_r) / 2.0;
|
||||
let width = outer_r - inner_r;
|
||||
@@ -127,6 +134,7 @@ fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType {
|
||||
let mut p = LwPolyline::new();
|
||||
p.is_closed = true;
|
||||
p.constant_width = width;
|
||||
p.elevation = elevation;
|
||||
|
||||
// Vertex at (cx - r, cy) with bulge 1.0 (180° CCW arc to next vertex)
|
||||
let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy));
|
||||
|
||||
@@ -9,7 +9,7 @@ use acadrust::types::Vector3;
|
||||
use acadrust::{Ellipse, EntityType};
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
@@ -57,13 +57,14 @@ fn ellipse_wire(
|
||||
ratio: f64, // minor/major
|
||||
t_start: f64,
|
||||
t_end: f64,
|
||||
plane: WorkingPlane,
|
||||
) -> WireModel {
|
||||
let r_major = major.length();
|
||||
if r_major < 1e-9 {
|
||||
return WireModel::solid("rubber_band".into(), vec![], WireModel::CYAN, false);
|
||||
}
|
||||
let major_dir = major / r_major;
|
||||
let v = DVec3::Z.cross(major_dir).normalize();
|
||||
let v = plane.z.cross(major_dir).normalize_or(plane.y);
|
||||
let segs = 64u32;
|
||||
// Unwrap t_end so the arc goes counter-clockwise.
|
||||
let t_e = if t_end <= t_start { t_end + TAU } else { t_end };
|
||||
@@ -88,13 +89,21 @@ fn param_angle(center: DVec3, major_dir: DVec3, v: DVec3, pt: DVec3, ratio: f64)
|
||||
}
|
||||
|
||||
/// Build the final Ellipse entity.
|
||||
fn make_ellipse(center: DVec3, major: DVec3, ratio: f64, t_start: f64, t_end: f64) -> Ellipse {
|
||||
fn make_ellipse(
|
||||
center: DVec3,
|
||||
major: DVec3,
|
||||
ratio: f64,
|
||||
t_start: f64,
|
||||
t_end: f64,
|
||||
plane: WorkingPlane,
|
||||
) -> Ellipse {
|
||||
Ellipse {
|
||||
center: Vector3::new(center.x, center.y, center.z),
|
||||
major_axis: Vector3::new(major.x, major.y, major.z),
|
||||
minor_axis_ratio: ratio,
|
||||
start_parameter: t_start,
|
||||
end_parameter: t_end,
|
||||
normal: Vector3::new(plane.z.x, plane.z.y, plane.z.z),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -110,17 +119,23 @@ enum CtrStep {
|
||||
|
||||
pub struct EllipseCommand {
|
||||
step: CtrStep,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl EllipseCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: CtrStep::Center,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for EllipseCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ELLIPSE"
|
||||
}
|
||||
@@ -170,7 +185,7 @@ impl CadCommand for EllipseCommand {
|
||||
let (center, major) = (*center, *major);
|
||||
let ratio = minor_ratio(center, major, pt);
|
||||
CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
|
||||
center, major, ratio, 0.0, TAU,
|
||||
center, major, ratio, 0.0, TAU, self.plane,
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -195,7 +210,7 @@ impl CadCommand for EllipseCommand {
|
||||
if r_minor > 0.0 {
|
||||
let ratio = (r_minor / major.length()).clamp(1e-6, 1.0);
|
||||
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
|
||||
*center, *major, ratio, 0.0, TAU,
|
||||
*center, *major, ratio, 0.0, TAU, self.plane,
|
||||
))));
|
||||
}
|
||||
}
|
||||
@@ -207,7 +222,7 @@ impl CadCommand for EllipseCommand {
|
||||
CtrStep::MajorAxis { center } => Some(line_wire(*center, pt)),
|
||||
CtrStep::MinorRatio { center, major } => {
|
||||
let ratio = minor_ratio(*center, *major, pt).max(0.001);
|
||||
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU))
|
||||
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -250,17 +265,23 @@ enum AxisStep {
|
||||
|
||||
pub struct EllipseAxisCommand {
|
||||
step: AxisStep,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl EllipseAxisCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: AxisStep::Pt1,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for EllipseAxisCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ELLIPSE_AXIS"
|
||||
}
|
||||
@@ -297,7 +318,7 @@ impl CadCommand for EllipseAxisCommand {
|
||||
let (center, major) = (*center, *major);
|
||||
let ratio = minor_ratio(center, major, pt);
|
||||
CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
|
||||
center, major, ratio, 0.0, TAU,
|
||||
center, major, ratio, 0.0, TAU, self.plane,
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -313,7 +334,7 @@ impl CadCommand for EllipseAxisCommand {
|
||||
if r_minor > 0.0 {
|
||||
let ratio = (r_minor / major.length()).clamp(1e-6, 1.0);
|
||||
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
|
||||
*center, *major, ratio, 0.0, TAU,
|
||||
*center, *major, ratio, 0.0, TAU, self.plane,
|
||||
))));
|
||||
}
|
||||
}
|
||||
@@ -326,7 +347,7 @@ impl CadCommand for EllipseAxisCommand {
|
||||
AxisStep::Pt2 { p1 } => Some(line_wire(*p1, pt)),
|
||||
AxisStep::MinorRatio { center, major } => {
|
||||
let ratio = minor_ratio(*center, *major, pt).max(0.001);
|
||||
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU))
|
||||
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,6 +406,7 @@ pub struct EllipseArcCommand {
|
||||
step: ArcStep,
|
||||
prev_pt: Option<DVec3>,
|
||||
cw: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl EllipseArcCommand {
|
||||
@@ -393,11 +415,16 @@ impl EllipseArcCommand {
|
||||
step: ArcStep::Center,
|
||||
prev_pt: None,
|
||||
cw: false,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for EllipseArcCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ELLIPSE_ARC"
|
||||
}
|
||||
@@ -458,7 +485,7 @@ impl CadCommand for EllipseArcCommand {
|
||||
ratio,
|
||||
} => {
|
||||
let (center, major, ratio) = (*center, *major, *ratio);
|
||||
let t_start = angle_from_point(center, major, ratio, pt);
|
||||
let t_start = angle_from_point(center, major, ratio, pt, self.plane);
|
||||
self.prev_pt = None; // reset direction tracking for end-angle step
|
||||
self.step = ArcStep::EndAngle {
|
||||
center,
|
||||
@@ -475,11 +502,11 @@ impl CadCommand for EllipseArcCommand {
|
||||
t_start,
|
||||
} => {
|
||||
let (center, major, ratio, t_start) = (*center, *major, *ratio, *t_start);
|
||||
let t_end = angle_from_point(center, major, ratio, pt);
|
||||
let t_end = angle_from_point(center, major, ratio, pt, self.plane);
|
||||
let entity = if self.cw {
|
||||
make_ellipse(center, major, ratio, t_end, t_start)
|
||||
make_ellipse(center, major, ratio, t_end, t_start, self.plane)
|
||||
} else {
|
||||
make_ellipse(center, major, ratio, t_start, t_end)
|
||||
make_ellipse(center, major, ratio, t_start, t_end, self.plane)
|
||||
};
|
||||
CmdResult::CommitAndExit(EntityType::Ellipse(entity))
|
||||
}
|
||||
@@ -530,7 +557,7 @@ impl CadCommand for EllipseArcCommand {
|
||||
// Typed degrees: positive = CCW, negative = CW.
|
||||
let t_end = val.to_radians();
|
||||
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
|
||||
*center, *major, *ratio, *t_start, t_end,
|
||||
*center, *major, *ratio, *t_start, t_end, self.plane,
|
||||
))));
|
||||
}
|
||||
_ => {}
|
||||
@@ -543,7 +570,7 @@ impl CadCommand for EllipseArcCommand {
|
||||
ArcStep::MajorAxis { center } => Some(line_wire(*center, pt)),
|
||||
ArcStep::MinorRatio { center, major } => {
|
||||
let ratio = minor_ratio(*center, *major, pt).max(0.001);
|
||||
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU))
|
||||
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane))
|
||||
}
|
||||
ArcStep::StartAngle { center, .. } => {
|
||||
// Only the start angle is being chosen here — show a line from
|
||||
@@ -564,8 +591,8 @@ impl CadCommand for EllipseArcCommand {
|
||||
// tolerance ignores jitter; the reference advances only on a
|
||||
// clear move so slow sweeps accumulate.
|
||||
if let Some(prev) = self.prev_pt {
|
||||
let t_prev = angle_from_point(*center, *major, *ratio, prev);
|
||||
let t_cur = angle_from_point(*center, *major, *ratio, pt);
|
||||
let t_prev = angle_from_point(*center, *major, *ratio, prev, self.plane);
|
||||
let t_cur = angle_from_point(*center, *major, *ratio, pt, self.plane);
|
||||
let mut d = t_cur - t_prev;
|
||||
while d > std::f64::consts::PI {
|
||||
d -= TAU;
|
||||
@@ -580,11 +607,11 @@ impl CadCommand for EllipseArcCommand {
|
||||
} else {
|
||||
self.prev_pt = Some(pt);
|
||||
}
|
||||
let t_end = angle_from_point(*center, *major, *ratio, pt);
|
||||
let t_end = angle_from_point(*center, *major, *ratio, pt, self.plane);
|
||||
Some(if self.cw {
|
||||
ellipse_wire(*center, *major, *ratio, t_end, *t_start)
|
||||
ellipse_wire(*center, *major, *ratio, t_end, *t_start, self.plane)
|
||||
} else {
|
||||
ellipse_wire(*center, *major, *ratio, *t_start, t_end)
|
||||
ellipse_wire(*center, *major, *ratio, *t_start, t_end, self.plane)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
@@ -638,13 +665,19 @@ fn minor_ratio(center: DVec3, major: DVec3, pt: DVec3) -> f64 {
|
||||
(r_minor / r_major).clamp(1e-6, 1.0)
|
||||
}
|
||||
|
||||
fn angle_from_point(center: DVec3, major: DVec3, ratio: f64, pt: DVec3) -> f64 {
|
||||
fn angle_from_point(
|
||||
center: DVec3,
|
||||
major: DVec3,
|
||||
ratio: f64,
|
||||
pt: DVec3,
|
||||
plane: WorkingPlane,
|
||||
) -> f64 {
|
||||
let r_major = major.length();
|
||||
if r_major < 1e-9 {
|
||||
return 0.0;
|
||||
}
|
||||
let major_dir = major / r_major;
|
||||
let v = DVec3::Z.cross(major_dir).normalize();
|
||||
let v = plane.z.cross(major_dir).normalize_or(plane.y);
|
||||
param_angle(center, major_dir, v, pt, ratio)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, DynField};
|
||||
use crate::command::{CadCommand, CmdResult, DynField, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
@@ -58,6 +58,7 @@ pub struct HelixCommand {
|
||||
top_radius: f64,
|
||||
height: f64,
|
||||
turns: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl HelixCommand {
|
||||
@@ -69,6 +70,7 @@ impl HelixCommand {
|
||||
top_radius: 0.0,
|
||||
height: 0.0,
|
||||
turns: DEFAULT_TURNS,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +84,12 @@ impl HelixCommand {
|
||||
let angle = turns * std::f64::consts::TAU * frac;
|
||||
let radius = base_radius + (top_radius - base_radius) * frac;
|
||||
let z = height * frac;
|
||||
pts.push(DVec3::new(
|
||||
self.center.x + radius * angle.cos(),
|
||||
self.center.y + radius * angle.sin(),
|
||||
self.center.z + z,
|
||||
));
|
||||
pts.push(
|
||||
self.center
|
||||
+ self.plane.x * (radius * angle.cos())
|
||||
+ self.plane.y * (radius * angle.sin())
|
||||
+ self.plane.z * z,
|
||||
);
|
||||
}
|
||||
pts
|
||||
}
|
||||
@@ -110,6 +113,10 @@ impl HelixCommand {
|
||||
}
|
||||
|
||||
impl CadCommand for HelixCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HELIX"
|
||||
}
|
||||
@@ -134,7 +141,8 @@ impl CadCommand for HelixCommand {
|
||||
Step::BaseRadius => {
|
||||
// A point click resolves the base radius as the planar distance
|
||||
// from the centre to the picked point.
|
||||
let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt();
|
||||
let delta = self.plane.vector_to_local(pt - self.center);
|
||||
let r = delta.x.hypot(delta.y);
|
||||
if r > 0.0 {
|
||||
self.base_radius = r;
|
||||
self.step = Step::TopRadius;
|
||||
@@ -142,14 +150,15 @@ impl CadCommand for HelixCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::TopRadius => {
|
||||
let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt();
|
||||
let delta = self.plane.vector_to_local(pt - self.center);
|
||||
let r = delta.x.hypot(delta.y);
|
||||
self.top_radius = if r > 0.0 { r } else { self.base_radius };
|
||||
self.step = Step::Height;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Height => {
|
||||
// Use the elevation of the picked point relative to the centre.
|
||||
let h = pt.z - self.center.z;
|
||||
let h = (pt - self.center).dot(self.plane.z);
|
||||
if h.abs() > 0.0 {
|
||||
self.height = h;
|
||||
self.step = Step::Turns;
|
||||
@@ -250,7 +259,8 @@ impl CadCommand for HelixCommand {
|
||||
match self.step {
|
||||
Step::Center => None,
|
||||
Step::BaseRadius => {
|
||||
let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt();
|
||||
let delta = self.plane.vector_to_local(pt - self.center);
|
||||
let r = delta.x.hypot(delta.y);
|
||||
if r <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
@@ -267,12 +277,13 @@ impl CadCommand for HelixCommand {
|
||||
// Use the parameters fixed so far; for the height step let the
|
||||
// cursor elevation drive the live height.
|
||||
let height = if self.step == Step::Height {
|
||||
pt.z - self.center.z
|
||||
(pt - self.center).dot(self.plane.z)
|
||||
} else {
|
||||
self.height
|
||||
};
|
||||
let top = if self.step == Step::TopRadius {
|
||||
let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt();
|
||||
let delta = self.plane.vector_to_local(pt - self.center);
|
||||
let r = delta.x.hypot(delta.y);
|
||||
if r > 0.0 {
|
||||
r
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,7 @@ use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
pub struct MlineCommand {
|
||||
@@ -21,6 +21,7 @@ pub struct MlineCommand {
|
||||
style_name: String,
|
||||
style_handle: Option<acadrust::Handle>,
|
||||
style_element_count: usize,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl MlineCommand {
|
||||
@@ -33,6 +34,7 @@ impl MlineCommand {
|
||||
style_name: "Standard".into(),
|
||||
style_handle: None,
|
||||
style_element_count: 2,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +50,16 @@ impl MlineCommand {
|
||||
style_name: style_name.into(),
|
||||
style_handle,
|
||||
style_element_count: style_element_count.max(1),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MlineCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MLINE"
|
||||
}
|
||||
@@ -105,14 +112,18 @@ impl CadCommand for MlineCommand {
|
||||
// Close command
|
||||
if (up == "C" || up == "CLOSE") && self.points.len() >= 3 {
|
||||
let entity = build_mline(
|
||||
&self.points,
|
||||
&self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect::<Vec<_>>(),
|
||||
self.scale,
|
||||
true,
|
||||
&self.style_name,
|
||||
self.style_handle,
|
||||
self.style_element_count,
|
||||
);
|
||||
return Some(CmdResult::CommitAndExit(entity));
|
||||
return Some(CmdResult::CommitAndExit(self.plane.place_entity(entity)));
|
||||
}
|
||||
|
||||
// Scale: "S" alone → prompt for value
|
||||
@@ -144,14 +155,18 @@ impl CadCommand for MlineCommand {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let entity = build_mline(
|
||||
&self.points,
|
||||
&self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect::<Vec<_>>(),
|
||||
self.scale,
|
||||
false,
|
||||
&self.style_name,
|
||||
self.style_handle,
|
||||
self.style_element_count,
|
||||
);
|
||||
CmdResult::CommitAndExit(entity)
|
||||
CmdResult::CommitAndExit(self.plane.place_entity(entity))
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
|
||||
@@ -15,7 +15,7 @@ use acadrust::{EntityType, Handle, LwPolyline};
|
||||
use glam::{DVec2, DVec3, Vec2, Vec3};
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
@@ -52,6 +52,7 @@ pub struct PlineCommand {
|
||||
/// document makes the partial polyline snappable while later vertices are
|
||||
/// being placed. (#119)
|
||||
live_handle: Option<Handle>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl PlineCommand {
|
||||
@@ -62,6 +63,7 @@ impl PlineCommand {
|
||||
mode: SegMode::Line,
|
||||
last_tangent: None,
|
||||
live_handle: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +80,11 @@ impl PlineCommand {
|
||||
// following Arc segment stays tangent-continuous.
|
||||
let n = self.vertices.len();
|
||||
self.last_tangent = if n >= 2 {
|
||||
seg_exit_tangent(self.vertices[n - 2], self.vertices[n - 1], self.bulges[n - 2])
|
||||
seg_exit_tangent(
|
||||
self.plane.to_local(self.vertices[n - 2]),
|
||||
self.plane.to_local(self.vertices[n - 1]),
|
||||
self.bulges[n - 2],
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -116,8 +122,12 @@ impl PlineCommand {
|
||||
if self.vertices.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let lw_verts: Vec<LwVertex> = self
|
||||
let local: Vec<DVec3> = self
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| self.plane.to_local(*vertex))
|
||||
.collect();
|
||||
let lw_verts: Vec<LwVertex> = local
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
@@ -128,10 +138,11 @@ impl PlineCommand {
|
||||
.collect();
|
||||
let pline = LwPolyline {
|
||||
vertices: lw_verts,
|
||||
elevation: local.first().map_or(0.0, |point| point.z),
|
||||
is_closed: closed,
|
||||
..Default::default()
|
||||
};
|
||||
Some(EntityType::LwPolyline(pline))
|
||||
Some(self.plane.place_entity(EntityType::LwPolyline(pline)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +261,10 @@ pub(crate) fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[
|
||||
// ── CadCommand impl ────────────────────────────────────────────────────────
|
||||
|
||||
impl CadCommand for PlineCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PLINE"
|
||||
}
|
||||
@@ -291,11 +306,13 @@ impl CadCommand for PlineCommand {
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if !self.vertices.is_empty() {
|
||||
let last = *self.vertices.last().unwrap();
|
||||
let last_local = self.plane.to_local(last);
|
||||
let pt_local = self.plane.to_local(pt);
|
||||
let last_idx = self.vertices.len() - 1;
|
||||
|
||||
let bulge = match self.mode {
|
||||
SegMode::Line => {
|
||||
let d = DVec2::new(pt.x - last.x, pt.y - last.y);
|
||||
let d = DVec2::new(pt_local.x - last_local.x, pt_local.y - last_local.y);
|
||||
if d.length_squared() > 1e-10 {
|
||||
// Direction only — f32 is sufficient for tangent continuity.
|
||||
self.last_tangent = Some(d.normalize().as_vec2());
|
||||
@@ -303,8 +320,8 @@ impl CadCommand for PlineCommand {
|
||||
0.0
|
||||
}
|
||||
SegMode::Arc => {
|
||||
let a = DVec2::new(last.x, last.y);
|
||||
let b = DVec2::new(pt.x, pt.y);
|
||||
let a = DVec2::new(last_local.x, last_local.y);
|
||||
let b = DVec2::new(pt_local.x, pt_local.y);
|
||||
let tangent = self
|
||||
.last_tangent
|
||||
.map(|t| t.as_dvec2())
|
||||
@@ -417,11 +434,13 @@ impl CadCommand for PlineCommand {
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
let pt = pt.as_vec3();
|
||||
let pt_world = pt;
|
||||
// The committed vertices already render as a real document entity, so
|
||||
// the preview is just the pending segment from the last vertex to the
|
||||
// cursor. (#119) Downcast to f32 for the pixel-space rubber band.
|
||||
let last = self.vertices.last()?.as_vec3();
|
||||
let last_world = *self.vertices.last()?;
|
||||
let last = self.plane.to_local(last_world).as_vec3();
|
||||
let pt = self.plane.to_local(pt_world).as_vec3();
|
||||
|
||||
let mut pts: Vec<[f32; 3]> = Vec::new();
|
||||
match self.mode {
|
||||
@@ -442,9 +461,13 @@ impl CadCommand for PlineCommand {
|
||||
}
|
||||
}
|
||||
|
||||
let world_points = pts
|
||||
.into_iter()
|
||||
.map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array())
|
||||
.collect();
|
||||
Some(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
pts,
|
||||
world_points,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
|
||||
@@ -12,7 +12,7 @@ use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
pub struct ImageCommand {
|
||||
@@ -20,6 +20,7 @@ pub struct ImageCommand {
|
||||
pixel_width: u32,
|
||||
pixel_height: u32,
|
||||
origin: Option<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ImageCommand {
|
||||
@@ -29,6 +30,7 @@ impl ImageCommand {
|
||||
pixel_width,
|
||||
pixel_height,
|
||||
origin: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +43,8 @@ impl ImageCommand {
|
||||
}
|
||||
|
||||
fn make_entity(&self, origin: DVec3, width_pt: DVec3) -> EntityType {
|
||||
let origin = self.plane.to_local(origin);
|
||||
let width_pt = self.plane.to_local(width_pt);
|
||||
let world_width = (width_pt.x - origin.x).abs().max(0.001);
|
||||
let world_height = world_width / self.aspect();
|
||||
|
||||
@@ -56,11 +60,15 @@ impl ImageCommand {
|
||||
);
|
||||
img.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE
|
||||
| acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
|
||||
EntityType::RasterImage(img)
|
||||
self.plane.place_entity(EntityType::RasterImage(img))
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ImageCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"IMAGE"
|
||||
}
|
||||
@@ -91,7 +99,7 @@ impl CadCommand for ImageCommand {
|
||||
// If origin is set, place with a default width of 1 unit * pixel count / 100
|
||||
if let Some(origin) = self.origin {
|
||||
let default_w = (self.pixel_width as f64 / 100.0).max(1.0);
|
||||
let width_pt = DVec3::new(origin.x + default_w, origin.y, origin.z);
|
||||
let width_pt = origin + self.plane.x * default_w;
|
||||
let entity = self.make_entity(origin, width_pt);
|
||||
CmdResult::CommitAndExit(entity)
|
||||
} else {
|
||||
@@ -99,15 +107,20 @@ impl CadCommand for ImageCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
||||
let origin = self.origin?.as_vec3();
|
||||
let world_width = (pt.x - origin.x).abs().max(0.001);
|
||||
let world_height = world_width / self.aspect() as f32;
|
||||
|
||||
let p0 = [origin.x, origin.y, origin.z];
|
||||
let p1 = [origin.x + world_width, origin.y, origin.z];
|
||||
let p2 = [origin.x + world_width, origin.y + world_height, origin.z];
|
||||
let p3 = [origin.x, origin.y + world_height, origin.z];
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
let origin = self.origin?;
|
||||
let origin_local = self.plane.to_local(origin);
|
||||
let point_local = self.plane.to_local(pt);
|
||||
let width = (point_local.x - origin_local.x).abs().max(0.001);
|
||||
let height = width / self.aspect();
|
||||
let corners = [
|
||||
origin_local,
|
||||
origin_local + DVec3::X * width,
|
||||
origin_local + DVec3::new(width, height, 0.0),
|
||||
origin_local + DVec3::Y * height,
|
||||
]
|
||||
.map(|point| self.plane.to_world(point).as_vec3().to_array());
|
||||
let [p0, p1, p2, p3] = corners;
|
||||
|
||||
Some(WireModel {
|
||||
taper_widths: Vec::new(),
|
||||
|
||||
@@ -10,7 +10,7 @@ use acadrust::{entities::LwVertex, EntityType};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
@@ -30,6 +30,7 @@ const DEFAULT_ARC_LEN: f64 = 1.0; // default arc bump length
|
||||
pub struct RevCloudCommand {
|
||||
points: Vec<DVec3>,
|
||||
arc_length: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl RevCloudCommand {
|
||||
@@ -37,11 +38,16 @@ impl RevCloudCommand {
|
||||
Self {
|
||||
points: vec![],
|
||||
arc_length: DEFAULT_ARC_LEN,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for RevCloudCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"REVCLOUD"
|
||||
}
|
||||
@@ -71,8 +77,13 @@ impl CadCommand for RevCloudCommand {
|
||||
if self.points.len() < 3 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let entity = make_revcloud(&self.points, self.arc_length);
|
||||
CmdResult::CommitAndExit(entity)
|
||||
let local: Vec<DVec3> = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let entity = make_revcloud(&local, self.arc_length);
|
||||
CmdResult::CommitAndExit(self.plane.place_entity(entity))
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
@@ -153,6 +164,7 @@ fn make_revcloud(pts: &[DVec3], arc_len: f64) -> EntityType {
|
||||
|
||||
let mut p = LwPolyline::new();
|
||||
p.is_closed = true;
|
||||
p.elevation = pts.first().map_or(0.0, |point| point.z);
|
||||
p.vertices = vertices;
|
||||
EntityType::LwPolyline(p)
|
||||
}
|
||||
|
||||
+146
-98
@@ -15,35 +15,32 @@ use acadrust::types::Vector2;
|
||||
use acadrust::{EntityType, LwPolyline};
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::{DVec3, Mat4};
|
||||
use glam::DVec3;
|
||||
|
||||
/// Build the four corners of an axis-aligned box between opposite corners `a`
|
||||
/// 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
|
||||
/// the UCS axes instead of the world axes.
|
||||
fn ucs_box_corners(a: DVec3, b: DVec3, ucs: Mat4) -> [DVec3; 4] {
|
||||
let ucs = ucs.as_dmat4();
|
||||
let inv = ucs.inverse();
|
||||
let au = inv.transform_point3(a);
|
||||
let bu = inv.transform_point3(b);
|
||||
fn ucs_box_corners(a: DVec3, b: DVec3, plane: WorkingPlane) -> [DVec3; 4] {
|
||||
let au = plane.to_local(a);
|
||||
let bu = plane.to_local(b);
|
||||
[
|
||||
ucs.transform_point3(DVec3::new(au.x, au.y, au.z)),
|
||||
ucs.transform_point3(DVec3::new(bu.x, au.y, au.z)),
|
||||
ucs.transform_point3(DVec3::new(bu.x, bu.y, au.z)),
|
||||
ucs.transform_point3(DVec3::new(au.x, bu.y, au.z)),
|
||||
plane.to_world(DVec3::new(au.x, au.y, au.z)),
|
||||
plane.to_world(DVec3::new(bu.x, au.y, au.z)),
|
||||
plane.to_world(DVec3::new(bu.x, bu.y, au.z)),
|
||||
plane.to_world(DVec3::new(au.x, bu.y, au.z)),
|
||||
]
|
||||
}
|
||||
|
||||
/// Four corners of a box centred at `c` with half-extents taken from `corner`,
|
||||
/// axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = world).
|
||||
fn ucs_box_around_center(c: DVec3, corner: DVec3, ucs: Mat4) -> [DVec3; 4] {
|
||||
let ucs = ucs.as_dmat4();
|
||||
let d = ucs.inverse().transform_vector3(corner - c);
|
||||
let rx = ucs.transform_vector3(DVec3::new(d.x.abs(), 0.0, 0.0));
|
||||
let ry = ucs.transform_vector3(DVec3::new(0.0, d.y.abs(), 0.0));
|
||||
fn ucs_box_around_center(c: DVec3, corner: DVec3, plane: WorkingPlane) -> [DVec3; 4] {
|
||||
let d = plane.vector_to_local(corner - c);
|
||||
let rx = plane.x * d.x.abs();
|
||||
let ry = plane.y * d.y.abs();
|
||||
[c - rx - ry, c + rx - ry, c + rx + ry, c - rx + ry]
|
||||
}
|
||||
|
||||
@@ -84,15 +81,18 @@ pub const ICON: IconKind = ICON_RECT;
|
||||
|
||||
// ── Shared geometry helpers ────────────────────────────────────────────────
|
||||
|
||||
fn make_pline(xy_pairs: &[[f64; 2]]) -> EntityType {
|
||||
EntityType::LwPolyline(LwPolyline {
|
||||
vertices: xy_pairs
|
||||
fn make_pline(points: &[DVec3], plane: WorkingPlane) -> EntityType {
|
||||
let local: Vec<DVec3> = points.iter().map(|point| plane.to_local(*point)).collect();
|
||||
let elevation = local.first().map_or(0.0, |point| point.z);
|
||||
plane.place_entity(EntityType::LwPolyline(LwPolyline {
|
||||
vertices: local
|
||||
.iter()
|
||||
.map(|&[x, y]| LwVertex::new(Vector2::new(x, y)))
|
||||
.map(|point| LwVertex::new(Vector2::new(point.x, point.y)))
|
||||
.collect(),
|
||||
elevation,
|
||||
is_closed: true,
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn wire_loop(pts: Vec<[f64; 3]>) -> WireModel {
|
||||
@@ -114,46 +114,57 @@ fn wire_seg(a: DVec3, b: DVec3) -> WireModel {
|
||||
|
||||
// ── Polygon geometry ───────────────────────────────────────────────────────
|
||||
|
||||
fn poly_verts_xy(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> Vec<[f64; 2]> {
|
||||
fn poly_verts(
|
||||
center: DVec3,
|
||||
vertex_r: f64,
|
||||
sides: u32,
|
||||
start_angle: f64,
|
||||
plane: WorkingPlane,
|
||||
) -> Vec<DVec3> {
|
||||
(0..sides)
|
||||
.map(|i| {
|
||||
let a = start_angle + (i as f64) * TAU / sides as f64;
|
||||
[
|
||||
center.x + vertex_r * a.cos(),
|
||||
center.y + vertex_r * a.sin(),
|
||||
]
|
||||
center + plane.x * (vertex_r * a.cos()) + plane.y * (vertex_r * a.sin())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn poly_wire(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> WireModel {
|
||||
let pts: Vec<[f64; 3]> = (0..sides)
|
||||
.map(|i| {
|
||||
let a = start_angle + (i as f64) * TAU / sides as f64;
|
||||
[
|
||||
center.x + vertex_r * a.cos(),
|
||||
center.y + vertex_r * a.sin(),
|
||||
center.z,
|
||||
]
|
||||
})
|
||||
fn poly_wire(
|
||||
center: DVec3,
|
||||
vertex_r: f64,
|
||||
sides: u32,
|
||||
start_angle: f64,
|
||||
plane: WorkingPlane,
|
||||
) -> WireModel {
|
||||
let pts: Vec<[f64; 3]> = poly_verts(center, vertex_r, sides, start_angle, plane)
|
||||
.into_iter()
|
||||
.map(|point| [point.x, point.y, point.z])
|
||||
.collect();
|
||||
wire_loop(pts)
|
||||
}
|
||||
|
||||
fn angle_xy(from: DVec3, to: DVec3) -> f64 {
|
||||
(to.y - from.y).atan2(to.x - from.x)
|
||||
fn angle_xy(from: DVec3, to: DVec3, plane: WorkingPlane) -> f64 {
|
||||
plane.angle(from, to).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn plane_distance(from: DVec3, to: DVec3, plane: WorkingPlane) -> f64 {
|
||||
let delta = plane.vector_to_local(to - from);
|
||||
delta.x.hypot(delta.y)
|
||||
}
|
||||
|
||||
// ── Command: Rectangle — Two Corners (RECT) ──────────────────────────────
|
||||
|
||||
pub struct RectCommand {
|
||||
a: Option<DVec3>,
|
||||
ucs: Mat4,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl RectCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { a: None, ucs: Mat4::IDENTITY }
|
||||
Self {
|
||||
a: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +172,8 @@ impl CadCommand for RectCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"RECT"
|
||||
}
|
||||
fn set_ucs(&mut self, ucs: Mat4) {
|
||||
self.ucs = ucs;
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
if self.a.is_none() {
|
||||
@@ -209,13 +220,8 @@ impl CadCommand for RectCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Some(a) => {
|
||||
let c = ucs_box_corners(a, pt, self.ucs);
|
||||
CmdResult::CommitAndExit(make_pline(&[
|
||||
[c[0].x, c[0].y],
|
||||
[c[1].x, c[1].y],
|
||||
[c[2].x, c[2].y],
|
||||
[c[3].x, c[3].y],
|
||||
]))
|
||||
let c = ucs_box_corners(a, pt, self.plane);
|
||||
CmdResult::CommitAndExit(make_pline(&c, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,7 +233,7 @@ impl CadCommand for RectCommand {
|
||||
}
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
let a = self.a?;
|
||||
let c = ucs_box_corners(a, pt, self.ucs);
|
||||
let c = ucs_box_corners(a, pt, self.plane);
|
||||
Some(wire_loop(vec![
|
||||
[c[0].x, c[0].y, c[0].z],
|
||||
[c[1].x, c[1].y, c[1].z],
|
||||
@@ -261,6 +267,7 @@ pub struct RectRotCommand {
|
||||
step: u8,
|
||||
a: DVec3,
|
||||
b: DVec3,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl RectRotCommand {
|
||||
@@ -269,11 +276,16 @@ impl RectRotCommand {
|
||||
step: 0,
|
||||
a: DVec3::ZERO,
|
||||
b: DVec3::ZERO,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for RectRotCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"RECT_ROT"
|
||||
}
|
||||
@@ -297,18 +309,18 @@ impl CadCommand for RectRotCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let (a, b) = (self.a, self.b);
|
||||
let (a, b, pt) = (
|
||||
self.plane.to_local(self.a),
|
||||
self.plane.to_local(self.b),
|
||||
self.plane.to_local(pt),
|
||||
);
|
||||
let dir = (b - a).normalize_or_zero();
|
||||
let perp = DVec3::new(-dir.y, dir.x, 0.0);
|
||||
let h = (pt - b).dot(perp); // signed height
|
||||
let c = b + perp * h;
|
||||
let d = a + perp * h;
|
||||
CmdResult::CommitAndExit(make_pline(&[
|
||||
[a.x, a.y],
|
||||
[b.x, b.y],
|
||||
[c.x, c.y],
|
||||
[d.x, d.y],
|
||||
]))
|
||||
let corners = [a, b, c, d].map(|point| self.plane.to_world(point));
|
||||
CmdResult::CommitAndExit(make_pline(&corners, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,18 +334,20 @@ impl CadCommand for RectRotCommand {
|
||||
match self.step {
|
||||
1 => Some(wire_seg(self.a, pt)),
|
||||
2 => {
|
||||
let (a, b) = (self.a, self.b);
|
||||
let (a, b, pt) = (
|
||||
self.plane.to_local(self.a),
|
||||
self.plane.to_local(self.b),
|
||||
self.plane.to_local(pt),
|
||||
);
|
||||
let dir = (b - a).normalize_or_zero();
|
||||
let perp = DVec3::new(-dir.y, dir.x, 0.0);
|
||||
let h = (pt - b).dot(perp);
|
||||
let c = b + perp * h;
|
||||
let d = a + perp * h;
|
||||
Some(wire_loop(vec![
|
||||
[a.x, a.y, a.z],
|
||||
[b.x, b.y, b.z],
|
||||
[c.x, c.y, c.z],
|
||||
[d.x, d.y, d.z],
|
||||
]))
|
||||
let points = [a, b, c, d].map(|point| self.plane.to_world(point));
|
||||
Some(wire_loop(
|
||||
points.into_iter().map(|p| [p.x, p.y, p.z]).collect(),
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -356,9 +370,12 @@ impl CadCommand for RectRotCommand {
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
// Live height = perpendicular distance from the cursor to the base edge.
|
||||
(self.step == 2).then(|| {
|
||||
let dir = (self.b - self.a).normalize_or_zero();
|
||||
let dir = self
|
||||
.plane
|
||||
.vector_to_local(self.b - self.a)
|
||||
.normalize_or_zero();
|
||||
let perp = DVec3::new(-dir.y, dir.x, 0.0);
|
||||
(cursor - self.b).dot(perp).abs()
|
||||
self.plane.vector_to_local(cursor - self.b).dot(perp).abs()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -369,12 +386,15 @@ impl CadCommand for RectRotCommand {
|
||||
|
||||
pub struct RectCenCommand {
|
||||
center: Option<DVec3>,
|
||||
ucs: Mat4,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl RectCenCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { center: None, ucs: Mat4::IDENTITY }
|
||||
Self {
|
||||
center: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,8 +402,8 @@ impl CadCommand for RectCenCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"RECT_CEN"
|
||||
}
|
||||
fn set_ucs(&mut self, ucs: Mat4) {
|
||||
self.ucs = ucs;
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
if self.center.is_none() {
|
||||
@@ -399,13 +419,8 @@ impl CadCommand for RectCenCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Some(c) => {
|
||||
let q = ucs_box_around_center(c, pt, self.ucs);
|
||||
CmdResult::CommitAndExit(make_pline(&[
|
||||
[q[0].x, q[0].y],
|
||||
[q[1].x, q[1].y],
|
||||
[q[2].x, q[2].y],
|
||||
[q[3].x, q[3].y],
|
||||
]))
|
||||
let q = ucs_box_around_center(c, pt, self.plane);
|
||||
CmdResult::CommitAndExit(make_pline(&q, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,7 +432,7 @@ impl CadCommand for RectCenCommand {
|
||||
}
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
let c = self.center?;
|
||||
let q = ucs_box_around_center(c, pt, self.ucs);
|
||||
let q = ucs_box_around_center(c, pt, self.plane);
|
||||
Some(wire_loop(vec![
|
||||
[q[0].x, q[0].y, q[0].z],
|
||||
[q[1].x, q[1].y, q[1].z],
|
||||
@@ -449,6 +464,7 @@ pub struct PolyCommand {
|
||||
sides: u32,
|
||||
step: u8,
|
||||
center: DVec3,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl PolyCommand {
|
||||
@@ -457,11 +473,16 @@ impl PolyCommand {
|
||||
sides: 6,
|
||||
step: 0,
|
||||
center: DVec3::ZERO,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PolyCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"POLY"
|
||||
}
|
||||
@@ -555,9 +576,10 @@ impl CadCommand for PolyCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let r = self.center.distance(pt);
|
||||
let sa = angle_xy(self.center, pt);
|
||||
CmdResult::CommitAndExit(make_pline(&poly_verts_xy(self.center, r, self.sides, sa)))
|
||||
let r = plane_distance(self.center, pt, self.plane);
|
||||
let sa = angle_xy(self.center, pt, self.plane);
|
||||
let vertices = poly_verts(self.center, r, self.sides, sa, self.plane);
|
||||
CmdResult::CommitAndExit(make_pline(&vertices, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,9 +599,9 @@ impl CadCommand for PolyCommand {
|
||||
if self.step < 2 {
|
||||
return None;
|
||||
}
|
||||
let r = self.center.distance(pt);
|
||||
let sa = angle_xy(self.center, pt);
|
||||
Some(poly_wire(self.center, r, self.sides, sa))
|
||||
let r = plane_distance(self.center, pt, self.plane);
|
||||
let sa = angle_xy(self.center, pt, self.plane);
|
||||
Some(poly_wire(self.center, r, self.sides, sa, self.plane))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,6 +613,7 @@ pub struct PolyCCommand {
|
||||
sides: u32,
|
||||
step: u8,
|
||||
center: DVec3,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl PolyCCommand {
|
||||
@@ -599,11 +622,16 @@ impl PolyCCommand {
|
||||
sides: 6,
|
||||
step: 0,
|
||||
center: DVec3::ZERO,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PolyCCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"POLY_C"
|
||||
}
|
||||
@@ -669,18 +697,20 @@ impl CadCommand for PolyCCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
let inradius = self.center.distance(pt);
|
||||
let inradius = plane_distance(self.center, pt, self.plane);
|
||||
let vr = inradius / (PI / self.sides as f64).cos();
|
||||
// The picked pt is at the midpoint of an edge; the vertex is
|
||||
// offset by half a sector (π/N) from that direction.
|
||||
let edge_angle = angle_xy(self.center, pt);
|
||||
let edge_angle = angle_xy(self.center, pt, self.plane);
|
||||
let sa = edge_angle + PI / self.sides as f64;
|
||||
CmdResult::CommitAndExit(make_pline(&poly_verts_xy(
|
||||
let vertices = poly_verts(
|
||||
self.center,
|
||||
vr,
|
||||
self.sides,
|
||||
sa,
|
||||
)))
|
||||
self.plane,
|
||||
);
|
||||
CmdResult::CommitAndExit(make_pline(&vertices, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -700,10 +730,10 @@ impl CadCommand for PolyCCommand {
|
||||
if self.step < 2 {
|
||||
return None;
|
||||
}
|
||||
let inradius = self.center.distance(pt);
|
||||
let inradius = plane_distance(self.center, pt, self.plane);
|
||||
let vr = inradius / (PI / self.sides as f64).cos();
|
||||
let sa = angle_xy(self.center, pt) + PI / self.sides as f64;
|
||||
Some(poly_wire(self.center, vr, self.sides, sa))
|
||||
let sa = angle_xy(self.center, pt, self.plane) + PI / self.sides as f64;
|
||||
Some(poly_wire(self.center, vr, self.sides, sa, self.plane))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,6 +745,7 @@ pub struct PolyECommand {
|
||||
sides: u32,
|
||||
step: u8,
|
||||
a: DVec3,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl PolyECommand {
|
||||
@@ -723,11 +754,16 @@ impl PolyECommand {
|
||||
sides: 6,
|
||||
step: 0,
|
||||
a: DVec3::ZERO,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PolyECommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"POLY_E"
|
||||
}
|
||||
@@ -792,8 +828,11 @@ impl CadCommand for PolyECommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => {
|
||||
if let Some((center, vr, sa)) = edge_poly_params(self.a, pt, self.sides) {
|
||||
CmdResult::CommitAndExit(make_pline(&poly_verts_xy(center, vr, self.sides, sa)))
|
||||
if let Some((center, vr, sa)) =
|
||||
edge_poly_params(self.a, pt, self.sides, self.plane)
|
||||
{
|
||||
let vertices = poly_verts(center, vr, self.sides, sa, self.plane);
|
||||
CmdResult::CommitAndExit(make_pline(&vertices, self.plane))
|
||||
} else {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
@@ -816,8 +855,10 @@ impl CadCommand for PolyECommand {
|
||||
if self.step < 2 {
|
||||
return None;
|
||||
}
|
||||
if let Some((center, vr, sa)) = edge_poly_params(self.a, pt, self.sides) {
|
||||
Some(poly_wire(center, vr, self.sides, sa))
|
||||
if let Some((center, vr, sa)) =
|
||||
edge_poly_params(self.a, pt, self.sides, self.plane)
|
||||
{
|
||||
Some(poly_wire(center, vr, self.sides, sa, self.plane))
|
||||
} else {
|
||||
Some(wire_seg(self.a, pt))
|
||||
}
|
||||
@@ -826,7 +867,13 @@ impl CadCommand for PolyECommand {
|
||||
|
||||
/// Compute polygon center, vertex-radius and start-angle from two edge endpoints.
|
||||
/// The polygon is placed on the left side of A→B (CCW convention).
|
||||
fn edge_poly_params(a: DVec3, b: DVec3, sides: u32) -> Option<(DVec3, f64, f64)> {
|
||||
fn edge_poly_params(
|
||||
a: DVec3,
|
||||
b: DVec3,
|
||||
sides: u32,
|
||||
plane: WorkingPlane,
|
||||
) -> Option<(DVec3, f64, f64)> {
|
||||
let (a, b) = (plane.to_local(a), plane.to_local(b));
|
||||
let edge_len = a.distance(b);
|
||||
if edge_len < 1e-6 {
|
||||
return None;
|
||||
@@ -841,8 +888,9 @@ fn edge_poly_params(a: DVec3, b: DVec3, sides: u32) -> Option<(DVec3, f64, f64)>
|
||||
let mid = (a + b) * 0.5;
|
||||
let center = mid + perp * inradius;
|
||||
// First vertex = A
|
||||
let sa = angle_xy(center, a);
|
||||
Some((center, vr, sa))
|
||||
let center_world = plane.to_world(center);
|
||||
let sa = angle_xy(center_world, plane.to_world(a), plane);
|
||||
Some((center_world, vr, sa))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::EntityType;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
@@ -45,6 +45,7 @@ pub struct SketchCommand {
|
||||
strokes: Vec<Vec<DVec3>>,
|
||||
/// True while the pen is down and samples accumulate into the current stroke.
|
||||
pen_down: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl SketchCommand {
|
||||
@@ -52,15 +53,20 @@ impl SketchCommand {
|
||||
Self {
|
||||
strokes: Vec::new(),
|
||||
pen_down: false,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build one lightweight polyline from a stroke's sampled points, or `None`
|
||||
/// when the stroke has too few points to form a segment.
|
||||
fn build_stroke(points: &[DVec3]) -> Option<EntityType> {
|
||||
fn build_stroke(&self, points: &[DVec3]) -> Option<EntityType> {
|
||||
if points.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let points: Vec<DVec3> = points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let elevation = points[0].z;
|
||||
let mut pl = LwPolyline::new();
|
||||
pl.is_closed = false;
|
||||
@@ -70,19 +76,23 @@ impl SketchCommand {
|
||||
.map(|p| LwVertex::new(Vector2::new(p.x, p.y)))
|
||||
.collect();
|
||||
pl.normal = Vector3::UNIT_Z;
|
||||
Some(EntityType::LwPolyline(pl))
|
||||
Some(self.plane.place_entity(EntityType::LwPolyline(pl)))
|
||||
}
|
||||
|
||||
/// All strokes that have enough points, as committable entities.
|
||||
fn build_all(&self) -> Vec<EntityType> {
|
||||
self.strokes
|
||||
.iter()
|
||||
.filter_map(|s| Self::build_stroke(s))
|
||||
.filter_map(|stroke| self.build_stroke(stroke))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for SketchCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SKETCH"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Command: SOLID (reachable as SO / SOLID2D — the bare SOLID verb currently
|
||||
// toggles the shaded display) — pick three or four corner points and commit a
|
||||
// filled triangle or quadrilateral. Four points may be picked either around
|
||||
// the perimeter (the natural order shown by the rubber band) or in AutoCAD's
|
||||
// the perimeter (the natural order shown by the rubber band) or in the
|
||||
// traditional Z pattern. The command normalizes both forms to the DXF corner
|
||||
// order before committing. Enter after the third point commits a triangle.
|
||||
|
||||
@@ -12,7 +12,7 @@ use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
@@ -34,11 +34,15 @@ pub fn tool() -> ToolDef {
|
||||
pub struct Solid2dCommand {
|
||||
/// Corner points picked so far (3 → triangle, 4 → quadrilateral).
|
||||
points: Vec<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl Solid2dCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { points: Vec::new() }
|
||||
Self {
|
||||
points: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn v3(p: DVec3) -> Vector3 {
|
||||
@@ -86,6 +90,10 @@ impl Solid2dCommand {
|
||||
}
|
||||
|
||||
impl CadCommand for Solid2dCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SOLID"
|
||||
}
|
||||
@@ -112,8 +120,13 @@ impl CadCommand for Solid2dCommand {
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
self.points.push(pt);
|
||||
if self.points.len() == 4 {
|
||||
let solid = Self::solid_from_four_points(&self.points);
|
||||
CmdResult::CommitAndExit(EntityType::Solid(solid))
|
||||
let local: Vec<DVec3> = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let solid = Self::solid_from_four_points(&local);
|
||||
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Solid(solid)))
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -121,9 +134,13 @@ impl CadCommand for Solid2dCommand {
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.points.len() == 3 {
|
||||
let p = &self.points;
|
||||
let p: Vec<DVec3> = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let solid = Solid::triangle(Self::v3(p[0]), Self::v3(p[1]), Self::v3(p[2]));
|
||||
CmdResult::CommitAndExit(EntityType::Solid(solid))
|
||||
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Solid(solid)))
|
||||
} else {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::EntityType;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
@@ -45,6 +45,7 @@ pub struct TraceCommand {
|
||||
/// Every point picked so far, in world space. `points[0]` fixes the band's
|
||||
/// elevation (its Z).
|
||||
points: Vec<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl TraceCommand {
|
||||
@@ -53,6 +54,7 @@ impl TraceCommand {
|
||||
width: 1.0,
|
||||
width_set: false,
|
||||
points: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,13 +64,17 @@ impl TraceCommand {
|
||||
if self.points.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let elevation = self.points[0].z;
|
||||
let local: Vec<DVec3> = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let elevation = local[0].z;
|
||||
let mut pl = LwPolyline::new();
|
||||
pl.is_closed = false;
|
||||
pl.constant_width = self.width;
|
||||
pl.elevation = elevation;
|
||||
pl.vertices = self
|
||||
.points
|
||||
pl.vertices = local
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let mut v = LwVertex::new(Vector2::new(p.x, p.y));
|
||||
@@ -78,11 +84,15 @@ impl TraceCommand {
|
||||
})
|
||||
.collect();
|
||||
pl.normal = Vector3::UNIT_Z;
|
||||
Some(EntityType::LwPolyline(pl))
|
||||
Some(self.plane.place_entity(EntityType::LwPolyline(pl)))
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for TraceCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TRACE"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct WipeoutCommand {
|
||||
mode: WipeoutMode,
|
||||
first: Option<DVec3>,
|
||||
points: Vec<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
@@ -40,6 +41,7 @@ impl WipeoutCommand {
|
||||
mode: WipeoutMode::Draw,
|
||||
first: None,
|
||||
points: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +50,7 @@ impl WipeoutCommand {
|
||||
mode: WipeoutMode::Polyline,
|
||||
first: None,
|
||||
points: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +60,18 @@ impl WipeoutCommand {
|
||||
mode: WipeoutMode::Rectangular,
|
||||
first: None,
|
||||
points: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_draw(&self) -> CmdResult {
|
||||
match make_poly_wipeout(&self.points) {
|
||||
Some(entity) => CmdResult::CommitAndExit(entity),
|
||||
let local: Vec<DVec3> = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
match make_poly_wipeout(&local) {
|
||||
Some(entity) => CmdResult::CommitAndExit(self.plane.place_entity(entity)),
|
||||
None => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
@@ -74,6 +83,10 @@ impl WipeoutCommand {
|
||||
}
|
||||
|
||||
impl CadCommand for WipeoutCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"WIPEOUT"
|
||||
}
|
||||
@@ -131,7 +144,11 @@ impl CadCommand for WipeoutCommand {
|
||||
}
|
||||
WipeoutMode::Rectangular => {
|
||||
if let Some(first) = self.first {
|
||||
CmdResult::CommitAndExit(make_rect_wipeout(first, point))
|
||||
let first = self.plane.to_local(first);
|
||||
let point = self.plane.to_local(point);
|
||||
CmdResult::CommitAndExit(
|
||||
self.plane.place_entity(make_rect_wipeout(first, point)),
|
||||
)
|
||||
} else {
|
||||
self.first = Some(point);
|
||||
CmdResult::NeedPoint
|
||||
@@ -228,15 +245,21 @@ impl CadCommand for WipeoutCommand {
|
||||
}
|
||||
WipeoutMode::Rectangular => {
|
||||
let first = self.first?;
|
||||
let corners = {
|
||||
let first_local = self.plane.to_local(first);
|
||||
let point_local = self.plane.to_local(point);
|
||||
[
|
||||
first_local,
|
||||
DVec3::new(point_local.x, first_local.y, first_local.z),
|
||||
DVec3::new(point_local.x, point_local.y, first_local.z),
|
||||
DVec3::new(first_local.x, point_local.y, first_local.z),
|
||||
first_local,
|
||||
]
|
||||
.map(|corner| self.plane.to_world(corner))
|
||||
};
|
||||
Some(WireModel::solid_f64(
|
||||
"wipeout_preview".into(),
|
||||
vec![
|
||||
[first.x, first.y, first.z],
|
||||
[point.x, first.y, first.z],
|
||||
[point.x, point.y, first.z],
|
||||
[first.x, point.y, first.z],
|
||||
[first.x, first.y, first.z],
|
||||
],
|
||||
corners.iter().map(|p| [p.x, p.y, p.z]).collect(),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
pub struct DistCommand {
|
||||
@@ -11,15 +11,23 @@ pub struct DistCommand {
|
||||
// loses ~0.03–0.06 units at survey-scale coordinates (e.g. eastings near
|
||||
// 5e5), which made snapped-endpoint measurements read off by that much.
|
||||
first: Option<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl DistCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { first: None }
|
||||
Self {
|
||||
first: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DistCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DIST"
|
||||
}
|
||||
@@ -34,11 +42,11 @@ impl CadCommand for DistCommand {
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let Some(p1) = self.first {
|
||||
let delta = pt - p1;
|
||||
let delta = self.plane.vector_to_local(pt - p1);
|
||||
let dist = delta.length();
|
||||
let dx = delta.x;
|
||||
let dy = delta.y; // drawing plane is world XY
|
||||
let dz = delta.z; // elevation
|
||||
let dy = delta.y;
|
||||
let dz = delta.z;
|
||||
|
||||
// Angle in XY plane — degrees from +X
|
||||
let angle_xy = dy.atan2(dx).to_degrees();
|
||||
|
||||
@@ -3,17 +3,25 @@
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
|
||||
pub struct IdCommand;
|
||||
pub struct IdCommand {
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl IdCommand {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
Self {
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for IdCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ID"
|
||||
}
|
||||
@@ -23,10 +31,10 @@ impl CadCommand for IdCommand {
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
// Drawing plane is world XY (z = elevation).
|
||||
let x = pt.x;
|
||||
let y = pt.y;
|
||||
let z = pt.z;
|
||||
let local = self.plane.to_local(pt);
|
||||
let x = local.x;
|
||||
let y = local.y;
|
||||
let z = local.z;
|
||||
let x_s = format!("{x:.4}");
|
||||
let y_s = format!("{y:.4}");
|
||||
let z_s = format!("{z:.4}");
|
||||
|
||||
@@ -18,7 +18,7 @@ use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
@@ -54,6 +54,7 @@ pub struct MeasureGeomCommand {
|
||||
points: Vec<DVec3>,
|
||||
/// The picked entity for RADIUS, injected before `on_entity_pick`.
|
||||
picked: Option<EntityType>,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl MeasureGeomCommand {
|
||||
@@ -62,6 +63,7 @@ impl MeasureGeomCommand {
|
||||
mode: Mode::Choose,
|
||||
points: vec![],
|
||||
picked: None,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,12 +109,14 @@ impl MeasureGeomCommand {
|
||||
}
|
||||
|
||||
/// DISTANCE readout for the two collected points.
|
||||
fn distance_msg(p1: DVec3, p2: DVec3) -> String {
|
||||
fn distance_msg(plane: WorkingPlane, p1: DVec3, p2: DVec3) -> String {
|
||||
let p1 = plane.to_local(p1);
|
||||
let p2 = plane.to_local(p2);
|
||||
let delta = p2 - p1;
|
||||
let dist = delta.length();
|
||||
let dx = delta.x;
|
||||
let dy = delta.y; // drawing plane is world XY
|
||||
let dz = delta.z; // elevation
|
||||
let dy = delta.y;
|
||||
let dz = delta.z;
|
||||
let angle_xy = dy.atan2(dx).to_degrees();
|
||||
let dist_s = format!("{dist:.4}");
|
||||
let angle_xy_s = format!("{angle_xy:.4}");
|
||||
@@ -131,7 +135,8 @@ impl MeasureGeomCommand {
|
||||
}
|
||||
|
||||
/// AREA readout: shoelace area (f64, relative to first vertex) + perimeter.
|
||||
fn area_msg(points: &[DVec3]) -> String {
|
||||
fn area_msg(plane: WorkingPlane, points: &[DVec3]) -> String {
|
||||
let points = points.iter().map(|point| plane.to_local(*point)).collect::<Vec<_>>();
|
||||
let n = points.len();
|
||||
let origin = points[0];
|
||||
let mut area_sum = 0.0f64;
|
||||
@@ -207,6 +212,10 @@ impl CadCommand for MeasureGeomCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
// Only the opening mode-keyword step reads a typed token.
|
||||
self.mode == Mode::Choose
|
||||
@@ -267,7 +276,11 @@ impl CadCommand for MeasureGeomCommand {
|
||||
Mode::Distance => {
|
||||
self.points.push(pt);
|
||||
if self.points.len() == 2 {
|
||||
CmdResult::Measurement(Self::distance_msg(self.points[0], self.points[1]))
|
||||
CmdResult::Measurement(Self::distance_msg(
|
||||
self.plane,
|
||||
self.points[0],
|
||||
self.points[1],
|
||||
))
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -299,7 +312,7 @@ impl CadCommand for MeasureGeomCommand {
|
||||
if self.points.len() < 3 {
|
||||
CmdResult::Cancel
|
||||
} else {
|
||||
CmdResult::Measurement(Self::area_msg(&self.points))
|
||||
CmdResult::Measurement(Self::area_msg(self.plane, &self.points))
|
||||
}
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
// 1. Center point → 2. Item count (text) → 3. Total angle in degrees (text)
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::{DVec3, Mat4};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane};
|
||||
use crate::modules::draw::defaults;
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
@@ -63,7 +63,7 @@ pub struct ArrayRectCommand {
|
||||
default_cols: u32,
|
||||
default_row_sp: f64,
|
||||
default_col_sp: f64,
|
||||
ucs: Mat4,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArrayRectCommand {
|
||||
@@ -76,18 +76,17 @@ impl ArrayRectCommand {
|
||||
default_cols: defaults::get_array_cols() as u32,
|
||||
default_row_sp: defaults::get_array_row_sp(),
|
||||
default_col_sp: defaults::get_array_col_sp(),
|
||||
ucs: Mat4::IDENTITY,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Row/column offsets run along the active UCS axes (`ucs` rotates the
|
||||
/// world-frame grid step), identity = world.
|
||||
/// Row/column offsets run along the active coordinate axes.
|
||||
fn build_transforms(
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
row_sp: f64,
|
||||
col_sp: f64,
|
||||
ucs: Mat4,
|
||||
plane: WorkingPlane,
|
||||
) -> Vec<EntityTransform> {
|
||||
let mut t = Vec::new();
|
||||
for r in 0..rows {
|
||||
@@ -95,9 +94,11 @@ impl ArrayRectCommand {
|
||||
if r == 0 && c == 0 {
|
||||
continue;
|
||||
}
|
||||
t.push(EntityTransform::Translate(ucs.as_dmat4().transform_vector3(
|
||||
glam::DVec3::new(col_sp * c as f64, row_sp * r as f64, 0.0),
|
||||
)));
|
||||
t.push(EntityTransform::Translate(plane.vector_to_world(DVec3::new(
|
||||
col_sp * c as f64,
|
||||
row_sp * r as f64,
|
||||
0.0,
|
||||
))));
|
||||
}
|
||||
}
|
||||
t
|
||||
@@ -109,8 +110,8 @@ impl CadCommand for ArrayRectCommand {
|
||||
"ARRAYRECT"
|
||||
}
|
||||
|
||||
fn set_ucs(&mut self, ucs: Mat4) {
|
||||
self.ucs = ucs;
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
@@ -194,7 +195,7 @@ impl CadCommand for ArrayRectCommand {
|
||||
};
|
||||
Some(CmdResult::BatchCopy(
|
||||
self.handles.clone(),
|
||||
Self::build_transforms(rows, cols, row_sp, col_sp, self.ucs),
|
||||
Self::build_transforms(rows, cols, row_sp, col_sp, self.plane),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -219,7 +220,7 @@ impl CadCommand for ArrayRectCommand {
|
||||
}
|
||||
RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp),
|
||||
};
|
||||
Self::build_transforms(rows, cols, row_sp, col_sp, self.ucs)
|
||||
Self::build_transforms(rows, cols, row_sp, col_sp, self.plane)
|
||||
.iter()
|
||||
.flat_map(|t| {
|
||||
if let EntityTransform::Translate(delta) = t {
|
||||
@@ -262,6 +263,7 @@ pub struct ArrayPolarCommand {
|
||||
step: PolarStep,
|
||||
default_count: u32,
|
||||
default_angle: f64,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArrayPolarCommand {
|
||||
@@ -272,11 +274,16 @@ impl ArrayPolarCommand {
|
||||
step: PolarStep::Center,
|
||||
default_count: defaults::get_array_p_count() as u32,
|
||||
default_angle: defaults::get_array_p_angle(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArrayPolarCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARRAYPOLAR"
|
||||
}
|
||||
@@ -342,6 +349,7 @@ impl CadCommand for ArrayPolarCommand {
|
||||
let transforms = (1..count)
|
||||
.map(|n| EntityTransform::Rotate {
|
||||
center,
|
||||
axis: self.plane.z,
|
||||
angle_rad: step_rad * n as f64,
|
||||
})
|
||||
.collect();
|
||||
@@ -363,12 +371,13 @@ impl CadCommand for ArrayPolarCommand {
|
||||
}
|
||||
};
|
||||
let step_rad = total_deg.to_radians() / count as f64;
|
||||
let axis = self.plane.z.as_vec3();
|
||||
let mut out: Vec<WireModel> = (1..count)
|
||||
.flat_map(|n| {
|
||||
let angle_rad = (step_rad * n as f64) as f32;
|
||||
self.wire_models
|
||||
.iter()
|
||||
.map(move |w| w.rotated(center, angle_rad))
|
||||
.map(move |wire| wire.rotated_about_axis(center, axis, angle_rad))
|
||||
})
|
||||
.collect();
|
||||
// Rubber-band from center to cursor while picking the center point.
|
||||
@@ -675,6 +684,7 @@ impl ArrayPathCommand {
|
||||
let center = Self::rigid_center(p0, p, dth);
|
||||
EntityTransform::Rotate {
|
||||
center,
|
||||
axis: DVec3::Z,
|
||||
angle_rad: dth,
|
||||
}
|
||||
}
|
||||
@@ -814,7 +824,7 @@ impl CadCommand for ArrayPathCommand {
|
||||
.iter()
|
||||
.map(|w| w.translated(delta.as_vec3()))
|
||||
.collect::<Vec<_>>(),
|
||||
EntityTransform::Rotate { center, angle_rad } => self
|
||||
EntityTransform::Rotate { center, angle_rad, .. } => self
|
||||
.wire_models
|
||||
.iter()
|
||||
.map(|w| w.rotated(center.as_vec3(), *angle_rad as f32))
|
||||
|
||||
@@ -12,7 +12,7 @@ use acadrust::Handle;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
@@ -41,6 +41,7 @@ pub struct MirrorCommand {
|
||||
text_ghosts: Vec<(WireModel, DVec3)>,
|
||||
mirror_text: bool,
|
||||
step: Step,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl MirrorCommand {
|
||||
@@ -56,11 +57,16 @@ impl MirrorCommand {
|
||||
text_ghosts,
|
||||
mirror_text,
|
||||
step: Step::P1,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MirrorCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MIRROR"
|
||||
}
|
||||
@@ -147,18 +153,32 @@ impl CadCommand for MirrorCommand {
|
||||
let mut out: Vec<WireModel> = self
|
||||
.wire_models
|
||||
.iter()
|
||||
.map(|w| w.mirrored(p1.as_vec3(), p2.as_vec3()))
|
||||
.map(|w| {
|
||||
w.mirrored_in_plane(
|
||||
p1.as_vec3(),
|
||||
p2.as_vec3(),
|
||||
self.plane.z.as_vec3(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// Text ghosts honour MIRRTEXT: on → true glyph mirror (full reflection);
|
||||
// off → keep glyphs readable, relocate to the mirror-symmetric position
|
||||
// by reflecting the box centre and translating.
|
||||
for (w, center) in &self.text_ghosts {
|
||||
if self.mirror_text {
|
||||
out.push(w.mirrored(p1.as_vec3(), p2.as_vec3()));
|
||||
out.push(w.mirrored_in_plane(
|
||||
p1.as_vec3(),
|
||||
p2.as_vec3(),
|
||||
self.plane.z.as_vec3(),
|
||||
));
|
||||
} else {
|
||||
let (mut cx, mut cy) = (center.x, center.y);
|
||||
crate::scene::view::transform::reflect_xy_point(&mut cx, &mut cy, p1, p2);
|
||||
let delta = glam::Vec3::new((cx - center.x) as f32, (cy - center.y) as f32, 0.0);
|
||||
let reflected = crate::scene::view::transform::reflected_point(
|
||||
*center,
|
||||
p1,
|
||||
p2,
|
||||
self.plane.z,
|
||||
);
|
||||
let delta = (reflected - *center).as_vec3();
|
||||
out.push(w.translated(delta));
|
||||
}
|
||||
}
|
||||
@@ -180,7 +200,11 @@ impl MirrorCommand {
|
||||
/// Commit the mirror. `erase` true flips the originals in place; false
|
||||
/// keeps them and adds a mirrored copy. Either way the command ends.
|
||||
fn finish(&self, p1: DVec3, p2: DVec3, erase: bool) -> CmdResult {
|
||||
let xform = EntityTransform::Mirror { p1, p2 };
|
||||
let xform = EntityTransform::Mirror {
|
||||
p1,
|
||||
p2,
|
||||
working_normal: self.plane.z,
|
||||
};
|
||||
if erase {
|
||||
CmdResult::TransformSelected(self.handles.clone(), xform)
|
||||
} else {
|
||||
|
||||
@@ -13,7 +13,7 @@ use acadrust::Handle;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, DynField, EntityTransform};
|
||||
use crate::command::{CadCommand, CmdResult, DynField, EntityTransform, WorkingPlane};
|
||||
use crate::modules::draw::defaults;
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
@@ -44,6 +44,7 @@ pub struct RotateCommand {
|
||||
wire_models: Vec<WireModel>,
|
||||
step: Step,
|
||||
default_angle: f64, // degrees
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl RotateCommand {
|
||||
@@ -53,6 +54,7 @@ impl RotateCommand {
|
||||
wire_models,
|
||||
step: Step::Center,
|
||||
default_angle: defaults::get_rotate_angle(),
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,12 +62,20 @@ impl RotateCommand {
|
||||
defaults::set_rotate_angle(angle_rad.to_degrees());
|
||||
CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Rotate { center, angle_rad },
|
||||
EntityTransform::Rotate {
|
||||
center,
|
||||
axis: self.plane.z,
|
||||
angle_rad,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for RotateCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ROTATE"
|
||||
}
|
||||
@@ -110,11 +120,9 @@ impl CadCommand for RotateCommand {
|
||||
}
|
||||
Step::Angle { center } => {
|
||||
let center = *center;
|
||||
let direction = pt - center;
|
||||
if direction.x.hypot(direction.y) <= f64::EPSILON {
|
||||
let Some(angle_rad) = self.plane.angle(center, pt) else {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let angle_rad = direction.y.atan2(direction.x);
|
||||
};
|
||||
self.commit(center, angle_rad)
|
||||
}
|
||||
Step::RefFirst { center } => {
|
||||
@@ -123,22 +131,18 @@ impl CadCommand for RotateCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::RefSecond { center, first } => {
|
||||
let direction = pt - *first;
|
||||
if direction.x.hypot(direction.y) <= f64::EPSILON {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let center = *center;
|
||||
let ref_angle = direction.y.atan2(direction.x);
|
||||
let Some(ref_angle) = self.plane.angle(*first, pt) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.step = Step::RefNew { center, ref_angle };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::RefNew { center, ref_angle } => {
|
||||
let center = *center;
|
||||
let direction = pt - center;
|
||||
if direction.x.hypot(direction.y) <= f64::EPSILON {
|
||||
let Some(new_angle) = self.plane.angle(center, pt) else {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let new_angle = direction.y.atan2(direction.x);
|
||||
};
|
||||
self.commit(center, new_angle - *ref_angle)
|
||||
}
|
||||
}
|
||||
@@ -192,11 +196,10 @@ impl CadCommand for RotateCommand {
|
||||
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
|
||||
let (center, angle_rad) = match &self.step {
|
||||
Step::Angle { center } => {
|
||||
let direction = pt - *center;
|
||||
if direction.x.hypot(direction.y) <= f64::EPSILON {
|
||||
let Some(angle) = self.plane.angle(*center, pt) else {
|
||||
return vec![];
|
||||
}
|
||||
(*center, direction.y.atan2(direction.x))
|
||||
};
|
||||
(*center, angle)
|
||||
}
|
||||
Step::RefSecond { first, .. } => {
|
||||
return vec![WireModel::solid(
|
||||
@@ -210,11 +213,10 @@ impl CadCommand for RotateCommand {
|
||||
)];
|
||||
}
|
||||
Step::RefNew { center, ref_angle } => {
|
||||
let direction = pt - *center;
|
||||
if direction.x.hypot(direction.y) <= f64::EPSILON {
|
||||
let Some(angle) = self.plane.angle(*center, pt) else {
|
||||
return vec![];
|
||||
}
|
||||
(*center, direction.y.atan2(direction.x) - *ref_angle)
|
||||
};
|
||||
(*center, angle - *ref_angle)
|
||||
}
|
||||
_ => return vec![],
|
||||
};
|
||||
@@ -222,7 +224,13 @@ impl CadCommand for RotateCommand {
|
||||
// drawn by the dynamic-input overlay (polar guide), not here.
|
||||
self.wire_models
|
||||
.iter()
|
||||
.map(|w| w.rotated(center.as_vec3(), angle_rad as f32))
|
||||
.map(|w| {
|
||||
w.rotated_about_axis(
|
||||
center.as_vec3(),
|
||||
self.plane.z.as_vec3(),
|
||||
angle_rad as f32,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user