Merge main into codex/graphic-attributes-cleanup

Conflicts were additions on both sides, kept together: the Count palette
and Sheet Set toggles next to the Graphic Attributes toggle in the ribbon,
main's new locale groups and catalog entries next to graphic-attributes,
and module declarations. The palette code follows main's crate renames
(acadrust -> codec, cadkernel -> kernel).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VgkJN8DEojuPTumAXLHotu
This commit is contained in:
Claude committed 2026-10-07 20:11:05 +00:00
commit 7a03e1c43c
625 files changed
+104397 -28053

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ pub const MENU_ITEMS: &[(&str, &str, IconKind)] = &[
// ── CadCommand implementation ─────────────────────────────────────────────
use acadrust::Handle;
use codec::Handle;
use glam::{DVec3, Vec3};
use crate::t;
+8
View File
@@ -32,6 +32,7 @@ thread_local! {
static HELIX_HEIGHT: Cell<f64> = Cell::new(1.0);
static HELIX_TURNS: Cell<f64> = Cell::new(3.0);
static HELIX_COUNTER_CLOCKWISE: Cell<bool> = Cell::new(true);
static FILLET_TRIM: Cell<bool> = Cell::new(true); // TRIMMODE
}
macro_rules! accessors {
@@ -85,6 +86,13 @@ pub fn get_helix_counter_clockwise() -> bool {
pub fn set_helix_counter_clockwise(value: bool) {
HELIX_COUNTER_CLOCKWISE.with(|current| current.set(value));
}
pub fn get_fillet_trim() -> bool {
FILLET_TRIM.with(|value| value.get())
}
pub fn set_fillet_trim(value: bool) {
FILLET_TRIM.with(|current| current.set(value));
}
accessors!(
get_donut_inner_diameter,
set_donut_inner_diameter,
+332
View File
@@ -0,0 +1,332 @@
// Apollonius Circle Solvers for Fluid Tangent Arcs.
//
// Solves:
// - CCP (Curve-Curve-Point): Circle passing through point P and tangent to two circles.
// - PPC (Point-Point-Curve): Circle passing through points P1, P2 and tangent to a circle.
//
// Both use Circle Inversion centered at the pass-through point, mapping circles to
// circles/lines and reducing the problem to basic bitangent or point-tangent lines,
// then inverting back. Closed-form, O(1), and numerically stable.
use glam::DVec2;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Circle2D {
pub center: DVec2,
pub radius: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct ApolloniusSolution {
pub center: DVec2,
pub radius: f64,
/// Tangent contact point on the first curve.
pub tangent1: DVec2,
/// Tangent contact point on the second curve (for CCP) or None (for PPC).
pub tangent2: Option<DVec2>,
}
/// Invert a circle about origin with inversion radius k.
/// If circle does not pass through origin, returns inverted Circle2D.
fn invert_circle(c: Circle2D, k: f64) -> Option<Circle2D> {
let d2 = c.center.length_squared();
let denom = d2 - c.radius * c.radius;
if denom.abs() < 1e-9 {
// Circle passes through origin -> inverts to a straight line
return None;
}
let scale = (k * k) / denom;
Some(Circle2D {
center: c.center * scale,
radius: (scale * c.radius).abs(),
})
}
/// Invert a line `n.x * x + n.y * y + c = 0` (where |n| = 1) about origin with radius k.
/// Line inverts to a circle through origin.
fn invert_line_to_circle(n: DVec2, c: f64, k: f64) -> Option<Circle2D> {
if c.abs() < 1e-9 {
// Line passes through origin -> inverts to itself
return None;
}
let radius = (k * k) / (2.0 * c.abs());
let center = -n * (k * k / (2.0 * c));
Some(Circle2D { center, radius })
}
/// Find the contact point on `original` closest to tangency with `sol`.
fn compute_contact_point(original: Circle2D, sol: Circle2D) -> DVec2 {
let dir = sol.center - original.center;
let dist = dir.length();
if dist < 1e-9 {
return original.center + DVec2::new(original.radius, 0.0);
}
let u = dir / dist;
// Choose sign + or - depending on whether sol is external or enclosing
let p_plus = original.center + u * original.radius;
let p_minus = original.center - u * original.radius;
if ((p_plus - sol.center).length() - sol.radius).abs() < 1e-3 {
p_plus
} else {
p_minus
}
}
/// Bitangent lines between two circles: returns line normals `n` (|n| = 1) and constants `c`
/// such that `n . x + c = 0`.
fn bitangent_lines(c1: Circle2D, c2: Circle2D) -> Vec<(DVec2, f64)> {
let mut lines = Vec::with_capacity(4);
let d_vec = c2.center - c1.center;
let d = d_vec.length();
if d < 1e-9 {
return lines;
}
let u = d_vec / d;
let v = DVec2::new(-u.y, u.x);
// 1. External bitangents (r_diff = r1 - r2)
let r_diff = c1.radius - c2.radius;
if d >= r_diff.abs() - 1e-9 {
let sin_a = (r_diff / d).clamp(-1.0, 1.0);
let cos_a = (1.0 - sin_a * sin_a).max(0.0).sqrt();
for sign in [-1.0, 1.0] {
let n = u * sin_a + v * (cos_a * sign);
let c = -(n.dot(c1.center) + c1.radius);
lines.push((n, c));
}
}
// 2. Internal bitangents (r_sum = r1 + r2)
let r_sum = c1.radius + c2.radius;
if d >= r_sum - 1e-9 {
let sin_a = (r_sum / d).clamp(-1.0, 1.0);
let cos_a = (1.0 - sin_a * sin_a).max(0.0).sqrt();
for sign in [-1.0, 1.0] {
let n = u * sin_a + v * (cos_a * sign);
let c = -(n.dot(c1.center) + c1.radius);
lines.push((n, c));
}
}
lines
}
/// Tangent lines from a point to a circle: returns lines `n . x + c = 0`.
fn point_circle_tangent_lines(pt: DVec2, circle: Circle2D) -> Vec<(DVec2, f64)> {
let mut lines = Vec::with_capacity(2);
let d_vec = circle.center - pt;
let d = d_vec.length();
if d < circle.radius - 1e-9 {
return lines;
}
if d < 1e-9 {
return lines;
}
let u = d_vec / d;
let v = DVec2::new(-u.y, u.x);
let sin_a = (circle.radius / d).clamp(-1.0, 1.0);
let cos_a = (1.0 - sin_a * sin_a).max(0.0).sqrt();
for sign in [-1.0, 1.0] {
let n = u * sin_a + v * (cos_a * sign);
let c = -n.dot(pt);
lines.push((n, c));
}
lines
}
/// Solve Apollonius CCP: circle passing through point `p` and tangent to circles `c1` and `c2`.
pub fn solve_ccp(c1: Circle2D, c2: Circle2D, p: DVec2) -> Vec<ApolloniusSolution> {
let k = 100.0; // Inversion radius
// Translate origin to p
let c1_local = Circle2D {
center: c1.center - p,
radius: c1.radius,
};
let c2_local = Circle2D {
center: c2.center - p,
radius: c2.radius,
};
let Some(c1_inv) = invert_circle(c1_local, k) else {
return Vec::new();
};
let Some(c2_inv) = invert_circle(c2_local, k) else {
return Vec::new();
};
let lines = bitangent_lines(c1_inv, c2_inv);
let mut solutions = Vec::new();
for (n, c) in lines {
if let Some(sol_local) = invert_line_to_circle(n, c, k) {
let sol = Circle2D {
center: sol_local.center + p,
radius: sol_local.radius,
};
// Verify sol is actually tangent to c1 and c2 within tolerance
let d1 = (sol.center - c1.center).length();
let d2 = (sol.center - c2.center).length();
let tan1_valid = ((d1 - (sol.radius + c1.radius)).abs() < 1e-4)
|| ((d1 - (sol.radius - c1.radius).abs()).abs() < 1e-4);
let tan2_valid = ((d2 - (sol.radius + c2.radius)).abs() < 1e-4)
|| ((d2 - (sol.radius - c2.radius).abs()).abs() < 1e-4);
if tan1_valid && tan2_valid {
let tangent1 = compute_contact_point(c1, sol);
let tangent2 = compute_contact_point(c2, sol);
solutions.push(ApolloniusSolution {
center: sol.center,
radius: sol.radius,
tangent1,
tangent2: Some(tangent2),
});
}
}
}
solutions
}
/// Solve Apollonius PPC: circle passing through points `p1` and `p2`, and tangent to circle `c1`.
pub fn solve_ppc(c1: Circle2D, p1: DVec2, p2: DVec2) -> Vec<ApolloniusSolution> {
if (p1 - p2).length_squared() < 1e-12 {
return Vec::new();
}
let k = 100.0;
// Translate origin to p1
let c1_local = Circle2D {
center: c1.center - p1,
radius: c1.radius,
};
let p2_local = p2 - p1;
let Some(c1_inv) = invert_circle(c1_local, k) else {
return Vec::new();
};
let p2_inv_len2 = p2_local.length_squared();
if p2_inv_len2 < 1e-12 {
return Vec::new();
}
let p2_inv = p2_local * (k * k / p2_inv_len2);
let lines = point_circle_tangent_lines(p2_inv, c1_inv);
let mut solutions = Vec::new();
for (n, c) in lines {
if let Some(sol_local) = invert_line_to_circle(n, c, k) {
let sol = Circle2D {
center: sol_local.center + p1,
radius: sol_local.radius,
};
let d1 = (sol.center - c1.center).length();
let tan1_valid = ((d1 - (sol.radius + c1.radius)).abs() < 1e-4)
|| ((d1 - (sol.radius - c1.radius).abs()).abs() < 1e-4);
let passes_p2 = ((sol.center - p2).length() - sol.radius).abs() < 1e-4;
if tan1_valid && passes_p2 {
let tangent1 = compute_contact_point(c1, sol);
solutions.push(ApolloniusSolution {
center: sol.center,
radius: sol.radius,
tangent1,
tangent2: None,
});
}
}
}
solutions
}
/// Choose the best CCP candidate closest to pick hints `hint1` and `hint2`.
pub fn best_ccp(
candidates: &[ApolloniusSolution],
hint1: DVec2,
hint2: DVec2,
) -> Option<ApolloniusSolution> {
candidates
.iter()
.filter(|sol| sol.radius.is_finite() && sol.radius > 1e-4)
.min_by(|a, b| {
let score = |s: &ApolloniusSolution| -> f64 {
let d1 = (s.tangent1 - hint1).length();
let d2 = s.tangent2.map_or(0.0, |t| (t - hint2).length());
let r_pen = if s.radius > 50000.0 { 10000.0 } else { 0.0 };
d1 + d2 + r_pen
};
score(a).total_cmp(&score(b))
})
.copied()
}
/// Choose the best PPC candidate closest to pick hint `hint1`.
pub fn best_ppc(
candidates: &[ApolloniusSolution],
hint1: DVec2,
) -> Option<ApolloniusSolution> {
candidates
.iter()
.filter(|sol| sol.radius.is_finite() && sol.radius > 1e-4)
.min_by(|a, b| {
let score = |s: &ApolloniusSolution| -> f64 {
let d1 = (s.tangent1 - hint1).length();
let r_pen = if s.radius > 50000.0 { 10000.0 } else { 0.0 };
d1 + r_pen
};
score(a).total_cmp(&score(b))
})
.copied()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ccp_two_equal_circles() {
// Two circles of radius 10 at (-20, 0) and (20, 0).
// Middle point P at (0, 10).
// A circle through (0, 10) tangent to both circles at their top (+Y).
let c1 = Circle2D {
center: DVec2::new(-20.0, 0.0),
radius: 10.0,
};
let c2 = Circle2D {
center: DVec2::new(20.0, 0.0),
radius: 10.0,
};
let p = DVec2::new(0.0, 10.0);
let sols = solve_ccp(c1, c2, p);
assert!(!sols.is_empty(), "Should find CCP solutions");
let best = best_ccp(&sols, DVec2::new(-20.0, 10.0), DVec2::new(20.0, 10.0)).unwrap();
// Check that distance to c1 center is radius +- 10
let d1 = (best.center - c1.center).length();
assert!((d1 - (best.radius + c1.radius)).abs() < 1e-3 || (d1 - (best.radius - c1.radius).abs()).abs() < 1e-3);
// Check that point P is on the circle
assert!(((best.center - p).length() - best.radius).abs() < 1e-3);
}
#[test]
fn test_ppc_circle_and_two_points() {
// Circle of radius 10 at (0, 0).
// Points at (0, 20) and (20, 20).
let c = Circle2D {
center: DVec2::new(0.0, 0.0),
radius: 10.0,
};
let p1 = DVec2::new(0.0, 20.0);
let p2 = DVec2::new(20.0, 20.0);
let sols = solve_ppc(c, p1, p2);
assert!(!sols.is_empty(), "Should find PPC solutions");
let best = best_ppc(&sols, DVec2::new(0.0, 10.0)).unwrap();
assert!(((best.center - p1).length() - best.radius).abs() < 1e-3);
assert!(((best.center - p2).length() - best.radius).abs() < 1e-3);
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -1,8 +1,8 @@
//! Associative centre-line construction from lines and linear polyline segments.
use acadrust::entities::{CenterLineAssociation, CenterLineSource};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use codec::entities::{CenterLineAssociation, CenterLineSource};
use codec::types::Vector3;
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
+594 -13
View File
@@ -1,8 +1,8 @@
// Circle creation commands.
use acadrust::types::Vector3;
use acadrust::{Circle, EntityType};
use cadkernel::geom2d::{
use codec::types::Vector3;
use codec::{Circle, EntityType};
use kernel::geom2d::{
fillets_between, Circle as KernelCircle, Curve as KernelCurve, Line as KernelLine,
Tolerance,
};
@@ -12,7 +12,8 @@ use crate::command::{CadCommand, CmdResult, DynField, TangentObject, WorkingPlan
use crate::modules::draw::defaults;
use crate::modules::IconKind;
use crate::scene::model::wire_model::{TangentGeom, WireModel};
use glam::DVec3;
use super::arc::Ellipse2D;
use glam::{DVec2, DVec3};
const TAU: f64 = std::f64::consts::TAU;
@@ -91,7 +92,7 @@ fn circumcircle(
plane: WorkingPlane,
) -> Option<(DVec3, f64)> {
let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c));
let circle = cadkernel::geom2d::arc_through_points(
let circle = kernel::geom2d::arc_through_points(
[a.x, a.y],
[b.x, b.y],
[c.x, c.y],
@@ -115,6 +116,17 @@ pub(crate) fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -
center: plane.to_local(center),
radius,
},
TangentObject::Ellipse {
center,
major_axis,
normal,
minor_axis_ratio,
} => TangentObject::Ellipse {
center: plane.to_local(center),
major_axis: plane.vector_to_local(major_axis),
normal: plane.vector_to_local(normal),
minor_axis_ratio,
},
}
}
@@ -601,6 +613,7 @@ fn solve_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
}
#[allow(dead_code)]
pub(crate) fn best_of(candidates: &[DVec3], hint: DVec3) -> Option<DVec3> {
candidates.iter().copied().min_by(|a, b| {
a.distance(hint)
@@ -609,6 +622,169 @@ pub(crate) fn best_of(candidates: &[DVec3], hint: DVec3) -> Option<DVec3> {
})
}
fn closest_point_on_obj(obj: &TangentObject, pt: DVec3, radius: f64) -> DVec3 {
match obj {
TangentObject::Line { p1, p2 } => {
let d = *p2 - *p1;
let len2 = d.x * d.x + d.y * d.y;
if len2 < 1e-12 {
*p1
} else {
let t = ((pt.x - p1.x) * d.x + (pt.y - p1.y) * d.y) / len2;
DVec3::new(p1.x + d.x * t, p1.y + d.y * t, 0.0)
}
}
TangentObject::Circle { center, radius: r_obj } => {
let dx = pt.x - center.x;
let dy = pt.y - center.y;
let len = dx.hypot(dy);
if len < 1e-9 {
DVec3::new(center.x + r_obj, center.y, 0.0)
} else {
let p_plus = DVec3::new(center.x + dx * (r_obj / len), center.y + dy * (r_obj / len), 0.0);
let p_minus = DVec3::new(center.x - dx * (r_obj / len), center.y - dy * (r_obj / len), 0.0);
if (p_plus.distance(pt) - radius).abs() < (p_minus.distance(pt) - radius).abs() {
p_plus
} else {
p_minus
}
}
}
TangentObject::Ellipse { center, major_axis, normal, minor_axis_ratio } => {
let ell = Ellipse2D::new(
DVec2::new(center.x, center.y),
DVec2::new(major_axis.x, major_axis.y),
normal.z,
*minor_axis_ratio,
);
let p_2d = DVec2::new(pt.x, pt.y);
let norm_pts = ell.normal_points_for_center(p_2d);
let closest_2d = norm_pts
.into_iter()
.min_by(|a, b| {
let da = ((*a - p_2d).length() - radius).abs();
let db = ((*b - p_2d).length() - radius).abs();
da.total_cmp(&db)
})
.unwrap_or_else(|| ell.project_point(p_2d));
DVec3::new(closest_2d.x, closest_2d.y, 0.0)
}
}
}
fn object_side_alignment(obj: &TangentObject, c: DVec3, target: DVec3) -> f64 {
match obj {
TangentObject::Line { p1, p2 } => {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = dx.hypot(dy);
if len < 1e-12 {
return 1.0;
}
let nx = -dy / len;
let ny = dx / len;
let h_c = (c.x - p1.x) * nx + (c.y - p1.y) * ny;
let h_t = (target.x - p1.x) * nx + (target.y - p1.y) * ny;
if h_t.abs() < 1e-9 {
1.0
} else {
(h_c * h_t).signum()
}
}
TangentObject::Circle { center, radius: r_obj } => {
let dc = DVec2::new(c.x - center.x, c.y - center.y);
let dt = DVec2::new(target.x - center.x, target.y - center.y);
let lc = dc.length();
let lt = dt.length();
if lc < 1e-9 || lt < 1e-9 {
return 1.0;
}
let dir_dot = dc.dot(dt) / (lc * lt);
let c_is_external = lc > *r_obj;
let t_is_external = lt > *r_obj;
if c_is_external == t_is_external {
dir_dot
} else {
-1.0
}
}
TangentObject::Ellipse { center, major_axis, normal, minor_axis_ratio } => {
let ell = Ellipse2D::new(
DVec2::new(center.x, center.y),
DVec2::new(major_axis.x, major_axis.y),
normal.z,
*minor_axis_ratio,
);
let c_2d = DVec2::new(c.x, c.y);
let t_2d = DVec2::new(target.x, target.y);
let p_c = ell.project_point(c_2d);
let n_c = (c_2d - p_c).normalize_or_zero();
let p_t = ell.project_point(t_2d);
let n_t = (t_2d - p_t).normalize_or_zero();
n_c.dot(n_t)
}
}
}
fn score_ttr_candidate(
c: DVec3,
obj1: &TangentObject,
obj2: &TangentObject,
radius: f64,
r_inv: f64,
hit1: DVec3,
hit2: DVec3,
cursor: Option<DVec3>,
) -> f64 {
let t1 = closest_point_on_obj(obj1, c, radius);
let t2 = closest_point_on_obj(obj2, c, radius);
let (s1, s2, d_cur) = if let Some(cur) = cursor {
let s1 = object_side_alignment(obj1, c, cur);
let s2 = object_side_alignment(obj2, c, cur);
let d_cur = ((cur - c).length() - radius).abs();
(s1, s2, d_cur)
} else {
// Without cursor, obj1's desired side is towards hit2, obj2's desired side is towards hit1
let s1 = object_side_alignment(obj1, c, hit2);
let s2 = object_side_alignment(obj2, c, hit1);
(s1, s2, 0.0)
};
let mismatch = (if s1 < 0.0 { 1.0 } else { 0.0 }) + (if s2 < 0.0 { 1.0 } else { 0.0 });
let d_pick = (t1 - hit1).length() + (t2 - hit2).length();
mismatch * 1000.0 - (s1 + s2) * 20.0 + (d_cur + d_pick) * r_inv
}
pub(crate) fn pick_best_ttr_candidate(
obj1: TangentObject,
obj2: TangentObject,
radius: f64,
hit1: DVec3,
hit2: DVec3,
cursor: Option<DVec3>,
) -> Option<DVec3> {
let candidates = ttr_candidates(obj1, obj2, radius);
if candidates.is_empty() {
return None;
}
if candidates.len() == 1 {
return Some(candidates[0]);
}
let r_inv = 1.0 / radius.max(1e-6);
candidates
.into_iter()
.min_by(|&c_a, &c_b| {
let score_a = score_ttr_candidate(c_a, &obj1, &obj2, radius, r_inv, hit1, hit2, cursor);
let score_b = score_ttr_candidate(c_b, &obj1, &obj2, radius, r_inv, hit1, hit2, cursor);
score_a.total_cmp(&score_b)
})
}
fn best_circle_of(candidates: &[(DVec3, f64)], hint: DVec3) -> Option<(DVec3, f64)> {
candidates
.iter()
@@ -631,10 +807,242 @@ fn tangent_curve(object: TangentObject) -> KernelCurve {
centre: [center.x, center.y],
radius,
}),
TangentObject::Ellipse {
center,
major_axis,
minor_axis_ratio,
..
} => {
let a = major_axis.length();
let major_axis_2d = if a > 1e-9 {
[major_axis.x / a, major_axis.y / a]
} else {
[1.0, 0.0]
};
KernelCurve::Ellipse(kernel::geom2d::EllipseArc {
ellipse: kernel::geom2d::Ellipse {
centre: [center.x, center.y],
major_radius: a,
minor_radius: a * minor_axis_ratio,
major_axis: major_axis_2d,
},
start_parameter: 0.0,
end_parameter: std::f64::consts::TAU,
})
}
}
}
fn to_ellipse2d(obj: TangentObject) -> Option<Ellipse2D> {
match obj {
TangentObject::Ellipse {
center,
major_axis,
normal,
minor_axis_ratio,
} => Some(Ellipse2D::new(
DVec2::new(center.x, center.y),
DVec2::new(major_axis.x, major_axis.y),
normal.z,
minor_axis_ratio,
)),
_ => None,
}
}
fn solve_1d_roots(
n_steps: usize,
mut eval: impl FnMut(f64) -> Option<f64>,
) -> Vec<f64> {
let dt = TAU / (n_steps as f64);
let mut samples: Vec<(f64, Option<f64>)> = Vec::with_capacity(n_steps);
for i in 0..n_steps {
let t = i as f64 * dt;
let val = eval(t);
samples.push((t, val));
}
let mut roots = Vec::new();
for i in 0..n_steps {
let next_i = (i + 1) % n_steps;
let (ta, fa_opt) = samples[i];
let (tb_raw, fb_opt) = samples[next_i];
let tb = if next_i == 0 { TAU } else { tb_raw };
let (Some(fa), Some(fb)) = (fa_opt, fb_opt) else { continue };
// 1. Sign crossing
if fa.signum() != fb.signum() {
let mut lo = ta;
let mut hi = tb;
let mut best_t = 0.5 * (lo + hi);
for _ in 0..24 {
let mid = 0.5 * (lo + hi);
let Some(fm) = eval(mid) else { break };
best_t = mid;
if fm.abs() < 1e-12 {
break;
}
if fm.signum() == fa.signum() {
lo = mid;
} else {
hi = mid;
}
}
if let Some(f_val) = eval(best_t) {
if f_val.abs() < 1e-3 {
roots.push(best_t.rem_euclid(TAU));
}
}
} else {
// 2. Local extremum touching zero (minimum or maximum)
let prev_i = if i == 0 { n_steps - 1 } else { i - 1 };
if let Some(f_prev) = samples[prev_i].1 {
if fa.abs() <= f_prev.abs() && fa.abs() <= fb.abs() && fa.abs() < 0.2 {
let mut a = ta - dt;
let mut b = tb;
let phi = (5.0f64.sqrt() - 1.0) * 0.5;
let mut c = b - phi * (b - a);
let mut d = a + phi * (b - a);
for _ in 0..24 {
let fc = eval(c).map(|v| v.abs()).unwrap_or(f64::MAX);
let fd = eval(d).map(|v| v.abs()).unwrap_or(f64::MAX);
if fc < fd {
b = d;
} else {
a = c;
}
c = b - phi * (b - a);
d = a + phi * (b - a);
}
let best_t = 0.5 * (a + b);
if let Some(f_val) = eval(best_t) {
if f_val.abs() < 1e-3 {
roots.push(best_t.rem_euclid(TAU));
}
}
}
}
}
}
roots
}
fn ttr_ellipse_candidates(
ell: &Ellipse2D,
other: TangentObject,
radius: f64,
) -> Vec<DVec3> {
let mut candidates = Vec::new();
let n_steps = 180;
let other_ell = to_ellipse2d(other);
// s = +1 (outer offset) and s = -1 (inner offset)
for &s in &[1.0, -1.0] {
let center_at = |t: f64| -> DVec2 {
let pt = ell.point_at(t);
let n = ell.normal_at(t);
pt + n * (s * radius)
};
match other {
TangentObject::Line { p1, p2 } => {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-9 {
continue;
}
let n_line = DVec2::new(-dy / len, dx / len);
let p1_2d = DVec2::new(p1.x, p1.y);
for &line_side in &[1.0, -1.0] {
let eval = |t: f64| -> Option<f64> {
let c = center_at(t);
let h = n_line.dot(c - p1_2d);
Some(h - line_side * radius)
};
for t_root in solve_1d_roots(n_steps, eval) {
let c = center_at(t_root);
candidates.push(DVec3::new(c.x, c.y, 0.0));
}
}
}
TangentObject::Circle { center, radius: r2 } => {
let c2_2d = DVec2::new(center.x, center.y);
for &is_int in &[false, true] {
let target_dist = if is_int {
(radius - r2).abs()
} else {
radius + r2
};
if target_dist < 1e-6 {
continue;
}
let eval = |t: f64| -> Option<f64> {
let c = center_at(t);
Some((c - c2_2d).length() - target_dist)
};
for t_root in solve_1d_roots(n_steps, eval) {
let c = center_at(t_root);
candidates.push(DVec3::new(c.x, c.y, 0.0));
}
}
}
TangentObject::Ellipse { .. } => {
if let Some(ell2) = &other_ell {
for &is_enclosing in &[false, true] {
let eval = |t: f64| -> Option<f64> {
let c = center_at(t);
let norm_pts = ell2.normal_points_for_center(c);
let pt2 = if is_enclosing {
norm_pts
.into_iter()
.max_by(|a, b| (*a - c).length_squared().total_cmp(&(*b - c).length_squared()))?
} else {
norm_pts
.into_iter()
.min_by(|a, b| (*a - c).length_squared().total_cmp(&(*b - c).length_squared()))?
};
let d2 = (c - pt2).length();
Some(d2 - radius)
};
for t_root in solve_1d_roots(n_steps, eval) {
let c = center_at(t_root);
candidates.push(DVec3::new(c.x, c.y, 0.0));
}
}
}
}
}
}
let mut unique = Vec::new();
for c in candidates {
if !unique.iter().any(|u: &DVec3| (*u - c).length_squared() < 1e-4) {
unique.push(c);
}
}
unique
}
pub(crate) fn ttr_candidates(first: TangentObject, second: TangentObject, radius: f64) -> Vec<DVec3> {
if radius <= 1e-9 {
return Vec::new();
}
if let Some(ell1) = to_ellipse2d(first) {
return ttr_ellipse_candidates(&ell1, second, radius);
}
if let Some(ell2) = to_ellipse2d(second) {
return ttr_ellipse_candidates(&ell2, first, radius);
}
let first = tangent_curve(first);
let second = tangent_curve(second);
fillets_between(&first, &second, radius, Tolerance::default())
@@ -697,6 +1105,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64)
TangentObject::Circle { .. } => {
circle_idx.push(i);
}
_ => {}
}
}
@@ -829,6 +1238,7 @@ pub struct CircleTTRCommand {
step: StepTTR,
default_r: f64,
plane: WorkingPlane,
last_cursor: Option<DVec3>,
}
enum StepTTR {
@@ -851,10 +1261,11 @@ impl CircleTTRCommand {
step: StepTTR::First,
default_r: defaults::get_circle_radius(),
plane: WorkingPlane::default(),
last_cursor: None,
}
}
fn result_for_radius(&self, radius: f64) -> CmdResult {
fn result_for_radius(&self, radius: f64, cursor: Option<DVec3>) -> CmdResult {
if radius <= 1.0e-9 {
return CmdResult::NeedPoint;
}
@@ -867,9 +1278,14 @@ impl CircleTTRCommand {
else {
return CmdResult::NeedPoint;
};
let hint = (*hit1 + *hit2) * 0.5;
let candidates = ttr_candidates(*obj1, *obj2, radius);
let Some(center) = best_of(&candidates, hint) else {
let Some(center) = pick_best_ttr_candidate(
*obj1,
*obj2,
radius,
*hit1,
*hit2,
cursor,
) else {
return CmdResult::NeedPoint;
};
defaults::set_circle_radius(radius);
@@ -927,8 +1343,10 @@ impl CadCommand for CircleTTRCommand {
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
let cursor_local = self.plane.to_local(pt);
self.last_cursor = Some(cursor_local);
self.radius_from_point(pt)
.map(|radius| self.result_for_radius(radius))
.map(|radius| self.result_for_radius(radius, Some(cursor_local)))
.unwrap_or(CmdResult::NeedPoint)
}
@@ -951,6 +1369,7 @@ impl CadCommand for CircleTTRCommand {
hit1: h1,
hit2: hit,
};
self.last_cursor = None;
CmdResult::NeedPoint
}
StepTTR::Radius { .. } => CmdResult::NeedPoint,
@@ -962,12 +1381,13 @@ impl CadCommand for CircleTTRCommand {
if r <= 0.0 {
return Some(CmdResult::NeedPoint);
}
matches!(self.step, StepTTR::Radius { .. }).then(|| self.result_for_radius(r))
matches!(self.step, StepTTR::Radius { .. })
.then(|| self.result_for_radius(r, self.last_cursor))
}
fn on_enter(&mut self) -> CmdResult {
if matches!(self.step, StepTTR::Radius { .. }) {
self.result_for_radius(self.default_r)
self.result_for_radius(self.default_r, self.last_cursor)
} else {
CmdResult::Cancel
}
@@ -986,7 +1406,16 @@ impl CadCommand for CircleTTRCommand {
else {
return None;
};
let center = best_of(&ttr_candidates(*obj1, *obj2, radius), (*hit1 + *hit2) * 0.5)?;
let cursor_local = self.plane.to_local(pt);
self.last_cursor = Some(cursor_local);
let center = pick_best_ttr_candidate(
*obj1,
*obj2,
radius,
*hit1,
*hit2,
Some(cursor_local),
)?;
Some(circle_wire(
self.plane.to_world(center),
radius,
@@ -1180,4 +1609,156 @@ mod tests {
assert!(ok, "no annulus candidate, got {cands:?}");
}
}
#[test]
fn ttr_ellipse_and_circle_test() {
let e1 = TangentObject::Ellipse {
center: DVec3::new(-30.0, 0.0, 0.0),
major_axis: DVec3::new(20.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.5,
};
let c2 = TangentObject::Circle {
center: DVec3::new(30.0, 0.0, 0.0),
radius: 10.0,
};
let cands = ttr_candidates(e1, c2, 20.0);
assert!(!cands.is_empty(), "ttr_candidates with ellipse + circle should find solutions");
for c in &cands {
let dist_to_circle_center = (c.truncate() - DVec2::new(30.0, 0.0)).length();
assert!(
(dist_to_circle_center - (20.0 + 10.0)).abs() < 1e-3
|| (dist_to_circle_center - 10.0).abs() < 1e-3,
"candidate center {c:?} must be tangent to circle"
);
}
// Test reverse order: circle first, ellipse second
let cands_rev = ttr_candidates(c2, e1, 20.0);
assert_eq!(cands.len(), cands_rev.len());
}
#[test]
fn ttr_ellipse_and_line_test() {
let e = TangentObject::Ellipse {
center: DVec3::ZERO,
major_axis: DVec3::new(20.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.5, // b = 10.0, bottom vertex is (0, -10, 0)
};
let line = TangentObject::Line {
p1: DVec3::new(-50.0, -20.0, 0.0),
p2: DVec3::new(50.0, -20.0, 0.0),
};
// Gap at bottom vertex (0, -10) to y = -20 is 10. Radius = 5 fits with center (0, -15, 0).
let cands = ttr_candidates(e, line, 5.0);
assert!(!cands.is_empty(), "ttr_candidates with ellipse + line should find solutions");
assert!(
cands.iter().any(|c| (c.x.abs() < 1e-3) && ((c.y - (-15.0)).abs() < 1e-3)),
"should contain bottom vertex solution (0, -15, 0), got: {cands:?}"
);
// Reverse order
let cands_rev = ttr_candidates(line, e, 5.0);
assert!(!cands_rev.is_empty());
}
#[test]
fn ttr_ellipse_and_ellipse_test() {
let e1 = TangentObject::Ellipse {
center: DVec3::new(-25.0, 0.0, 0.0),
major_axis: DVec3::new(15.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.6,
};
let e2 = TangentObject::Ellipse {
center: DVec3::new(25.0, 0.0, 0.0),
major_axis: DVec3::new(15.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.6,
};
// Gap between right vertex of e1 (-10, 0) and left vertex of e2 (10, 0) is 20.
// A circle with radius 10 fits at (0, 0, 0).
let cands_10 = ttr_candidates(e1, e2, 10.0);
assert!(!cands_10.is_empty(), "ttr_candidates with ellipse + ellipse should find solutions for R=10");
assert!(
cands_10.iter().any(|c| c.length() < 1e-2),
"should contain center solution at approx (0,0,0), got: {cands_10:?}"
);
// Radius 15 should yield symmetric solutions above and below x-axis
let cands_15 = ttr_candidates(e1, e2, 15.0);
assert!(!cands_15.is_empty(), "ttr_candidates with ellipse + ellipse should find solutions for R=15");
assert!(cands_15.iter().any(|c| c.y > 1.0));
assert!(cands_15.iter().any(|c| c.y < -1.0));
}
#[test]
fn ttr_large_radius_stays_on_cursor_side_acute_angle() {
let l1 = TangentObject::Line {
p1: DVec3::ZERO,
p2: DVec3::new(100.0, 0.0, 0.0),
};
let ang = 30.0f64.to_radians();
let l2 = TangentObject::Line {
p1: DVec3::ZERO,
p2: DVec3::new(100.0 * ang.cos(), 100.0 * ang.sin(), 0.0),
};
let hit1 = DVec3::new(20.0, 0.0, 0.0);
let hit2 = DVec3::new(20.0 * ang.cos(), 20.0 * ang.sin(), 0.0);
// Test across a wide range of radii from small to very large
for &r in &[5.0, 10.0, 30.0, 50.0, 100.0, 500.0, 2000.0] {
// Cursor in the acute 30-degree wedge (x > 0, y > 0)
let cursor = DVec3::new(r * 0.8, r * 0.2, 0.0);
let center = pick_best_ttr_candidate(l1, l2, r, hit1, hit2, Some(cursor))
.expect("should find candidate");
assert!(
center.x > 0.0 && center.y > 0.0,
"center should stay in acute wedge (x > 0, y > 0) for r={r}, but got {center:?}"
);
// Also test when cursor is None (using pick points): should also stay in acute wedge
let center_no_cur = pick_best_ttr_candidate(l1, l2, r, hit1, hit2, None)
.expect("should find candidate without cursor");
assert!(
center_no_cur.x > 0.0 && center_no_cur.y > 0.0,
"center without cursor should stay in acute wedge for r={r}, but got {center_no_cur:?}"
);
// If cursor intentionally moves into obtuse 150-degree wedge (x < 0, y > 0)
let cursor_obtuse = DVec3::new(-r * 0.5, r * 0.5, 0.0);
let center_obtuse = pick_best_ttr_candidate(l1, l2, r, hit1, hit2, Some(cursor_obtuse))
.expect("should find candidate");
assert!(
center_obtuse.x < 0.0 && center_obtuse.y > 0.0,
"center should follow cursor into obtuse wedge (x < 0, y > 0) for r={r}, got {center_obtuse:?}"
);
}
}
#[test]
fn ttr_two_circles_large_radius_stays_on_cursor_side() {
let c1 = TangentObject::Circle {
center: DVec3::new(-20.0, 0.0, 0.0),
radius: 10.0,
};
let c2 = TangentObject::Circle {
center: DVec3::new(20.0, 0.0, 0.0),
radius: 10.0,
};
let hit1 = DVec3::new(-20.0, 10.0, 0.0);
let hit2 = DVec3::new(20.0, 10.0, 0.0);
for &r in &[25.0, 35.0, 50.0, 100.0, 500.0, 2000.0] {
// Cursor above both circles
let cursor = DVec3::new(0.0, r + 10.0, 0.0);
let center = pick_best_ttr_candidate(c1, c2, r, hit1, hit2, Some(cursor))
.expect("should find candidate");
assert!(
center.y > 10.0,
"circle should stay externally tangent above circles for r={r}, but got {center:?}"
);
}
}
}
+3 -3
View File
@@ -1,8 +1,8 @@
// Legacy centre cross and associative centre mark commands.
use acadrust::types::Vector3;
use acadrust::entities::{CenterMarkAssociation, CenterMarkSource};
use acadrust::{EntityType, Handle, Line};
use codec::types::Vector3;
use codec::entities::{CenterMarkAssociation, CenterMarkSource};
use codec::{EntityType, Handle, Line};
use glam::DVec3;
use crate::t;
+4 -4
View File
@@ -1,8 +1,8 @@
// DONUT creates a ring as a closed, constant-width LwPolyline.
use acadrust::entities::{LwPolyline, LwVertex};
use acadrust::EntityType;
use cadkernel::geom2d::{
use codec::entities::{LwPolyline, LwVertex};
use codec::EntityType;
use kernel::geom2d::{
Circle as KernelCircle, Curve as KernelCurve, Vec2 as KernelVec2,
};
use glam::DVec3;
@@ -254,7 +254,7 @@ fn donut_wire(
}
pub(crate) fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType {
use acadrust::types::Vector2;
use codec::types::Vector2;
let r_avg = (inner_r + outer_r) / 2.0;
let width = outer_r - inner_r;
+2 -2
View File
@@ -5,8 +5,8 @@
// ELLIPSE_AXIS — Axis, End (axis endpoint 1 → endpoint 2 → minor distance)
// ELLIPSE_ARC — Ellipse Arc (shape as above, then start/end parametric angles)
use acadrust::types::Vector3;
use acadrust::{Ellipse, EntityType};
use codec::types::Vector3;
use codec::{Ellipse, EntityType};
use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
+5 -5
View File
@@ -1,8 +1,8 @@
use acadrust::entities::{face3d::InvisibleEdgeFlags, Face3D};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use cadkernel::geom2d::Tolerance;
use cadkernel::space::{curve::segments_overlap_collinearly, Vec3 as KernelVec3};
use codec::entities::{face3d::InvisibleEdgeFlags, Face3D};
use codec::types::Vector3;
use codec::{EntityType, Handle};
use kernel::geom2d::Tolerance;
use kernel::space::{curve::segments_overlap_collinearly, Vec3 as KernelVec3};
use glam::DVec3;
use std::collections::{HashMap, HashSet};
+26 -26
View File
@@ -4,8 +4,8 @@ use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::IconKind;
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
use crate::scene::model::wire_model::WireModel;
use acadrust::Handle;
use cadkernel::geom2d::{
use codec::Handle;
use kernel::geom2d::{
bounded_faces, contains, ring_nesting_depths, signed_area, Circle, Curve, Line, Tolerance,
};
use glam::DVec3;
@@ -199,11 +199,11 @@ pub struct HatchCommand {
default_origin: [f64; 2],
associative: bool,
separate_hatches: bool,
island_style: acadrust::entities::HatchStyleType,
island_style: codec::entities::HatchStyleType,
inherited: Option<(
HatchModel,
acadrust::types::Color,
acadrust::types::Transparency,
codec::types::Color,
codec::types::Transparency,
)>,
plane: WorkingPlane,
}
@@ -215,8 +215,8 @@ impl HatchCommand {
selected_objects: Vec<Handle>,
inherited: Option<(
HatchModel,
acadrust::types::Color,
acadrust::types::Transparency,
codec::types::Color,
codec::types::Transparency,
)>,
plane: WorkingPlane,
) -> Self {
@@ -251,7 +251,7 @@ impl HatchCommand {
island_style: inherited
.as_ref()
.map(|(model, _, _)| model.style)
.unwrap_or(acadrust::entities::HatchStyleType::Normal),
.unwrap_or(codec::entities::HatchStyleType::Normal),
inherited,
plane,
};
@@ -300,9 +300,9 @@ impl HatchCommand {
fn island_style_label(&self) -> &'static str {
match self.island_style {
acadrust::entities::HatchStyleType::Normal => "Normal",
acadrust::entities::HatchStyleType::Outer => "Outer",
acadrust::entities::HatchStyleType::Ignore => "Ignore",
codec::entities::HatchStyleType::Normal => "Normal",
codec::entities::HatchStyleType::Outer => "Outer",
codec::entities::HatchStyleType::Ignore => "Ignore",
}
}
@@ -346,7 +346,7 @@ impl HatchCommand {
x_axis: self.plane.x.to_array(),
y_axis: self.plane.y.to_array(),
};
let exterior: Vec<bool> = cadkernel::geom2d::ring_nesting_depths(&rings)
let exterior: Vec<bool> = kernel::geom2d::ring_nesting_depths(&rings)
.into_iter()
.map(|depth| depth == 0)
.collect();
@@ -475,9 +475,9 @@ impl HatchCommand {
}
}
fn manual_boundary_path(&self) -> Option<acadrust::entities::BoundaryPath> {
use acadrust::entities::{BoundaryEdge, BoundaryPath, PolylineEdge};
use acadrust::types::Vector3;
fn manual_boundary_path(&self) -> Option<codec::entities::BoundaryPath> {
use codec::entities::{BoundaryEdge, BoundaryPath, PolylineEdge};
use codec::types::Vector3;
if self.manual_pts.len() < 3 {
return None;
}
@@ -503,7 +503,7 @@ impl HatchCommand {
}
fn arc_bulge(start: DVec3, middle: DVec3, end: DVec3) -> Option<f64> {
let curvature = DVec3::from_array(cadkernel::space::curve::curvature_through(
let curvature = DVec3::from_array(kernel::space::curve::curvature_through(
start.to_array(),
middle.to_array(),
end.to_array(),
@@ -892,14 +892,14 @@ impl CadCommand for HatchCommand {
}
"Y" | "ISLAND" => {
self.island_style = match self.island_style {
acadrust::entities::HatchStyleType::Normal => {
acadrust::entities::HatchStyleType::Outer
codec::entities::HatchStyleType::Normal => {
codec::entities::HatchStyleType::Outer
}
acadrust::entities::HatchStyleType::Outer => {
acadrust::entities::HatchStyleType::Ignore
codec::entities::HatchStyleType::Outer => {
codec::entities::HatchStyleType::Ignore
}
acadrust::entities::HatchStyleType::Ignore => {
acadrust::entities::HatchStyleType::Normal
codec::entities::HatchStyleType::Ignore => {
codec::entities::HatchStyleType::Normal
}
};
Some(CmdResult::NeedPoint)
@@ -963,7 +963,7 @@ impl GradientCommand {
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
let (rel, origin, wcs) = pack_rings(&rings);
let exterior: Vec<bool> = cadkernel::geom2d::ring_nesting_depths(&rings)
let exterior: Vec<bool> = kernel::geom2d::ring_nesting_depths(&rings)
.into_iter()
.map(|depth| depth == 0)
.collect();
@@ -1001,7 +1001,7 @@ impl GradientCommand {
boundary_exterior: Some(std::sync::Arc::new(exterior)),
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
boundary_paths: Some(std::sync::Arc::new(boundary_paths)),
style: acadrust::entities::HatchStyleType::Normal,
style: codec::entities::HatchStyleType::Normal,
draw_depth: 0.0,
}
}
@@ -1304,7 +1304,7 @@ impl BoundaryCommand {
}
}
fn make_entities(&self) -> Vec<acadrust::EntityType> {
fn make_entities(&self) -> Vec<codec::EntityType> {
let sources = if self.restrict_sources {
&self.active_sources
} else {
@@ -1584,7 +1584,7 @@ inventory::submit!(crate::command::CommandRegistration { names: &["HATCH"] });
#[cfg(test)]
mod tests {
use super::*;
use acadrust::EntityType;
use codec::EntityType;
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<[f64; 2]> {
vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
+39 -39
View File
@@ -8,7 +8,7 @@
// A <degrees> — change angle
// Press Enter to apply changes.
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
@@ -30,20 +30,20 @@ pub struct HatcheditCommand {
disassociate: bool,
store_origin: bool,
current_origin: [f64; 2],
origin_plane: cadkernel::space::Plane,
origin_plane: kernel::space::Plane,
origin_bounds: Option<([f64;2],[f64;2])>,
origin_bounds_unavailable: bool,
style: Option<acadrust::entities::HatchStyleType>,
style: Option<codec::entities::HatchStyleType>,
annotative: Option<bool>,
annotative_current: bool,
style_current: acadrust::entities::HatchStyleType,
style_current: codec::entities::HatchStyleType,
input: Option<&'static str>,
source_appearance: Option<(acadrust::types::Color,String,acadrust::types::Transparency)>,
current_color: acadrust::types::Color,
current_transparency: acadrust::types::Transparency,
source_appearance: Option<(codec::types::Color,String,codec::types::Transparency)>,
current_color: codec::types::Color,
current_transparency: codec::types::Transparency,
boundary_region: bool,
association_sources: Option<(crate::command::WorkingPlane,rustc_hash::FxHashMap<Handle,crate::scene::BoundarySource>)>,
association_paths: Vec<acadrust::entities::BoundaryPath>,
association_paths: Vec<codec::entities::BoundaryPath>,
association_missed: bool,
}
@@ -57,15 +57,15 @@ impl HatcheditCommand {
origin_bounds: None,
origin_bounds_unavailable: false,
current_origin: [0.0, 0.0],
origin_plane: cadkernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]),
origin_plane: kernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]),
style: None,
annotative: None,
annotative_current: false,
style_current: acadrust::entities::HatchStyleType::Normal,
style_current: codec::entities::HatchStyleType::Normal,
input: None,
source_appearance: None,
current_color: acadrust::types::Color::ByLayer,
current_transparency: acadrust::types::Transparency::ByLayer,
current_color: codec::types::Color::ByLayer,
current_transparency: codec::types::Transparency::ByLayer,
boundary_region: false,
association_sources: None,
association_paths: Vec::new(),
@@ -93,15 +93,15 @@ impl HatcheditCommand {
origin_bounds: None,
origin_bounds_unavailable: false,
current_origin: [0.0, 0.0],
origin_plane: cadkernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]),
origin_plane: kernel::space::Plane::from_axes([0.0;3],[1.0,0.0,0.0],[0.0,1.0,0.0]),
style: None,
annotative: None,
annotative_current: annotative,
style_current: acadrust::entities::HatchStyleType::Normal,
style_current: codec::entities::HatchStyleType::Normal,
input: None,
source_appearance: None,
current_color: acadrust::types::Color::ByLayer,
current_transparency: acadrust::types::Transparency::ByLayer,
current_color: codec::types::Color::ByLayer,
current_transparency: codec::types::Transparency::ByLayer,
boundary_region: false,
association_sources: None,
association_paths: Vec::new(),
@@ -133,15 +133,15 @@ impl HatcheditCommand {
command.input=Some("associate-point");command.association_sources=Some((plane,sources));command
}
fn add_association_rings(&mut self,rings:Vec<Vec<[f64;2]>>,sources:&rustc_hash::FxHashMap<Handle,crate::scene::BoundarySource>) {
let exterior=cadkernel::geom2d::ring_nesting_depths(&rings).into_iter().map(|depth|depth==0).collect::<Vec<_>>();
let exterior=kernel::geom2d::ring_nesting_depths(&rings).into_iter().map(|depth|depth==0).collect::<Vec<_>>();
let paths=crate::scene::exact_hatch_paths(&rings,&exterior,sources,1e-6);
self.association_missed=paths.is_empty()||paths.len()!=rings.len()||paths.iter().any(|path|path.boundary_handles.is_empty());
if !self.association_missed {
for path in paths {if !self.association_paths.contains(&path){self.association_paths.push(path);}}
}
}
pub fn with_appearance(mut self,entity:Option<&acadrust::EntityType>,current_color:acadrust::types::Color,current_transparency:acadrust::types::Transparency)->Self {
if let Some(acadrust::EntityType::Hatch(hatch)) = entity {
pub fn with_appearance(mut self,entity:Option<&codec::EntityType>,current_color:codec::types::Color,current_transparency:codec::types::Transparency)->Self {
if let Some(codec::EntityType::Hatch(hatch)) = entity {
self.style_current = hatch.style;
self.origin_plane = crate::entities::curve::ocs_plane(hatch.normal, hatch.elevation);
self.origin_bounds = hatch.paths.iter().flat_map(|path| &path.edges)
@@ -153,20 +153,20 @@ impl HatcheditCommand {
let mut anchors = Vec::new();
for curve in curves {
match curve {
cadkernel::geom2d::Curve::Ellipse(arc) => {
kernel::geom2d::Curve::Ellipse(arc) => {
if (arc.end_parameter - arc.start_parameter).abs() < std::f64::consts::TAU { return None; }
for angle in [0.0, std::f64::consts::FRAC_PI_2] {
anchors.push(cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line {
anchors.push(kernel::geom2d::Curve::Line(kernel::geom2d::Line {
start: arc.ellipse.point_at(angle),
end: arc.ellipse.point_at(angle + std::f64::consts::PI),
}));
}
}
cadkernel::geom2d::Curve::Nurbs(_) => return None,
kernel::geom2d::Curve::Nurbs(_) => return None,
other => anchors.push(other),
}
}
cadkernel::geom2d::analytic_curve_bounds(&anchors)
kernel::geom2d::analytic_curve_bounds(&anchors)
});
}
self.source_appearance=entity.map(|e|{let c=e.common();(c.color,c.layer.clone(),c.transparency)});
@@ -195,9 +195,9 @@ impl CadCommand for HatcheditCommand {
if let Some(input)=self.input {
if input == "style" {
let current = match self.style_current {
acadrust::entities::HatchStyleType::Normal => "Normal",
acadrust::entities::HatchStyleType::Outer => "Outer",
acadrust::entities::HatchStyleType::Ignore => "Ignore",
codec::entities::HatchStyleType::Normal => "Normal",
codec::entities::HatchStyleType::Outer => "Outer",
codec::entities::HatchStyleType::Ignore => "Ignore",
};
return format!("Enter hatching style [Ignore/Outer/Normal] <{current}>:");
}
@@ -206,8 +206,8 @@ impl CadCommand for HatcheditCommand {
"color"=>return format!("New color [Truecolor/. (for use current)] <{color:?}>:"),
"layer"=>return format!("Specify layer or [. (for use current)] <{layer}>:"),
"transparency"=>return format!("Specify transparency (0-90) or ByLayer/ByBlock <{}>:",match transparency {
acadrust::types::Transparency::ByLayer=>"ByLayer".into(),
acadrust::types::Transparency::ByBlock=>"ByBlock".into(),
codec::types::Transparency::ByLayer=>"ByLayer".into(),
codec::types::Transparency::ByBlock=>"ByBlock".into(),
value=>format!("{:.0}",value.as_percent()*100.0),
}),
_=>{},
@@ -334,7 +334,7 @@ impl CadCommand for HatcheditCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword=text.trim().to_ascii_uppercase();
if let Some(input)=self.input {
use acadrust::types::{Color,Transparency};
use codec::types::{Color,Transparency};
let appearance=|color,layer,transparency|HatchEditOperation::Appearance{color,layer,transparency};
match input {
"origin" => match keyword.as_str() {
@@ -372,9 +372,9 @@ impl CadCommand for HatcheditCommand {
}
"style" => {
self.style = match keyword.as_str() {
"I" | "IGNORE" => Some(acadrust::entities::HatchStyleType::Ignore),
"O" | "OUTER" => Some(acadrust::entities::HatchStyleType::Outer),
"N" | "NORMAL" => Some(acadrust::entities::HatchStyleType::Normal),
"I" | "IGNORE" => Some(codec::entities::HatchStyleType::Ignore),
"O" | "OUTER" => Some(codec::entities::HatchStyleType::Outer),
"N" | "NORMAL" => Some(codec::entities::HatchStyleType::Normal),
_ => return Some(CmdResult::NeedPoint),
};
return self.apply_result(self.update_operation());
@@ -509,9 +509,9 @@ impl CadCommand for HatcheditCommand {
}
if let Some(rest) = text.strip_prefix('Y') {
self.style = match rest.trim() {
"NORMAL" | "N" => Some(acadrust::entities::HatchStyleType::Normal),
"OUTER" | "O" => Some(acadrust::entities::HatchStyleType::Outer),
"IGNORE" | "I" => Some(acadrust::entities::HatchStyleType::Ignore),
"NORMAL" | "N" => Some(codec::entities::HatchStyleType::Normal),
"OUTER" | "O" => Some(codec::entities::HatchStyleType::Outer),
"IGNORE" | "I" => Some(codec::entities::HatchStyleType::Ignore),
_ => self.style,
};
return Some(CmdResult::NeedPoint);
@@ -613,12 +613,12 @@ impl CadCommand for HatcheditCommand {
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::hatch::{
use codec::entities::hatch::{
BoundaryEdge, BoundaryPath, EllipticArcEdge, LineEdge,
};
use acadrust::entities::{Hatch, HatchStyleType};
use acadrust::types::{Color, Transparency, Vector2};
use acadrust::EntityType;
use codec::entities::{Hatch, HatchStyleType};
use codec::types::{Color, Transparency, Vector2};
use codec::EntityType;
fn hatch_command() -> HatcheditCommand {
let mut hatch = Hatch::new();
+6 -6
View File
@@ -1,7 +1,7 @@
use acadrust::entities::{Helix, HelixConstraint, Spline};
use acadrust::types::Vector3;
use acadrust::EntityType;
use cadkernel::space::{HelixCurve, HelixDirection, Vec3};
use codec::entities::{Helix, HelixConstraint, Spline};
use codec::types::Vector3;
use codec::EntityType;
use kernel::space::{HelixCurve, HelixDirection, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, DynField, WorkingPlane};
@@ -139,7 +139,7 @@ impl HelixCommand {
helix.turn_height = if curve.turns == 0.0 { 0.0 } else { height / curve.turns };
helix.handedness = self.counter_clockwise;
helix.constraint = self.constraint;
Some(EntityType::Helix(helix))
Some(EntityType::Helix(Box::new(helix)))
}
fn commit(&mut self, height: f64, axis: DVec3) -> CmdResult {
@@ -159,7 +159,7 @@ impl HelixCommand {
let points = self
.kernel_curve(height, axis)?
.nurbs()?
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE);
Some(WireModel::solid_f64(
"helix_preview".to_string(),
points,
+413 -93
View File
@@ -2,18 +2,18 @@
//
// Command: LINE — OpenCADStudio behaviour:
// 1. First click → stores start point, prompts for next point
// 2. Each further click → immediately commits an acadrust::Line entity
// 2. Each further click → immediately commits an codec::Line entity
// (start→end) to the document; end becomes the new start point
// 3. Enter / Escape → ends the command
use acadrust::types::Vector3;
use acadrust::{EntityType, Line};
use codec::types::Vector3;
use codec::{EntityType, Line};
use crate::t;
use crate::command::{CadCommand, CmdResult, TangentObject};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use glam::DVec3;
use glam::{DVec2, DVec3};
// ── Ribbon definition ─────────────────────────────────────────────────────
@@ -102,46 +102,41 @@ impl CadCommand for LineCommand {
pt: DVec3,
tangent: Option<TangentObject>,
) -> Option<CmdResult> {
use TangentObject::Circle;
match (self.points.last().copied(), self.deferred_tangent, tangent) {
// (A) The FIRST point is tangent to a circle: defer it. The tangent
// point depends on the line's direction, which the next pick gives.
// (A) The FIRST point is tangent to a circle or ellipse: defer it.
// The tangent point depends on the line's direction, which the next pick gives.
// Store a provisional point so the rubber band has an origin.
(None, _, Some(Circle { center, radius })) => {
(None, _, Some(target @ (TangentObject::Circle { .. } | TangentObject::Ellipse { .. }))) => {
self.points.push(pt);
self.deferred_tangent = Some((Circle { center, radius }, pt));
self.deferred_tangent = Some((target, pt));
Some(CmdResult::NeedPoint)
}
// (B) A deferred first tangent AND this point is also tangent to a
// circle: the line is the common tangent of the two circles. Pick
// the one of the (up to four) whose endpoints sit nearest the two
// cursor hits, so the user chooses inner/outer + side by where they
// hover.
// (B) A deferred first tangent AND this point is also tangent to a curve (circle or ellipse):
// The line is the common tangent of the two curves.
(
Some(_),
Some((Circle { center: c1, radius: r1 }, hit1)),
Some(Circle { center: c2, radius: r2 }),
Some((target1, hit1)),
Some(target2 @ (TangentObject::Circle { .. } | TangentObject::Ellipse { .. })),
) => {
self.deferred_tangent = None;
let (t1, t2) = circle_circle_tangents(c1, r1, c2, r2)
let (t1, t2) = common_curve_tangents(target1, target2)
.into_iter()
.min_by(|a, b| {
let da = (a.0 - hit1).length() + (a.1 - pt).length();
let db = (b.0 - hit1).length() + (b.1 - pt).length();
let da = (a.0 - hit1).length_squared() + (a.1 - pt).length_squared();
let db = (b.0 - hit1).length_squared() + (b.1 - pt).length_squared();
da.total_cmp(&db)
})
.unwrap_or((self.points[0], pt));
self.points = vec![t1, t2];
Some(CmdResult::CommitEntity(Self::line_between(t1, t2)))
}
// (C) A deferred first tangent, but this point is NOT a circle
// tangent: the line runs from this point, tangent to the deferred
// circle (the nearer of the two tangent points to the first hit).
(Some(_), Some((Circle { center: c1, radius: r1 }, hit1)), _) => {
// (C) A deferred first tangent, but this point is NOT a curve tangent pick:
// The line runs from the tangent point on the deferred curve to this point.
(Some(_), Some((target1, hit1)), _) => {
self.deferred_tangent = None;
let start = point_circle_tangents(pt, c1, r1)
let start = point_target_tangents(pt, target1)
.map(|(t0, t1)| {
if (t0 - hit1).length() <= (t1 - hit1).length() {
if (t0 - hit1).length_squared() <= (t1 - hit1).length_squared() {
t0
} else {
t1
@@ -151,9 +146,23 @@ impl CadCommand for LineCommand {
self.points = vec![start, pt];
Some(CmdResult::CommitEntity(Self::line_between(start, pt)))
}
// No deferred tangent (or a Line tangent, which needs no special
// handling — a line is tangent to itself everywhere): fall back to
// the plain point path.
// (D) First point was already placed (ordinary point or previous segment end),
// and this pick IS tangent to a circle or ellipse:
(Some(last), None, Some(target @ (TangentObject::Circle { .. } | TangentObject::Ellipse { .. }))) => {
let end = point_target_tangents(last, target)
.map(|(t0, t1)| {
if (t0 - pt).length_squared() <= (t1 - pt).length_squared() {
t0
} else {
t1
}
})
.unwrap_or(pt);
let line = Self::line_between(last, end);
self.points.push(end);
Some(CmdResult::CommitEntity(line))
}
// No deferred tangent (or a Line tangent): fall back to the plain point path.
_ => None,
}
}
@@ -171,6 +180,7 @@ impl CadCommand for LineCommand {
}
fn on_escape(&mut self) -> CmdResult {
self.deferred_tangent = None;
CmdResult::Cancel
}
@@ -212,6 +222,7 @@ impl CadCommand for LineCommand {
// Only the start is placed (nothing committed yet) — clear
// it so the next pick restarts the line.
self.points.clear();
self.deferred_tangent = None;
Some(CmdResult::NeedPoint)
} else {
Some(CmdResult::NeedPoint)
@@ -232,90 +243,262 @@ impl CadCommand for LineCommand {
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let last = *self.points.last()?;
// With a deferred first tangent, slide the rubber band's start point
// around the circle so it stays tangent toward the moving cursor,
// instead of sticking at the provisional pick point. Keep the side the
// user first picked (nearest that hit) for a stable preview. (#274)
let start = match self.deferred_tangent {
Some((TangentObject::Circle { center, radius }, hit)) => {
point_circle_tangents(pt, center, radius)
self.on_preview_wires_with_tangent(pt, None).into_iter().next()
}
fn on_preview_wires_with_tangent(
&mut self,
pt: DVec3,
tangent: Option<TangentObject>,
) -> Vec<WireModel> {
let last = match self.points.last() {
Some(&p) => p,
None => return Vec::new(),
};
let (start, end) = match (self.deferred_tangent, tangent) {
(Some((target1, hit1)), Some(target2 @ (TangentObject::Circle { .. } | TangentObject::Ellipse { .. }))) => {
common_curve_tangents(target1, target2)
.into_iter()
.min_by(|a, b| {
let da = (a.0 - hit1).length_squared() + (a.1 - pt).length_squared();
let db = (b.0 - hit1).length_squared() + (b.1 - pt).length_squared();
da.total_cmp(&db)
})
.unwrap_or((last, pt))
}
(Some((target1, hit1)), _) => {
let start = point_target_tangents(pt, target1)
.map(|(t0, t1)| {
if (t0 - hit).length() <= (t1 - hit).length() {
if (t0 - hit1).length_squared() <= (t1 - hit1).length_squared() {
t0
} else {
t1
}
})
.unwrap_or(last)
.unwrap_or(last);
(start, pt)
}
_ => last,
(None, Some(target @ (TangentObject::Circle { .. } | TangentObject::Ellipse { .. }))) => {
let end = point_target_tangents(last, target)
.map(|(t0, t1)| {
if (t0 - pt).length_squared() <= (t1 - pt).length_squared() {
t0
} else {
t1
}
})
.unwrap_or(pt);
(last, end)
}
_ => (last, pt),
};
Some(WireModel::solid_f64(
vec![WireModel::solid_f64(
"rubber_band".to_string(),
vec![[start.x, start.y, start.z], [pt.x, pt.y, pt.z]],
vec![[start.x, start.y, start.z], [end.x, end.y, end.z]],
WireModel::CYAN,
false,
))
)]
}
}
// ── Tangent geometry (f64) ────────────────────────────────────────────────
/// The two points on a circle (`center`, `radius`, XY plane) where a line from
/// external point `p` touches tangentially, or `None` when `p` is inside/on the
/// circle. Each tangent point's radius is perpendicular to the line from `p`,
/// so it sits at the half-angle `acos(radius / |center→p|)` either side of the
/// center→p direction. (#274)
fn point_circle_tangents(p: DVec3, center: DVec3, radius: f64) -> Option<(DVec3, DVec3)> {
let vx = p.x - center.x;
let vy = p.y - center.y;
let d = (vx * vx + vy * vy).sqrt();
if d <= radius + 1e-9 {
return None;
}
let base = vy.atan2(vx);
let off = (radius / d).acos();
let at =
|a: f64| DVec3::new(center.x + radius * a.cos(), center.y + radius * a.sin(), center.z);
Some((at(base + off), at(base - off)))
#[derive(Clone, Copy, Debug)]
struct Curve3D {
center: DVec3,
axis_a: DVec3,
axis_b: DVec3,
}
/// Up to four common tangent lines of two circles (XY plane), each returned as
/// (tangent point on circle 1, tangent point on circle 2): two external (both
/// radii point the same way) and two internal (opposite ways, present only when
/// the circles are separated). For a unit normal `n` to the tangent line,
/// `n·(c2−c1)` equals `r2−r1` (external) or `−(r1+r2)` (internal); each tangent
/// point is its centre offset by `∓r·n`. (#274)
impl Curve3D {
fn from_tangent_object(obj: TangentObject) -> Option<Self> {
match obj {
TangentObject::Circle { center, radius } => {
if radius < 1e-9 {
return None;
}
Some(Self {
center,
axis_a: DVec3::new(radius, 0.0, 0.0),
axis_b: DVec3::new(0.0, radius, 0.0),
})
}
TangentObject::Ellipse {
center,
major_axis,
normal,
minor_axis_ratio,
} => {
let a = major_axis.length();
if a < 1e-9 {
return None;
}
let mut n = normal.normalize_or_zero();
if n.length_squared() < 0.5 {
n = DVec3::Z;
}
let u = major_axis / a;
let v = n.cross(u);
let b = a * minor_axis_ratio;
Some(Self {
center,
axis_a: major_axis,
axis_b: v * b,
})
}
TangentObject::Line { .. } => None,
}
}
}
/// The two points on a curve (Circle or Ellipse) where a line from external point `p`
/// touches tangentially, or `None` when `p` is inside or on the curve.
fn point_target_tangents(p: DVec3, target: TangentObject) -> Option<(DVec3, DVec3)> {
let curve = Curve3D::from_tangent_object(target)?;
let d = p - curve.center;
let a = curve.axis_a.length();
let b = curve.axis_b.length();
if a < 1e-9 || b < 1e-9 {
return None;
}
let u_dir = curve.axis_a / a;
let v_dir = curve.axis_b / b;
let u0 = d.dot(u_dir);
let v0 = d.dot(v_dir);
let u_norm = u0 / a;
let v_norm = v0 / b;
let dist = (u_norm * u_norm + v_norm * v_norm).sqrt();
if dist <= 1.0 + 1e-9 {
return None;
}
let theta0 = v_norm.atan2(u_norm);
let delta = (1.0 / dist).acos();
let t0 = theta0 + delta;
let t1 = theta0 - delta;
let pt0 = curve.center + curve.axis_a * t0.cos() + curve.axis_b * t0.sin();
let pt1 = curve.center + curve.axis_a * t1.cos() + curve.axis_b * t1.sin();
Some((pt0, pt1))
}
#[cfg(test)]
fn point_circle_tangents(p: DVec3, center: DVec3, radius: f64) -> Option<(DVec3, DVec3)> {
point_target_tangents(p, TangentObject::Circle { center, radius })
}
/// Common tangent lines between any two curves (Circle-Circle, Circle-Ellipse, Ellipse-Ellipse).
/// Each tangent is returned as (tangent point on curve 1, tangent point on curve 2).
fn common_curve_tangents(target1: TangentObject, target2: TangentObject) -> Vec<(DVec3, DVec3)> {
let c1 = match Curve3D::from_tangent_object(target1) {
Some(c) => c,
None => return Vec::new(),
};
let c2 = match Curve3D::from_tangent_object(target2) {
Some(c) => c,
None => return Vec::new(),
};
let c1_2d = DVec2::new(c1.center.x, c1.center.y);
let a1_2d = DVec2::new(c1.axis_a.x, c1.axis_a.y);
let b1_2d = DVec2::new(c1.axis_b.x, c1.axis_b.y);
let c2_2d = DVec2::new(c2.center.x, c2.center.y);
let a2_2d = DVec2::new(c2.axis_a.x, c2.axis_a.y);
let b2_2d = DVec2::new(c2.axis_b.x, c2.axis_b.y);
let delta_c = c2_2d - c1_2d;
if delta_c.length_squared() < 1e-9 {
return Vec::new();
}
let eval = |theta: f64, is_int: bool| -> (f64, DVec2, DVec2) {
let n = DVec2::new(theta.cos(), theta.sin());
let su1 = n.dot(a1_2d);
let sv1 = n.dot(b1_2d);
let r1 = (su1 * su1 + sv1 * sv1).sqrt().max(1e-9);
let su2 = n.dot(a2_2d);
let sv2 = n.dot(b2_2d);
let r2 = (su2 * su2 + sv2 * sv2).sqrt().max(1e-9);
let f = if is_int {
n.dot(delta_c) - r2 - r1
} else {
n.dot(delta_c) + r2 - r1
};
let t1 = c1_2d + (a1_2d * su1 + b1_2d * sv1) / r1;
let t2 = if is_int {
c2_2d - (a2_2d * su2 + b2_2d * sv2) / r2
} else {
c2_2d + (a2_2d * su2 + b2_2d * sv2) / r2
};
(f, t1, t2)
};
let mut results = Vec::new();
let n_steps = 72;
let dt = std::f64::consts::TAU / (n_steps as f64);
for &is_int in &[false, true] {
for i in 0..n_steps {
let t_a = i as f64 * dt;
let t_b = (i + 1) as f64 * dt;
let (fa, _, _) = eval(t_a, is_int);
let (fb, _, _) = eval(t_b, is_int);
let mut root = None;
if fa.abs() < 1e-12 {
root = Some(t_a);
} else if fa.signum() != fb.signum() {
let mut lo = t_a;
let mut hi = t_b;
let mut f_lo = fa;
let mut best_t = 0.5 * (lo + hi);
for _ in 0..28 {
let mid = 0.5 * (lo + hi);
let (fm, _, _) = eval(mid, is_int);
best_t = mid;
if fm.abs() < 1e-13 {
break;
}
if fm.signum() == f_lo.signum() {
lo = mid;
f_lo = fm;
} else {
hi = mid;
}
}
root = Some(best_t);
}
if let Some(r_angle) = root {
let (f_final, t1_2d, t2_2d) = eval(r_angle, is_int);
if f_final.abs() < 1e-6 {
let t1 = DVec3::new(t1_2d.x, t1_2d.y, c1.center.z);
let t2 = DVec3::new(t2_2d.x, t2_2d.y, c2.center.z);
if !results.iter().any(|(p1, p2): &(DVec3, DVec3)| {
(*p1 - t1).length_squared() < 1e-6 && (*p2 - t2).length_squared() < 1e-6
}) {
results.push((t1, t2));
}
}
}
}
}
results
}
#[cfg(test)]
fn circle_circle_tangents(c1: DVec3, r1: f64, c2: DVec3, r2: f64) -> Vec<(DVec3, DVec3)> {
let dx = c2.x - c1.x;
let dy = c2.y - c1.y;
let dist = (dx * dx + dy * dy).sqrt();
if dist < 1e-9 {
return Vec::new(); // concentric: no common tangent
}
let (ux, uy) = (dx / dist, dy / dist); // centre→centre unit
let (px, py) = (-uy, ux); // its perpendicular
let mut out = Vec::new();
for (num, internal) in [(r2 - r1, false), (-(r1 + r2), true)] {
let k = num / dist;
if k.abs() > 1.0 {
continue; // circles too close for this tangent family
}
let s = (1.0 - k * k).max(0.0).sqrt();
for pm in [1.0_f64, -1.0] {
let nx = k * ux + pm * s * px;
let ny = k * uy + pm * s * py;
let t1 = DVec3::new(c1.x - r1 * nx, c1.y - r1 * ny, c1.z);
let t2 = if internal {
DVec3::new(c2.x + r2 * nx, c2.y + r2 * ny, c2.z)
} else {
DVec3::new(c2.x - r2 * nx, c2.y - r2 * ny, c2.z)
};
out.push((t1, t2));
}
}
out
common_curve_tangents(
TangentObject::Circle { center: c1, radius: r1 },
TangentObject::Circle { center: c2, radius: r2 },
)
}
#[cfg(test)]
@@ -360,6 +543,143 @@ mod tangent_tests {
let tans = circle_circle_tangents(DVec3::ZERO, 2.0, DVec3::new(1.5, 0.0, 0.0), 2.0);
assert_eq!(tans.len(), 2, "overlap → the 2 internal tangents vanish");
}
#[test]
fn point_ellipse_tangents_are_tangent_to_ellipse() {
let e = TangentObject::Ellipse {
center: DVec3::ZERO,
major_axis: DVec3::new(20.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.5, // minor radius = 10
};
let p = DVec3::new(20.0, 20.0, 0.0);
let (t0, t1) = point_target_tangents(p, e).expect("point is outside ellipse");
for t in [t0, t1] {
// Must lie on ellipse (x/20)^2 + (y/10)^2 = 1
let on_ell = ((t.x / 20.0).powi(2) + (t.y / 10.0).powi(2) - 1.0).abs();
assert!(on_ell < 1e-6, "point {t:?} not on ellipse: {on_ell}");
// The line (p - t) must be perpendicular to the ellipse normal at t.
// For (x/a)^2 + (y/b)^2 = 1, gradient/normal is (x/a^2, y/b^2).
let grad = DVec3::new(t.x / (20.0 * 20.0), t.y / (10.0 * 10.0), 0.0);
let line_dir = p - t;
let dot = grad.dot(line_dir);
assert!(dot.abs() < 1e-6, "line is not tangent to ellipse: dot={dot}");
}
// A point inside the ellipse has no tangents
assert!(point_target_tangents(DVec3::new(5.0, 5.0, 0.0), e).is_none());
}
#[test]
fn separated_ellipse_and_circle_have_four_common_tangents() {
let e1 = TangentObject::Ellipse {
center: DVec3::new(-30.0, 0.0, 0.0),
major_axis: DVec3::new(20.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.5,
};
let c2 = TangentObject::Circle {
center: DVec3::new(30.0, 0.0, 0.0),
radius: 10.0,
};
let tans = common_curve_tangents(e1, c2);
assert_eq!(tans.len(), 4, "separated ellipse and circle should have 4 common tangents");
for (t1, t2) in &tans {
// t1 on ellipse
let dx1 = t1.x - (-30.0);
let dy1 = t1.y;
let on_e1 = ((dx1 / 20.0).powi(2) + (dy1 / 10.0).powi(2) - 1.0).abs();
assert!(on_e1 < 1e-4, "t1 not on ellipse: {on_e1}");
// t2 on circle
let on_c2 = ((*t2 - DVec3::new(30.0, 0.0, 0.0)).length() - 10.0).abs();
assert!(on_c2 < 1e-4, "t2 not on circle: {on_c2}");
// Line t1->t2 is tangent to ellipse at t1
let grad_e = DVec3::new(dx1 / 400.0, dy1 / 100.0, 0.0);
let line_dir = *t2 - *t1;
assert!(grad_e.dot(line_dir).abs() < 1e-4, "line not tangent to ellipse");
// Line t1->t2 is tangent to circle at t2 (perpendicular to radius)
let rad_c = *t2 - DVec3::new(30.0, 0.0, 0.0);
assert!(rad_c.dot(line_dir).abs() < 1e-4, "line not tangent to circle");
}
}
#[test]
fn separated_two_ellipses_have_four_common_tangents() {
let e1 = TangentObject::Ellipse {
center: DVec3::new(-35.0, 0.0, 0.0),
major_axis: DVec3::new(20.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.5,
};
let e2 = TangentObject::Ellipse {
center: DVec3::new(35.0, 0.0, 0.0),
major_axis: DVec3::new(15.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.6,
};
let tans = common_curve_tangents(e1, e2);
assert_eq!(tans.len(), 4, "separated two ellipses should have 4 common tangents");
for (t1, t2) in &tans {
let dx1 = t1.x - (-35.0);
let dy1 = t1.y;
let on_e1 = ((dx1 / 20.0).powi(2) + (dy1 / 10.0).powi(2) - 1.0).abs();
assert!(on_e1 < 1e-4, "t1 not on ellipse 1: {on_e1}");
let dx2 = t2.x - 35.0;
let dy2 = t2.y;
let on_e2 = ((dx2 / 15.0).powi(2) + (dy2 / 9.0).powi(2) - 1.0).abs();
assert!(on_e2 < 1e-4, "t2 not on ellipse 2: {on_e2}");
let line_dir = *t2 - *t1;
let grad1 = DVec3::new(dx1 / 400.0, dy1 / 100.0, 0.0);
assert!(grad1.dot(line_dir).abs() < 1e-4, "line not tangent to ellipse 1");
let grad2 = DVec3::new(dx2 / 225.0, dy2 / 81.0, 0.0);
assert!(grad2.dot(line_dir).abs() < 1e-4, "line not tangent to ellipse 2");
}
}
#[test]
fn line_command_tangent_to_ellipse_workflow() {
let mut cmd = LineCommand::new();
let e1 = TangentObject::Ellipse {
center: DVec3::new(-30.0, 0.0, 0.0),
major_axis: DVec3::new(20.0, 0.0, 0.0),
normal: DVec3::Z,
minor_axis_ratio: 0.5,
};
let c2 = TangentObject::Circle {
center: DVec3::new(30.0, 0.0, 0.0),
radius: 10.0,
};
// Pick 1: Tangent on Ellipse 1 (top vertex area)
let r1 = cmd.on_point_with_tangent(DVec3::new(-30.0, 10.0, 0.0), Some(e1));
assert!(matches!(r1, Some(CmdResult::NeedPoint)));
// Live preview with tangent on circle 2
let preview = cmd.on_preview_wires_with_tangent(DVec3::new(30.0, 10.0, 0.0), Some(c2));
assert_eq!(preview.len(), 1);
// Pick 2: Tangent on Circle 2
let r2 = cmd.on_point_with_tangent(DVec3::new(30.0, 10.0, 0.0), Some(c2));
assert!(matches!(r2, Some(CmdResult::CommitEntity(_))));
// Start a new line: Pick 1 as normal point, Pick 2 as tangent to ellipse
let mut cmd2 = LineCommand::new();
cmd2.on_point(DVec3::new(0.0, 30.0, 0.0));
let r_second = cmd2.on_point_with_tangent(DVec3::new(-30.0, 10.0, 0.0), Some(e1));
assert!(matches!(r_second, Some(CmdResult::CommitEntity(_))));
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
use acadrust::entities::polygon_mesh::{PolygonMesh, PolygonMeshVertex};
use acadrust::types::Vector3;
use acadrust::EntityType;
use codec::entities::polygon_mesh::{PolygonMesh, PolygonMeshVertex};
use codec::types::Vector3;
use codec::EntityType;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, DynField};
+8 -5
View File
@@ -1,9 +1,9 @@
// MLINE command — create a styled group of parallel lines.
use acadrust::entities::{MLine, MLineFlags, MLineJustification, MLineSegment, MLineVertex};
use acadrust::objects::MLineStyle;
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use codec::entities::{MLine, MLineFlags, MLineJustification, MLineSegment, MLineVertex};
use codec::objects::MLineStyle;
use codec::types::Vector3;
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
@@ -187,6 +187,7 @@ impl MlineCommand {
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: true,
@@ -212,7 +213,9 @@ impl MlineCommand {
plinegen: true,
fill_tris,
fill_tris_low,
})
..Default::default()
})
}
}
+1
View File
@@ -1,3 +1,4 @@
pub mod apollonius;
pub mod arc;
pub mod attdef;
pub mod centerline;
+3 -3
View File
@@ -3,8 +3,8 @@
// Command: POINT (PO)
// POINT commits one entity and exits. MULTIPOINT stays active.
use acadrust::types::Vector3;
use acadrust::{EntityType, Point as CadPoint};
use codec::types::Vector3;
use codec::{EntityType, Point as CadPoint};
use crate::t;
use crate::command::{CadCommand, CmdResult};
@@ -81,5 +81,5 @@ impl CadCommand for PointCommand {
inventory::submit!(crate::command::CommandRegistration { names: &["POINT", "MULTIPOINT"] }); // PointCommand
// Point display style system variables + dialog.
inventory::submit!(crate::command::CommandRegistration {
names: &["PDMODE", "PDSIZE", "DDPTYPE"]
names: &["PDMODE", "PDSIZE", "DDPTYPE", "PTYPE"]
});
+3 -3
View File
@@ -1,8 +1,8 @@
// 3DPOLY creates open or closed non-planar paths.
use acadrust::entities::Polyline3D;
use acadrust::types::Vector3;
use acadrust::EntityType;
use codec::entities::Polyline3D;
use codec::types::Vector3;
use codec::EntityType;
use crate::t;
use crate::command::{CadCommand, CmdResult};
+153 -148
View File
@@ -13,9 +13,9 @@
// Bulge is stored per vertex (segment i→i+1); positive = CCW, negative = CW.
// Widths are stored per segment as (start, end).
use acadrust::entities::LwVertex;
use acadrust::types::Vector2;
use acadrust::{EntityType, Handle, LwPolyline};
use codec::entities::LwVertex;
use codec::types::Vector2;
use codec::{EntityType, Handle, LwPolyline};
use glam::{DVec2, DVec3, Vec2, Vec3};
use crate::t;
@@ -47,7 +47,7 @@ enum SegMode {
/// sub-step returns to `None` once it has produced a vertex, or when Undo /
/// Enter / Escape backs out of it.
#[derive(Clone, Copy, PartialEq, Debug)]
enum Sub {
pub(crate) enum Sub {
None,
/// Width / Halfwidth: starting width, then ending width. `half` scales the
/// prompt and the typed value by two.
@@ -81,7 +81,7 @@ enum Sub {
impl Sub {
/// The step reads a single typed value (width, length, angle, radius).
fn is_scalar(self) -> bool {
pub(crate) fn is_scalar(self) -> bool {
matches!(
self,
Sub::WidthStart { .. }
@@ -97,7 +97,7 @@ impl Sub {
}
/// The typed value is an angle (degrees) rather than a length.
fn is_angle(self) -> bool {
pub(crate) fn is_angle(self) -> bool {
matches!(
self,
Sub::ArcAngle | Sub::ArcCenterAngle { .. } | Sub::ArcRadiusAngle { .. }
@@ -249,149 +249,8 @@ impl PlineCommand {
.unwrap_or(DVec2::X)
}
/// Sign for an arc whose direction is not fixed by its definition
/// (CEnter / Radius endpoints, chord length): bend the way the previous
/// segment was heading so the polyline flows on, else counter-clockwise.
fn free_arc_sign(&self, a: DVec2, e: DVec2) -> f64 {
let chord = e - a;
let cross = match self.last_tangent {
Some(t) => t.as_dvec2().perp_dot(chord),
None => 1.0,
};
if cross < 0.0 {
-1.0
} else {
1.0
}
}
/// Bulge of the arc from `a` to `e` through `s` (3-point arc). `None` when
/// the points are collinear or coincident.
fn bulge_through(a: DVec2, s: DVec2, e: DVec2) -> Option<f64> {
let d = 2.0 * (a.x * (s.y - e.y) + s.x * (e.y - a.y) + e.x * (a.y - s.y));
if d.abs() < 1e-12 {
return None;
}
let a2 = a.length_squared();
let s2 = s.length_squared();
let e2 = e.length_squared();
let cx = (a2 * (s.y - e.y) + s2 * (e.y - a.y) + e2 * (a.y - s.y)) / d;
let cy = (a2 * (e.x - s.x) + s2 * (a.x - e.x) + e2 * (s.x - a.x)) / d;
let c = DVec2::new(cx, cy);
let start = (a - c).y.atan2((a - c).x);
let end = (e - c).y.atan2((e - c).x);
// The arc bends towards `s`: with `s` right of the chord a → e the
// arc runs counter-clockwise, left of it clockwise.
let ccw = (e - a).perp_dot(s - a) < 0.0;
let mut sweep = end - start;
if ccw {
if sweep <= 0.0 {
sweep += std::f64::consts::TAU;
}
} else if sweep >= 0.0 {
sweep -= std::f64::consts::TAU;
}
Some((sweep / 4.0).tan())
}
/// Endpoint of the arc about `c` starting at `a` with signed `sweep`.
fn rotate_about(c: DVec2, a: DVec2, sweep: f64) -> DVec2 {
let (sn, cs) = sweep.sin_cos();
let v = a - c;
c + DVec2::new(v.x * cs - v.y * sn, v.x * sn + v.y * cs)
}
/// Resolve the arc a sub-step describes for a candidate `cursor`/point:
/// `(endpoint, bulge)`. `None` when the geometry is degenerate for that
/// input (e.g. a chord longer than the diameter).
fn arc_for(&self, sub: Sub, a: DVec2, p: DVec2) -> Option<(DVec2, f64)> {
let sweep_ok = |s: f64| s.abs() > 1e-9 && s.abs() < std::f64::consts::TAU;
match sub {
Sub::ArcAngleEnd { angle } if sweep_ok(angle) => {
if (p - a).length_squared() < 1e-12 {
return None;
}
Some((p, (angle / 4.0).tan()))
}
Sub::ArcAngleCenter { angle } if sweep_ok(angle) => {
if (a - p).length_squared() < 1e-12 {
return None;
}
Some((Self::rotate_about(p, a, angle), (angle / 4.0).tan()))
}
Sub::ArcAngleRadiusDir { angle, r } if sweep_ok(angle) && r > 0.0 => {
let dir = (p - a).try_normalize()?;
let chord = 2.0 * r * (angle.abs() * 0.5).sin();
Some((a + dir * chord, (angle / 4.0).tan()))
}
Sub::ArcCenterEnd { c } => {
let r = (a - c).length();
if r < 1e-9 {
return None;
}
let dir = (p - c).try_normalize()?;
let e = c + dir * r;
let start = (a - c).y.atan2((a - c).x);
let end = (e - c).y.atan2((e - c).x);
let sign = self.free_arc_sign(a, e);
let mut sweep = (end - start).rem_euclid(std::f64::consts::TAU);
if sign < 0.0 {
sweep -= std::f64::consts::TAU;
}
if !sweep_ok(sweep) {
return None;
}
Some((e, (sweep / 4.0).tan()))
}
Sub::ArcCenterAngle { c } => {
// `p` carries the typed angle in `x`.
let angle = p.x;
if (a - c).length_squared() < 1e-12 || !sweep_ok(angle) {
return None;
}
Some((Self::rotate_about(c, a, angle), (angle / 4.0).tan()))
}
Sub::ArcCenterLength { c } => {
// `p` carries the typed chord length in `x`.
let len = p.x;
let r = (a - c).length();
if r < 1e-9 || len <= 0.0 || len > 2.0 * r {
return None;
}
let sweep = 2.0 * (len / (2.0 * r)).asin();
// Minor arc; bend on the side the previous segment favours.
let e_ccw = Self::rotate_about(c, a, sweep);
let sign = self.free_arc_sign(a, e_ccw);
let sweep = sweep * sign;
Some((Self::rotate_about(c, a, sweep), (sweep / 4.0).tan()))
}
Sub::ArcDirectionEnd { dir } => {
if (p - a).length_squared() < 1e-12 {
return None;
}
Some((p, compute_bulge(a, dir, p)))
}
Sub::ArcRadiusEnd { r } if r > 0.0 => {
let chord = (p - a).length();
if chord < 1e-9 || chord > 2.0 * r {
return None;
}
let sweep = 2.0 * (chord / (2.0 * r)).asin() * self.free_arc_sign(a, p);
Some((p, (sweep / 4.0).tan()))
}
Sub::ArcRadiusAngleDir { r, angle } if r > 0.0 && sweep_ok(angle) => {
let dir = (p - a).try_normalize()?;
let chord = 2.0 * r * (angle.abs() * 0.5).sin();
Some((a + dir * chord, (angle / 4.0).tan()))
}
Sub::ArcSecondEnd { s } => {
if (p - a).length_squared() < 1e-12 {
return None;
}
Some((p, Self::bulge_through(a, s, p)?))
}
_ => None,
}
arc_for(sub, a, p, self.last_tangent)
}
/// Land a segment ending at local `e` with `bulge`, publish it, and
@@ -582,6 +441,152 @@ impl PlineCommand {
// ── Arc geometry helpers ───────────────────────────────────────────────────
/// Sign for an arc whose direction is not fixed by its definition
/// (CEnter / Radius endpoints, chord length): bend the way the previous
/// segment was heading so the polyline flows on, else counter-clockwise.
fn free_arc_sign(last_tangent: Option<Vec2>, a: DVec2, e: DVec2) -> f64 {
let chord = e - a;
let cross = match last_tangent {
Some(t) => t.as_dvec2().perp_dot(chord),
None => 1.0,
};
if cross < 0.0 {
-1.0
} else {
1.0
}
}
/// Bulge of the arc from `a` to `e` through `s` (3-point arc). `None` when
/// the points are collinear or coincident.
fn bulge_through(a: DVec2, s: DVec2, e: DVec2) -> Option<f64> {
let d = 2.0 * (a.x * (s.y - e.y) + s.x * (e.y - a.y) + e.x * (a.y - s.y));
if d.abs() < 1e-12 {
return None;
}
let a2 = a.length_squared();
let s2 = s.length_squared();
let e2 = e.length_squared();
let cx = (a2 * (s.y - e.y) + s2 * (e.y - a.y) + e2 * (a.y - s.y)) / d;
let cy = (a2 * (e.x - s.x) + s2 * (a.x - e.x) + e2 * (s.x - a.x)) / d;
let c = DVec2::new(cx, cy);
let start = (a - c).y.atan2((a - c).x);
let end = (e - c).y.atan2((e - c).x);
// The arc bends towards `s`: with `s` right of the chord a → e the
// arc runs counter-clockwise, left of it clockwise.
let ccw = (e - a).perp_dot(s - a) < 0.0;
let mut sweep = end - start;
if ccw {
if sweep <= 0.0 {
sweep += std::f64::consts::TAU;
}
} else if sweep >= 0.0 {
sweep -= std::f64::consts::TAU;
}
Some((sweep / 4.0).tan())
}
/// Endpoint of the arc about `c` starting at `a` with signed `sweep`.
fn rotate_about(c: DVec2, a: DVec2, sweep: f64) -> DVec2 {
let (sn, cs) = sweep.sin_cos();
let v = a - c;
c + DVec2::new(v.x * cs - v.y * sn, v.x * sn + v.y * cs)
}
/// Resolve the arc an arc sub-step describes (PLINE, VPCLIP) from `a` for a
/// candidate `cursor`/point `p`, after a segment leaving along
/// `last_tangent`: `(endpoint, bulge)`. `None` when the geometry is
/// degenerate for that input (e.g. a chord longer than the diameter).
pub(crate) fn arc_for(sub: Sub, a: DVec2, p: DVec2, last_tangent: Option<Vec2>) -> Option<(DVec2, f64)> {
let sweep_ok = |s: f64| s.abs() > 1e-9 && s.abs() < std::f64::consts::TAU;
match sub {
Sub::ArcAngleEnd { angle } if sweep_ok(angle) => {
if (p - a).length_squared() < 1e-12 {
return None;
}
Some((p, (angle / 4.0).tan()))
}
Sub::ArcAngleCenter { angle } if sweep_ok(angle) => {
if (a - p).length_squared() < 1e-12 {
return None;
}
Some((rotate_about(p, a, angle), (angle / 4.0).tan()))
}
Sub::ArcAngleRadiusDir { angle, r } if sweep_ok(angle) && r > 0.0 => {
let dir = (p - a).try_normalize()?;
let chord = 2.0 * r * (angle.abs() * 0.5).sin();
Some((a + dir * chord, (angle / 4.0).tan()))
}
Sub::ArcCenterEnd { c } => {
let r = (a - c).length();
if r < 1e-9 {
return None;
}
let dir = (p - c).try_normalize()?;
let e = c + dir * r;
let start = (a - c).y.atan2((a - c).x);
let end = (e - c).y.atan2((e - c).x);
let sign = free_arc_sign(last_tangent, a, e);
let mut sweep = (end - start).rem_euclid(std::f64::consts::TAU);
if sign < 0.0 {
sweep -= std::f64::consts::TAU;
}
if !sweep_ok(sweep) {
return None;
}
Some((e, (sweep / 4.0).tan()))
}
Sub::ArcCenterAngle { c } => {
// `p` carries the typed angle in `x`.
let angle = p.x;
if (a - c).length_squared() < 1e-12 || !sweep_ok(angle) {
return None;
}
Some((rotate_about(c, a, angle), (angle / 4.0).tan()))
}
Sub::ArcCenterLength { c } => {
// `p` carries the typed chord length in `x`.
let len = p.x;
let r = (a - c).length();
if r < 1e-9 || len <= 0.0 || len > 2.0 * r {
return None;
}
let sweep = 2.0 * (len / (2.0 * r)).asin();
// Minor arc; bend on the side the previous segment favours.
let e_ccw = rotate_about(c, a, sweep);
let sign = free_arc_sign(last_tangent, a, e_ccw);
let sweep = sweep * sign;
Some((rotate_about(c, a, sweep), (sweep / 4.0).tan()))
}
Sub::ArcDirectionEnd { dir } => {
if (p - a).length_squared() < 1e-12 {
return None;
}
Some((p, compute_bulge(a, dir, p)))
}
Sub::ArcRadiusEnd { r } if r > 0.0 => {
let chord = (p - a).length();
if chord < 1e-9 || chord > 2.0 * r {
return None;
}
let sweep = 2.0 * (chord / (2.0 * r)).asin() * free_arc_sign(last_tangent, a, p);
Some((p, (sweep / 4.0).tan()))
}
Sub::ArcRadiusAngleDir { r, angle } if r > 0.0 && sweep_ok(angle) => {
let dir = (p - a).try_normalize()?;
let chord = 2.0 * r * (angle.abs() * 0.5).sin();
Some((a + dir * chord, (angle / 4.0).tan()))
}
Sub::ArcSecondEnd { s } => {
if (p - a).length_squared() < 1e-12 {
return None;
}
Some((p, bulge_through(a, s, p)?))
}
_ => None,
}
}
/// Compute the bulge for the arc from `a` to `b` that is tangent to `tangent` at `a`.
/// Returns 0.0 if the points are coincident or the tangent is parallel to the chord.
pub(crate) fn compute_bulge(a: DVec2, tangent: DVec2, b: DVec2) -> f64 {
+10 -6
View File
@@ -10,9 +10,9 @@
// 3. User drags to pick width; height is computed from the image's aspect ratio.
// 4. Entity is committed.
use acadrust::entities::RasterImage;
use acadrust::types::Vector3;
use acadrust::EntityType;
use codec::entities::RasterImage;
use codec::types::Vector3;
use codec::EntityType;
use glam::DVec3;
use crate::t;
@@ -95,8 +95,9 @@ impl ImageCommand {
world_width,
world_height,
);
img.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE
| acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
img.flags = codec::entities::ImageDisplayFlags::SHOW_IMAGE
| codec::entities::ImageDisplayFlags::SHOW_NOT_ALIGNED
| codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
self.plane.place_entity(EntityType::RasterImage(img))
}
ImageSource::Embedded(image) => {
@@ -175,6 +176,7 @@ impl CadCommand for ImageCommand {
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -200,7 +202,9 @@ impl CadCommand for ImageCommand {
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
..Default::default()
})
}
}
+16 -10
View File
@@ -5,9 +5,9 @@
// XLINE — infinite construction line: same two-click pattern, yields XLine.
use crate::t;
use acadrust::entities::{Ray as RayEnt, XLine as XLineEnt};
use acadrust::types::Vector3;
use acadrust::EntityType;
use codec::entities::{Ray as RayEnt, XLine as XLineEnt};
use codec::types::Vector3;
use codec::EntityType;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::scene::model::wire_model::WireModel;
@@ -93,6 +93,7 @@ impl CadCommand for RayCommand {
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -118,7 +119,9 @@ impl CadCommand for RayCommand {
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
..Default::default()
})
}
}
@@ -168,7 +171,7 @@ impl XLineCommand {
let base = self.base?;
let first = self.reference?.1;
let last = (pt - base).try_normalize()?;
let direction = cadkernel::space::curve::angle_bisector(
let direction = kernel::space::curve::angle_bisector(
first.to_array(), last.to_array(), self.plane.z.to_array())?;
(base, DVec3::from_array(direction))
}
@@ -290,7 +293,7 @@ impl CadCommand for XLineCommand {
(delta.x.hypot(delta.y) > 1e-10).then(|| delta.y.atan2(delta.x).to_degrees())
}
XLineMode::OffsetDistance => {
let distance = cadkernel::space::Vec3::from(cursor.to_array()).distance(origin.to_array().into());
let distance = kernel::space::Vec3::from(cursor.to_array()).distance(origin.to_array().into());
(distance.is_finite() && distance > 0.0).then_some(distance)
}
_ => None,
@@ -302,7 +305,7 @@ impl CadCommand for XLineCommand {
}
fn inject_before_entity_pick(&self) -> bool { true }
fn inject_picked_entity(&mut self, entity: EntityType) { self.picked = Some(entity); }
fn on_entity_pick(&mut self, _handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
fn on_entity_pick(&mut self, _handle: codec::Handle, _pt: DVec3) -> CmdResult {
let xyz = |v: Vector3| DVec3::new(v.x, v.y, v.z);
let reference = match self.picked.take() {
Some(EntityType::Line(line)) => Some((xyz(line.start), xyz(line.end) - xyz(line.start))),
@@ -378,6 +381,7 @@ impl CadCommand for XLineCommand {
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -406,7 +410,9 @@ impl CadCommand for XLineCommand {
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
..Default::default()
})
}
}
@@ -524,13 +530,13 @@ mod tests {
assert!(matches!(command.on_point(DVec3::Y * 2.0), CmdResult::NeedPoint));
assert!(command.needs_entity_pick());
let line = acadrust::entities::Line::from_points(
let line = codec::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
);
command.inject_picked_entity(EntityType::Line(line));
assert!(matches!(
command.on_entity_pick(acadrust::Handle::new(1), DVec3::ZERO),
command.on_entity_pick(codec::Handle::new(1), DVec3::ZERO),
CmdResult::NeedPoint
));
let CmdResult::CommitEntity(EntityType::XLine(entity)) =
+4 -4
View File
@@ -2,10 +2,10 @@
use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use cadkernel::geom2d::{distance_to, Curve, Polyline, PolylineVertex, Vec2};
use cadkernel::space::{PlanarCurve, Plane, Vec3};
use codec::types::Vector3;
use codec::{EntityType, Handle};
use kernel::geom2d::{distance_to, Curve, Polyline, PolylineVertex, Vec2};
use kernel::space::{PlanarCurve, Plane, Vec3};
use glam::DVec3;
use rustc_hash::FxHashMap;
+4 -4
View File
@@ -10,10 +10,10 @@
// POLY_C — Circumscribed about circle (edges tangent to circle)
// POLY_E — Edge (pick two endpoints of one edge)
use acadrust::entities::LwVertex;
use acadrust::types::Vector2;
use acadrust::{EntityType, LwPolyline};
use cadkernel::geom2d::{
use codec::entities::LwVertex;
use codec::types::Vector2;
use codec::{EntityType, LwPolyline};
use kernel::geom2d::{
arc_span, fillet_between_rays, Curve as KernelCurve, Frame as KernelFrame,
Polyline as KernelPolyline, PolylineVertex as KernelVertex, Ray as KernelRay,
Transform as KernelTransform, Vec2 as KernelVec2,
+4 -4
View File
@@ -1,9 +1,9 @@
// Freehand sketch tool.
use acadrust::entities::{Line, LwPolyline, LwVertex, Spline};
use acadrust::types::{Vector2, Vector3};
use acadrust::EntityType;
use cadkernel::geom2d::NurbsCurve;
use codec::entities::{Line, LwPolyline, LwVertex, Spline};
use codec::types::{Vector2, Vector3};
use codec::EntityType;
use kernel::geom2d::NurbsCurve;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
+3 -3
View File
@@ -6,9 +6,9 @@
// starts the next connected shape. Enter after the third point commits a
// triangle.
use acadrust::entities::Solid;
use acadrust::types::Vector3;
use acadrust::EntityType;
use codec::entities::Solid;
use codec::types::Vector3;
use codec::EntityType;
use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
+4 -4
View File
@@ -4,8 +4,8 @@
// Click to add fit points. Enter (≥2 pts) → commits EntityType::Spline.
use crate::t;
use acadrust::types::Vector3;
use acadrust::{CadDocument, EntityType, Handle, Spline};
use codec::types::Vector3;
use codec::{CadDocument, EntityType, Handle, Spline};
use std::collections::HashMap;
use crate::command::{CadCommand, CmdResult};
@@ -90,7 +90,7 @@ impl SplineCommand {
}
fn control_spline(points: &[[f64; 3]], degree: usize, closed: bool) -> Option<Spline> {
let curve = cadkernel::space::NurbsCurve3::from_control_polygon(degree, points, closed)?;
let curve = kernel::space::NurbsCurve3::from_control_polygon(degree, points, closed)?;
let mut spline = Spline {
degree: curve.degree() as i32,
control_points: curve.control_points().iter().map(|p| Vector3::new(p[0], p[1], p[2])).collect(),
@@ -453,7 +453,7 @@ mod tests {
#[test]
fn closed_fitted_polyline_conversion_preserves_a_periodic_spline() {
use acadrust::entities::{
use codec::entities::{
Polyline2D, PolylineFlags, SmoothSurfaceType, Vertex2D, VertexFlags,
};
+3 -3
View File
@@ -11,9 +11,9 @@
// so the planar point-pick path is used (every vertex shares the polyline
// elevation, taken from the first point's Z).
use acadrust::entities::{LwPolyline, LwVertex};
use acadrust::types::{Vector2, Vector3};
use acadrust::EntityType;
use codec::entities::{LwPolyline, LwVertex};
use codec::types::{Vector2, Vector3};
use codec::EntityType;
use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
+10 -10
View File
@@ -1,10 +1,10 @@
// WIPEOUT command — draw a polygonal mask or derive one from a closed polyline.
use acadrust::entities::{Wipeout, WipeoutClipType};
use acadrust::types::{Vector2, Vector3};
use acadrust::{CadDocument, EntityType, Handle};
use cadkernel::geom2d::{polygon_frame, Tolerance};
use cadkernel::space::Plane;
use codec::entities::{Wipeout, WipeoutClipType};
use codec::types::{Vector2, Vector3};
use codec::{CadDocument, EntityType, Handle};
use kernel::geom2d::{polygon_frame, Tolerance};
use kernel::space::Plane;
use glam::DVec3;
use crate::t;
@@ -184,8 +184,8 @@ impl CadCommand for WipeoutCommand {
if let Some(first) = self.points.first() {
let first = self.plane.to_local(*first);
let point = self.plane.to_local(point);
let distance = cadkernel::geom2d::Vec2::new(point.x, point.y)
.distance(cadkernel::geom2d::Vec2::new(first.x, first.y));
let distance = kernel::geom2d::Vec2::new(point.x, point.y)
.distance(kernel::geom2d::Vec2::new(first.x, first.y));
if self.points.len() >= 3
&& distance <= Tolerance::default().linear()
{
@@ -445,8 +445,8 @@ fn make_poly_wipeout(points: &[[f64; 2]], plane: Plane) -> Option<EntityType> {
fn explicitly_closed(points: &[[f64; 2]]) -> bool {
points.len() >= 4
&& cadkernel::geom2d::Vec2::from(points[0])
.distance(cadkernel::geom2d::Vec2::from(*points.last().unwrap()))
&& kernel::geom2d::Vec2::from(points[0])
.distance(kernel::geom2d::Vec2::from(*points.last().unwrap()))
<= Tolerance::default().linear()
}
@@ -501,7 +501,7 @@ inventory::submit!(crate::command::CommandRegistration { names: &["WIPEOUT"] });
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::LwPolyline;
use codec::entities::LwPolyline;
#[test]
fn command_undo_never_removes_the_first_point() {
+1 -1
View File
@@ -13,7 +13,7 @@ pub fn tool() -> ToolDef {
// ── CadCommand implementation ─────────────────────────────────────────────
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
+1 -1
View File
@@ -13,7 +13,7 @@ pub fn tool() -> ToolDef {
// ── CadCommand implementation ─────────────────────────────────────────────
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
+6 -3
View File
@@ -1,5 +1,5 @@
use cadkernel::space::polygon;
use acadrust::{EntityType, Handle};
use kernel::space::polygon;
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::command::{
@@ -375,6 +375,7 @@ impl CadCommand for AreaCommand {
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -400,7 +401,9 @@ impl CadCommand for AreaCommand {
plinegen: true,
fill_tris: Vec::new(),
fill_tris_low: Vec::new(),
})
..Default::default()
})
}
fn area_preview_regions(&self) -> Option<Vec<AreaPreviewRegion>> {
+4 -1
View File
@@ -95,6 +95,7 @@ impl CadCommand for DistCommand {
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -123,7 +124,9 @@ impl CadCommand for DistCommand {
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
..Default::default()
})
}
}
+7 -7
View File
@@ -1,9 +1,9 @@
// DIVIDE and MEASURE place points or block references along a curve.
use acadrust::entities::{Insert, Point as PointEnt};
use acadrust::types::Vector3;
use acadrust::{Entity, EntityType, Handle};
use cadkernel::space::PlanarCurve;
use codec::entities::{Insert, Point as PointEnt};
use codec::types::Vector3;
use codec::{Entity, EntityType, Handle};
use kernel::space::PlanarCurve;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, CurveMarker, WorkingPlane};
@@ -198,7 +198,7 @@ impl<const MEASURE: bool> CadCommand for CurveMarkerCommand<MEASURE> {
return None;
}
let origin = self.value_origin?;
let distance = cadkernel::space::Vec3::from(cursor.to_array())
let distance = kernel::space::Vec3::from(cursor.to_array())
.distance(origin.to_array().into());
(distance.is_finite() && distance > 0.0).then_some(distance)
}
@@ -308,7 +308,7 @@ fn make_marker(
enum MeasuredCurve {
Planar(PlanarCurve),
Spatial(cadkernel::space::ArcLengthCurve3),
Spatial(kernel::space::ArcLengthCurve3),
}
impl MeasuredCurve {
@@ -359,7 +359,7 @@ inventory::submit!(crate::command::CommandRegistration { names: &["MEASURE"] });
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::{Circle, Line};
use codec::entities::{Circle, Line};
fn point(entity: &EntityType) -> DVec3 {
let EntityType::Point(point) = entity else { panic!("expected point") };
+5 -2
View File
@@ -14,7 +14,7 @@
// All arithmetic is kept in f64 (picked points stay full precision; downcasting
// to f32 loses several hundredths of a unit at survey-scale coordinates).
use acadrust::{EntityType, Handle};
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::t;
@@ -77,6 +77,7 @@ impl MeasureGeomCommand {
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -102,7 +103,9 @@ impl MeasureGeomCommand {
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
..Default::default()
}
}
/// Extract (radius) from a Circle or Arc; `None` for anything else.
+4 -4
View File
@@ -11,7 +11,7 @@
use std::collections::BTreeMap;
use std::path::Path;
use acadrust::tables::Layer;
use codec::tables::Layer;
use crate::scene::Scene;
@@ -179,7 +179,7 @@ pub fn translate(
// Whatever reference the target drawing held it through does not
// come with it.
layer.flags.xref_dependent = false;
layer.xref_block_record_handle = acadrust::Handle::NULL;
layer.xref_block_record_handle = codec::Handle::NULL;
let _ = scene.document.layers.add(layer);
}
let moved = merge_layer(scene, &mapping.from, &mapping.to, options.force_bylayer);
@@ -214,12 +214,12 @@ pub fn merge_layer(scene: &mut Scene, from: &str, to: &str, force_bylayer: bool)
let common = entity.common_mut();
common.layer = to.to_string();
if force_bylayer {
common.color = acadrust::Color::ByLayer;
common.color = codec::Color::ByLayer;
common.color_name = None;
common.color_book_handle = None;
common.linetype = String::new();
common.linetype_handle = None;
common.line_weight = acadrust::LineWeight::ByLayer;
common.line_weight = codec::LineWeight::ByLayer;
}
moved += 1;
}
+1 -1
View File
@@ -15,7 +15,7 @@ pub fn tool() -> ToolDef {
// ── CadCommand implementation ─────────────────────────────────────────────
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
+7 -3
View File
@@ -31,7 +31,7 @@ impl CadModule for DrawModule {
use crate::modules::annotate::{
angular_dim, leader_cmd, linear_dim, mleader_cmd, mtext, radius_dim, text,
};
use crate::modules::insert::{create_block, insert_block};
use crate::modules::insert::{attedit, create_block, edit_block, insert_block};
use clipboard::{copy_clip, cut, paste};
use draw::{arc, circle, ellipse, hatch, line, polyline, shapes};
use groups::{group, ungroup};
@@ -200,9 +200,13 @@ impl CadModule for DrawModule {
},
RibbonGroup {
title: "Block",
// Slide-out: Define / Manage / Synchronize Attributes and
// Set Base Point.
tools: vec![
RibbonItem::LargeTool(create_block::tool()),
RibbonItem::LargeTool(insert_block::tool()),
insert_block::gallery(),
RibbonItem::Tool(create_block::tool()),
RibbonItem::Tool(edit_block::tool()),
RibbonItem::Tool(attedit::tool()),
],
},
RibbonGroup {
+7 -7
View File
@@ -10,7 +10,7 @@
// With 2 pairs: translate + rotate (+ optional uniform scale to fit)
// With 3 pairs: rigid 3D placement without scaling
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
@@ -127,7 +127,7 @@ impl CadCommand for AlignCommand {
}
AlignState::Src2 => {
let pair = [self.src1.unwrap().to_array(), pt.to_array()];
if cadkernel::space::align_point_pairs(&pair, &pair, false).is_none() {
if kernel::space::align_point_pairs(&pair, &pair, false).is_none() {
return CmdResult::NeedPoint;
}
self.src2 = Some(pt);
@@ -136,7 +136,7 @@ impl CadCommand for AlignCommand {
}
AlignState::Dst2 => {
let pair = [self.dst1.unwrap().to_array(), pt.to_array()];
if cadkernel::space::align_point_pairs(&pair, &pair, false).is_none() {
if kernel::space::align_point_pairs(&pair, &pair, false).is_none() {
return CmdResult::NeedPoint;
}
self.dst2 = Some(pt);
@@ -145,7 +145,7 @@ impl CadCommand for AlignCommand {
}
AlignState::Src3 => {
let frame = [self.src1.unwrap().to_array(), self.src2.unwrap().to_array(), pt.to_array()];
if cadkernel::space::align_point_pairs(&frame, &frame, false).is_none() {
if kernel::space::align_point_pairs(&frame, &frame, false).is_none() {
return CmdResult::NeedPoint;
}
self.src3 = Some(pt);
@@ -295,10 +295,10 @@ impl AlignCommand {
source.push(s3.to_array());
target.push(d3.to_array());
}
let Some(matrix) = cadkernel::space::align_point_pairs(&source, &target, with_scale)
let Some(matrix) = kernel::space::align_point_pairs(&source, &target, with_scale)
else { return CmdResult::NeedPoint; };
CmdResult::TransformSelected(self.handles.clone(), EntityTransform::Affine(
acadrust::types::Transform::from_matrix(acadrust::types::Matrix4 { m: matrix }),
codec::types::Transform::from_matrix(codec::types::Matrix4 { m: matrix }),
))
}
}
@@ -340,7 +340,7 @@ mod tests {
};
assert_eq!(handles, vec![handle]);
for (from, expected) in source.into_iter().zip(target) {
let actual = transform.apply(acadrust::types::Vector3::new(from.x, from.y, from.z));
let actual = transform.apply(codec::types::Vector3::new(from.x, from.y, from.z));
let actual = DVec3::new(actual.x, actual.y, actual.z);
assert!(actual.abs_diff_eq(expected, 1.0e-12));
}
+2 -2
View File
@@ -13,7 +13,7 @@
// Polar array: copies rotated around a center point by a total angle.
// 1. Center point → 2. Item count (text) → 3. Total angle in degrees (text)
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
@@ -456,7 +456,7 @@ impl CadCommand for ArrayPolarCommand {
// 2. Enter item count (total, including the original at the path start)
// → Returns BatchCopy with Translate transforms derived from path samples.
use acadrust::EntityType;
use codec::EntityType;
use crate::entities::curve::entity_curve;
use std::f64::consts::PI as FPI;
use std::f64::consts::TAU as FTAU;
+3 -3
View File
@@ -15,7 +15,7 @@ use crate::scene::model::wire_model::WireModel;
pub struct AtteditCommand {
/// The picked block reference, once the user clicks one.
picked: Option<acadrust::Handle>,
picked: Option<codec::Handle>,
}
impl AtteditCommand {
@@ -37,7 +37,7 @@ impl CadCommand for AtteditCommand {
self.picked.is_none()
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
fn on_entity_pick(&mut self, handle: codec::Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
@@ -57,7 +57,7 @@ impl CadCommand for AtteditCommand {
vec![]
}
fn attedit_pending_handle(&self) -> Option<acadrust::Handle> {
fn attedit_pending_handle(&self) -> Option<codec::Handle> {
self.picked
}
}
+7 -7
View File
@@ -5,13 +5,13 @@
//! first and second derivatives reproduce the source curves' tangent and
//! geometric curvature (G2).
use acadrust::entities::{EntityCommon, Spline};
use cadkernel::space::curve as space_curve;
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use codec::entities::{EntityCommon, Spline};
use kernel::space::curve as space_curve;
use codec::types::Vector3;
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::t;
use cadkernel::space::NurbsCurve3;
use kernel::space::NurbsCurve3;
use crate::command::{CadCommand, CmdOption, CmdResult};
use crate::entities::common::BulgeArc;
@@ -447,7 +447,7 @@ fn polyline_endpoint(points: &[DVec3], click: DVec3) -> Option<EndpointFrame> {
}
}
fn arc_endpoint(arc: &acadrust::entities::Arc, click: DVec3) -> Option<EndpointFrame> {
fn arc_endpoint(arc: &codec::entities::Arc, click: DVec3) -> Option<EndpointFrame> {
if arc.radius <= EPS {
return None;
}
@@ -479,7 +479,7 @@ fn arc_endpoint(arc: &acadrust::entities::Arc, click: DVec3) -> Option<EndpointF
})
}
fn lw_point(poly: &acadrust::entities::LwPolyline, index: usize) -> DVec3 {
fn lw_point(poly: &codec::entities::LwPolyline, index: usize) -> DVec3 {
let normal = to_dvec(poly.normal).normalize_or_zero();
let p = poly.vertices[index].location;
ocs_to_dvec(p.x, p.y, poly.elevation, normal)
+3 -3
View File
@@ -15,7 +15,7 @@
// block record in place, so there is no copy-back and no transform — the space
// render filter (`current_layout_block_handle`) already draws only the block.
use acadrust::{EntityType, Handle};
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
@@ -66,7 +66,7 @@ pub struct BlockEditSession {
/// Rebasing the origin moves them with the definition; Discard restores
/// their exact on-entry state.
pub reference_attributes:
Vec<(Handle, Vec<acadrust::entities::AttributeEntity>)>,
Vec<(Handle, Vec<codec::entities::AttributeEntity>)>,
/// Camera state of the space that was active when BEDIT began, restored
/// on Save/Discard so the view returns exactly where it was (#425).
pub return_camera: crate::scene::view::camera::Camera,
@@ -75,7 +75,7 @@ pub struct BlockEditSession {
pub editor_camera: crate::scene::view::camera::Camera,
/// Transient UCS being manipulated in this block tab. On commit its full
/// frame is baked into the definition and this returns to `None`.
pub editor_ucs: Option<acadrust::tables::Ucs>,
pub editor_ucs: Option<codec::tables::Ucs>,
}
// ── BEDIT pick command ─────────────────────────────────────────────────────
+13 -13
View File
@@ -8,11 +8,11 @@
// BREAK @ (at-sign as second point) → Break at a single point (splits without gap).
use crate::modules::draw::modify::spline_ops::{spline_cut, spline_nearest_t, spline_range};
use acadrust::entities::{
use codec::entities::{
Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt,
};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use codec::types::Vector3;
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::t;
@@ -88,12 +88,12 @@ fn break_line(line: &LineEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
result
}
fn picked_spans(curve: &cadkernel::space::PlanarCurve, p1: DVec3, p2: DVec3) -> Option<Vec<[f64; 2]>> {
fn picked_spans(curve: &kernel::space::PlanarCurve, p1: DVec3, p2: DVec3) -> Option<Vec<[f64; 2]>> {
let first = curve.plane.project(p1.to_array())?;
let second = curve.plane.project(p2.to_array())?;
let a = cadkernel::geom2d::closest_point(&curve.curve, first).t;
let b = cadkernel::geom2d::closest_point(&curve.curve, second).t;
cadkernel::geom2d::break_spans(&curve.curve, a, b, cadkernel::geom2d::Tolerance::new(1e-9))
let a = kernel::geom2d::closest_point(&curve.curve, first).t;
let b = kernel::geom2d::closest_point(&curve.curve, second).t;
kernel::geom2d::break_spans(&curve.curve, a, b, kernel::geom2d::Tolerance::new(1e-9))
}
fn break_arc(arc: &ArcEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
@@ -101,7 +101,7 @@ fn break_arc(arc: &ArcEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let Some(spans) = picked_spans(&curve, p1, p2) else {
return vec![EntityType::Arc(arc.clone())];
};
let cadkernel::geom2d::Curve::Arc(geometry) = &curve.curve else { unreachable!() };
let kernel::geom2d::Curve::Arc(geometry) = &curve.curve else { unreachable!() };
spans.into_iter().map(|[from, to]| {
let mut result = arc.clone();
result.common.handle = Handle::NULL;
@@ -111,7 +111,7 @@ fn break_arc(arc: &ArcEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
}).collect()
}
fn break_circle(circle: &acadrust::entities::Circle, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
fn break_circle(circle: &codec::entities::Circle, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let curve = crate::entities::curve::circle_curve(circle);
let Some(spans) = picked_spans(&curve, p1, p2) else {
return vec![EntityType::Circle(circle.clone())];
@@ -133,7 +133,7 @@ fn break_lwpolyline(p: &LwPolyline, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let unchanged = || vec![EntityType::LwPolyline(p.clone())];
let Some(curve) = crate::entities::curve::lwpolyline_curve(p) else { return unchanged(); };
let Some(spans) = picked_spans(&curve, p1, p2) else { return unchanged(); };
let cadkernel::geom2d::Curve::Polyline(geometry) = &curve.curve else { unreachable!() };
let kernel::geom2d::Curve::Polyline(geometry) = &curve.curve else { unreachable!() };
let mut result = Vec::with_capacity(spans.len());
for [from, to] in spans {
let Some(range) = geometry.ranged(from, to) else { return unchanged(); };
@@ -172,7 +172,7 @@ fn break_ellipse(ell: &EllipseEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
let Some(spans) = picked_spans(&curve, p1, p2) else {
return vec![EntityType::Ellipse(ell.clone())];
};
let cadkernel::geom2d::Curve::Ellipse(geometry) = &curve.curve else { unreachable!() };
let kernel::geom2d::Curve::Ellipse(geometry) = &curve.curve else { unreachable!() };
spans.into_iter().map(|[from, to]| {
let mut result = ell.clone();
result.common.handle = Handle::NULL;
@@ -387,8 +387,8 @@ inventory::submit!(crate::command::CommandRegistration { names: &["BREAK"] });
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::LwVertex;
use acadrust::types::Vector2;
use codec::entities::LwVertex;
use codec::types::Vector2;
#[test]
fn first_option_replaces_the_selection_point_and_at_reuses_it() {
+1 -1
View File
@@ -5,7 +5,7 @@
// Step 1: pick base point
// Step 2+: each click makes another copy at (click - base); Enter to finish.
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, EntityTransform};
+1 -1
View File
@@ -5,7 +5,7 @@
//! as TRIM, to nearby boundary entities. Keeping this compact command-local
//! index avoids cloning/scanning the complete drawing on every mouse move.
use acadrust::{EntityType, Handle};
use codec::{EntityType, Handle};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::scene::convert::tess::entity_world_aabb_f64;
+171 -49
View File
@@ -8,7 +8,7 @@
// Polyline2D → Lines + Arcs
// Polyline3D → Lines
// Polyline → Lines
// Insert → constituent entities (via acadrust explode_from_document)
// Insert → constituent entities (via opencadcodec explode_from_document)
// MLine → Lines (spine + offset lines per miter direction)
// Dimension → Lines (extension + dimension + arrows) + Text
//
@@ -16,15 +16,15 @@
use super::geom::normalize_angle as norm_angle;
use acadrust::entities::EntityCommon;
use acadrust::entities::{
use codec::entities::EntityCommon;
use codec::entities::{
Arc as ArcEnt, Block, BlockEnd, Circle as CircleEnt, Dimension, Line as LineEnt, LwPolyline,
MLine,
};
use acadrust::entities::{Polyline, Polyline2D};
use acadrust::tables::BlockRecord;
use acadrust::types::Vector3;
use acadrust::{CadDocument, EntityType, Handle};
use codec::entities::{Polyline, Polyline2D};
use codec::tables::BlockRecord;
use codec::types::Vector3;
use codec::{CadDocument, EntityType, Handle};
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::entities::curve::lwpolyline_world_xy;
@@ -64,7 +64,7 @@ pub fn explode_polyline_segments(entity: &EntityType) -> Vec<EntityType> {
pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<EntityType> {
match entity {
EntityType::Line(line) => {
let Some(association) = acadrust::entities::CenterMarkAssociation::read(
let Some(association) = codec::entities::CenterMarkAssociation::read(
&line.common.extended_data,
) else {
return vec![];
@@ -74,7 +74,7 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
.map(|segment| {
let mut common = line.common.clone();
common.handle = Handle::NULL;
acadrust::entities::CenterMarkAssociation::remove(&mut common.extended_data);
codec::entities::CenterMarkAssociation::remove(&mut common.extended_data);
EntityType::Line(LineEnt {
common,
start: Vector3::new(segment[0].x, segment[0].y, segment[0].z),
@@ -114,10 +114,58 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
}
EntityType::MLine(ml) => explode_mline(ml),
EntityType::Dimension(dim) => explode_dimension(dim, document),
EntityType::MText(t) => crate::entities::mtext::explode_mtext(t, document),
_ => vec![],
}
}
/// Explode every entity; one piece list per input, input order preserved,
/// empty lists kept. Used by benchmarks and regression tests.
pub fn explode_batch(
items: &[(Handle, &EntityType)],
doc: &CadDocument,
) -> Vec<Vec<EntityType>> {
items.iter().map(|(_, e)| explode_entity(e, doc)).collect()
}
/// Plan the EXPLODE command for an already lock-filtered selection:
/// order-preserving, empty pieces dropped (matching the command arm).
pub fn plan_explode(
selected: &[(Handle, &EntityType)],
doc: &CadDocument,
) -> Vec<(Handle, Vec<EntityType>)> {
selected
.iter()
.filter_map(|(handle, entity)| {
let pieces = explode_entity(entity, doc);
if pieces.is_empty() {
None
} else {
Some((*handle, pieces))
}
})
.collect()
}
/// Apply precomputed EXPLODE replacements. Initial version is a
/// byte-for-byte extraction of the command arm's loop (erase one handle,
/// add pieces one at a time); optimizations change its internals only.
/// Returns the number of exploded sources.
pub fn apply_explode_replacements(
scene: &mut crate::scene::Scene,
replacements: Vec<(Handle, Vec<EntityType>)>,
) -> usize {
let exploded = replacements.len();
let handles: Vec<Handle> = replacements.iter().map(|(h, _)| *h).collect();
scene.erase_entities(&handles);
for (_, pieces) in replacements {
for piece in pieces {
scene.add_entity(piece);
}
}
exploded
}
fn explode_polyline(p: &Polyline) -> Vec<EntityType> {
let n = p.vertices.len();
if n < 2 {
@@ -125,7 +173,7 @@ fn explode_polyline(p: &Polyline) -> Vec<EntityType> {
}
let closed = p.flags.is_closed();
let n_segs = if closed { n } else { n - 1 };
let mut result = Vec::new();
let mut result = Vec::with_capacity(n_segs);
for i in 0..n_segs {
let v0 = &p.vertices[i];
let v1 = &p.vertices[(i + 1) % n];
@@ -141,14 +189,14 @@ fn explode_polyline(p: &Polyline) -> Vec<EntityType> {
result
}
fn explode_polyline3d(p: &acadrust::entities::Polyline3D) -> Vec<EntityType> {
fn explode_polyline3d(p: &codec::entities::Polyline3D) -> Vec<EntityType> {
let n = p.vertices.len();
if n < 2 {
return vec![];
}
let closed = p.is_closed();
let n_segs = if closed { n } else { n - 1 };
let mut result = Vec::new();
let mut result = Vec::with_capacity(n_segs);
for i in 0..n_segs {
let v0 = &p.vertices[i];
let v1 = &p.vertices[(i + 1) % n];
@@ -178,7 +226,7 @@ fn explode_polyline2d(p: &Polyline2D) -> Vec<EntityType> {
let normal = Vector3::new(normal.x, normal.y, normal.z);
let plane = crate::entities::curve::ocs_plane(normal.clone(), elevation);
let mut result = Vec::new();
let mut result = Vec::with_capacity(n_segs);
for i in 0..n_segs {
let v0 = &p.vertices[i];
let v1 = &p.vertices[(i + 1) % n];
@@ -257,7 +305,7 @@ fn explode_lwpolyline(p: &LwPolyline) -> Vec<EntityType> {
let elevation = p.elevation;
let n_segs = if p.is_closed { n } else { n - 1 };
let mut result = Vec::new();
let mut result = Vec::with_capacity(n_segs);
for i in 0..n_segs {
let v0 = &p.vertices[i];
let v1 = &p.vertices[(i + 1) % n];
@@ -309,7 +357,7 @@ fn bulge_to_arc(
) -> Option<EntityType> {
let ba = crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge)?;
// acadrust Arc is always CCW from start_angle to end_angle. Negative
// opencadcodec Arc is always CCW from start_angle to end_angle. Negative
// bulge means the polyline goes p0→p1 the CW way around the centre,
// which is the same circular arc traversed p1→p0 the CCW way — so
// swap endpoints when bulge < 0.
@@ -340,15 +388,15 @@ fn explode_mline(ml: &MLine) -> Vec<EntityType> {
if n < 2 {
return vec![];
}
let closed = ml.flags.contains(acadrust::entities::MLineFlags::CLOSED);
let closed = ml.flags.contains(codec::entities::MLineFlags::CLOSED);
let scale = ml.scale_factor;
let n_segs = if closed { n } else { n - 1 };
let mut result = Vec::new();
let mut result = Vec::with_capacity(n_segs * 3);
// Helper: build a Line from two Vector3 positions.
let make_line = |common: &acadrust::entities::EntityCommon,
s: &acadrust::types::Vector3,
e: &acadrust::types::Vector3|
let make_line = |common: &codec::entities::EntityCommon,
s: &codec::types::Vector3,
e: &codec::types::Vector3|
-> EntityType {
let mut c = common.clone();
c.handle = Handle::NULL;
@@ -395,7 +443,7 @@ fn explode_mline(ml: &MLine) -> Vec<EntityType> {
/// Convert a Dimension entity into Lines (geometry) + Text (label).
/// A NULL-handle line segment for a baked dimension block.
fn dim_seg(a: Vector3, b: Vector3, common: &acadrust::entities::EntityCommon) -> EntityType {
fn dim_seg(a: Vector3, b: Vector3, common: &codec::entities::EntityCommon) -> EntityType {
let mut c = common.clone();
c.handle = Handle::NULL;
EntityType::Line(LineEnt {
@@ -408,8 +456,8 @@ fn dim_seg(a: Vector3, b: Vector3, common: &acadrust::entities::EntityCommon) ->
fn dim_geom_entities(
geometry: &crate::scene::convert::tessellate::DimGeom,
ext_common: &acadrust::entities::EntityCommon,
dim_common: &acadrust::entities::EntityCommon,
ext_common: &codec::entities::EntityCommon,
dim_common: &codec::entities::EntityCommon,
) -> Vec<EntityType> {
let mut entities = Vec::new();
let point = |value: [f32; 3]| {
@@ -418,6 +466,7 @@ fn dim_geom_entities(
for (points, common) in [
(geometry.ext_lines.as_slice(), ext_common),
(geometry.dim_lines.as_slice(), dim_common),
(geometry.arrow_lines.as_slice(), dim_common),
] {
for run in points.split(|point| point[0].is_nan()) {
for pair in run.windows(2) {
@@ -426,7 +475,7 @@ fn dim_geom_entities(
}
}
for triangle in geometry.arrow_fill.chunks_exact(3) {
let mut solid = acadrust::entities::Solid::triangle(
let mut solid = codec::entities::Solid::triangle(
point(triangle[0]),
point(triangle[1]),
point(triangle[2]),
@@ -448,7 +497,7 @@ fn dim_terminator(
dx: f64,
dy: f64,
arrow: &crate::scene::convert::tessellate::ArrowKind,
common: &acadrust::entities::EntityCommon,
common: &codec::entities::EntityCommon,
) -> Vec<EntityType> {
use crate::scene::convert::tessellate::ArrowKind as A;
let (dx, dy) = norm2(dx, dy, 1.0, 0.0);
@@ -459,7 +508,7 @@ fn dim_terminator(
};
let mut out: Vec<EntityType> = Vec::new();
let tri = |a: Vector3, b: Vector3, c: Vector3, out: &mut Vec<EntityType>| {
let mut s = acadrust::entities::Solid::triangle(a, b, c);
let mut s = codec::entities::Solid::triangle(a, b, c);
s.common = common.clone();
s.common.handle = Handle::NULL;
out.push(EntityType::Solid(s));
@@ -565,7 +614,7 @@ fn terminator_circle(
center: Vector3,
r: f64,
filled: bool,
common: &acadrust::entities::EntityCommon,
common: &codec::entities::EntityCommon,
out: &mut Vec<EntityType>,
) {
const N: usize = 16;
@@ -580,7 +629,7 @@ fn terminator_circle(
}
if filled {
for i in 0..N {
let mut s = acadrust::entities::Solid::triangle(center, ring[i], ring[i + 1]);
let mut s = codec::entities::Solid::triangle(center, ring[i], ring[i + 1]);
s.common = common.clone();
s.common.handle = Handle::NULL;
out.push(EntityType::Solid(s));
@@ -593,7 +642,7 @@ fn dim_center_mark(
center: Vector3,
dimcen: f64,
radius: f64,
common: &acadrust::entities::EntityCommon,
common: &codec::entities::EntityCommon,
) -> Vec<EntityType> {
let mag = dimcen.abs();
if mag < 1e-9 {
@@ -624,7 +673,7 @@ fn dim_arc_segs(
radius: f64,
a1: f64,
a2: f64,
common: &acadrust::entities::EntityCommon,
common: &codec::entities::EntityCommon,
) -> Vec<EntityType> {
use std::f64::consts::PI;
let mut sweep = a2 - a1;
@@ -644,7 +693,7 @@ fn dim_arc_segs_with_sweep(
radius: f64,
start: f64,
sweep: f64,
common: &acadrust::entities::EntityCommon,
common: &codec::entities::EntityCommon,
) -> Vec<EntityType> {
use std::f64::consts::PI;
let steps = 12usize.max((sweep.abs() / (PI / 36.0)).ceil() as usize);
@@ -759,25 +808,25 @@ fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> DimMetrics {
/// A DIMCLR* of 0 (ByBlock) or 256 (ByLayer) keeps the dimension's own colour
/// so the block inherits it; a specific ACI overrides. DIMLW* < 0 (ByBlock /
/// ByLayer) keeps the dimension's lineweight.
fn dim_common(base: &acadrust::entities::EntityCommon, clr: i16, lw: i16) -> acadrust::entities::EntityCommon {
fn dim_common(base: &codec::entities::EntityCommon, clr: i16, lw: i16) -> codec::entities::EntityCommon {
let mut c = base.clone();
c.handle = Handle::NULL;
if clr != 0 && clr != 256 {
c.color = acadrust::types::Color::from_index(clr);
c.color = codec::types::Color::from_index(clr);
c.color_name = None;
c.color_book_handle = None;
}
if lw >= 0 {
c.line_weight = acadrust::types::LineWeight::from_value(lw);
c.line_weight = codec::types::LineWeight::from_value(lw);
}
c
}
fn with_dim_linetype(
mut common: acadrust::entities::EntityCommon,
mut common: codec::entities::EntityCommon,
doc: &CadDocument,
handle: Handle,
) -> acadrust::entities::EntityCommon {
) -> codec::entities::EntityCommon {
common.linetype = doc
.line_types
.iter()
@@ -800,8 +849,8 @@ fn angular_block_segs(
p2: Vector3,
arc_loc: Vector3,
met: &DimMetrics,
ext_c: &acadrust::entities::EntityCommon,
dim_c: &acadrust::entities::EntityCommon,
ext_c: &codec::entities::EntityCommon,
dim_c: &codec::entities::EntityCommon,
explicit_sweep: Option<(f64, f64)>,
) -> Vec<EntityType> {
use std::f64::consts::PI;
@@ -853,7 +902,7 @@ fn angular_block_segs(
/// The text anchor for a radial leader: the saved text middle point when set,
/// else the midpoint of `a` and `b` — mirroring the live `dimension_text_position`.
fn dim_text_anchor(
base: &acadrust::entities::dimension::DimensionBase,
base: &codec::entities::dimension::DimensionBase,
a: Vector3,
b: Vector3,
) -> Vector3 {
@@ -1495,15 +1544,15 @@ impl CadCommand for ExplodeCommand {
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::DimensionLinear;
use codec::entities::DimensionLinear;
#[test]
fn minsert_too_large_to_materialize_is_left_intact() {
let mut doc = CadDocument::new();
let mut block = acadrust::tables::BlockRecord::new("B1");
let mut block = codec::tables::BlockRecord::new("B1");
block.handle = doc.allocate_handle();
doc.block_records.add(block).unwrap();
let mut insert = acadrust::entities::Insert::new("B1", Vector3::new(0.0, 0.0, 0.0));
let mut insert = codec::entities::Insert::new("B1", Vector3::new(0.0, 0.0, 0.0));
insert.row_count = u16::MAX;
insert.column_count = u16::MAX;
assert!(explode_entity(&EntityType::Insert(insert), &doc).is_empty());
@@ -1512,12 +1561,12 @@ mod tests {
#[test]
fn unexplodable_block_is_not_exploded() {
let mut doc = CadDocument::new();
let mut block = acadrust::tables::BlockRecord::new("NO_EXPLODE");
let mut block = codec::tables::BlockRecord::new("NO_EXPLODE");
block.handle = doc.allocate_handle();
block.explodable = false;
doc.block_records.add(block).unwrap();
let insert = acadrust::entities::Insert::new("NO_EXPLODE", Vector3::new(0.0, 0.0, 0.0));
let insert = codec::entities::Insert::new("NO_EXPLODE", Vector3::new(0.0, 0.0, 0.0));
assert!(explode_entity(&EntityType::Insert(insert), &doc).is_empty());
}
@@ -1772,7 +1821,7 @@ mod tests {
// two extension lines plus many arc chords.
#[test]
fn angular_dim_bakes_an_arc() {
use acadrust::entities::DimensionAngular3Pt;
use codec::entities::DimensionAngular3Pt;
let mut doc = CadDocument::new();
let mut d = DimensionAngular3Pt::new(
Vector3::new(0.0, 0.0, 0.0),
@@ -1799,7 +1848,7 @@ mod tests {
// Diameter endpoints stay equidistant from the circle center.
#[test]
fn diameter_dim_bakes_through_center() {
use acadrust::entities::DimensionDiameter;
use codec::entities::DimensionDiameter;
let mut doc = CadDocument::new();
let center = Vector3::new(3.0, 4.0, 0.0);
let edge = Vector3::new(8.0, 4.0, 0.0); // radius 5 along +x
@@ -1864,8 +1913,8 @@ mod tests {
// must carry the source thickness instead of resetting to 0 (#916).
#[test]
fn explode_keeps_lwpolyline_thickness() {
use acadrust::entities::LwVertex;
use acadrust::types::Vector2;
use codec::entities::LwVertex;
use codec::types::Vector2;
let mut pl = LwPolyline::new();
// One straight span + one bulged span, so the result holds a Line and
@@ -1900,7 +1949,7 @@ mod tests {
#[test]
fn explode_keeps_polyline2d_extrusion() {
use acadrust::entities::Vertex2D;
use codec::entities::Vertex2D;
let mut pl = Polyline2D::new();
pl.vertices = vec![
@@ -1921,4 +1970,77 @@ mod tests {
assert!((line.start.x - 2.0).abs() < 1e-12);
assert!((line.end.x - 2.0).abs() < 1e-12);
}
// RED (TDD): a real DIMLINEAR must survive save -> reload with its baked
// *D geometry intact and a clean handle graph (no duplicates, no dangling
// owners). Headless codec round-trip, no GUI needed.
#[test]
fn dimlinear_survives_save_reload_with_baked_geometry() {
let mut failures = Vec::new();
for ext in ["dxf", "dwg"] {
let mut doc = CadDocument::new();
let mut d =
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
doc.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.unwrap();
let path = std::env::temp_dir().join(format!(
"ocs_dim_rt_{}.{ext}",
std::process::id()
));
crate::io::save_as_version(&doc, &path, codec::DxfVersion::AC1032)
.expect("save");
let loaded = crate::io::load_file(&path).expect("load");
let _ = std::fs::remove_file(&path);
let mut seen = std::collections::HashSet::new();
for e in loaded.entities() {
if !seen.insert(e.common().handle) {
failures.push(format!(
"{ext}: duplicate handle {:?}",
e.common().handle
));
}
}
let dims: Vec<_> = loaded
.entities()
.filter_map(|e| match e {
EntityType::Dimension(d) => Some(d.clone()),
_ => None,
})
.collect();
if dims.len() != 1 {
failures.push(format!("{ext}: dimension entity lost"));
continue;
}
let block_name = dims[0].base().block_name.clone();
if block_name.trim().is_empty() {
failures.push(format!("{ext}: dimension came back blockless"));
continue;
}
let Some(rec) = loaded.block_records.get(&block_name) else {
failures.push(format!("{ext}: *D block {block_name} missing after reload"));
continue;
};
let owned: Vec<Handle> = {
let mut h = rec.entity_handles.clone();
h.push(rec.block_entity_handle);
h.push(rec.block_end_handle);
h
};
if rec.entity_handles.is_empty() {
failures.push(format!("{ext}: baked block has no sub-entities"));
}
for h in &owned {
if h.is_null() || loaded.get_entity(*h).is_none() {
failures.push(format!(
"{ext}: dangling handle {h:?} in baked block {block_name}"
));
}
}
}
assert!(failures.is_empty(), "round-trip failures:\n{}", failures.join("\n"));
}
}
+179 -56
View File
@@ -9,19 +9,19 @@
// Pick two lines (line-only; arcs are not chamferable).
// Finds intersection, backs off dist1 along line 1 and dist2 along line 2.
use acadrust::entities::{Arc as ArcEnt, Line as LineEnt, LwPolyline};
use codec::entities::{Arc as ArcEnt, Line as LineEnt, LwPolyline, LwVertex};
// Shared plane geometry, from cadkernel via the local adapters.
// Shared plane geometry, from opencadkernel via the local adapters.
use super::geom;
use super::geom::{arc_points as arc_pts, line_line as ll, normalize_angle as norm_angle};
use cadkernel::geom2d::{
use kernel::geom2d::{
circle_circle_points as circle_circle_pts, fillet_between_rays, fillets_between, line_circle,
Arc as KernelArc, Curve as KernelCurve, Fillet as KernelFillet,
Line as KernelLine, Tolerance,
};
use acadrust::entities::EntityCommon;
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use codec::entities::EntityCommon;
use codec::types::Vector3;
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::t;
@@ -817,6 +817,14 @@ fn compute_fillet_entities(
radius: f64,
) -> Option<(EntityType, EntityType, Option<EntityType>)> {
let z = e1.elevation();
// A polyline arc segment would be filleted as its straight chord.
let picks_arc_segment = |entity: &FilletEntity| {
matches!(entity, FilletEntity::LwPoly { poly, seg_idx, .. }
if poly.vertices.get(*seg_idx).is_some_and(|vertex| vertex.bulge.abs() > 1e-12))
};
if picks_arc_segment(e1) || picks_arc_segment(e2) {
return None;
}
match (e1, e2) {
// ── Line × Line ───────────────────────────────────────────────────
@@ -848,24 +856,39 @@ fn compute_fillet_entities(
..
},
) if h1 == h2 => {
// Adjacent segments share a corner vertex — fillet that corner.
let (low, high) = if *s1 < *s2 { (*s1, *s2) } else { (*s2, *s1) };
let n = p1.vertices.len();
// The wrap-around corner of a closed polyline joins the last
// segment (n-1) and the first (0); their shared vertex is v0.
let wrap = p1.is_closed && low == 0 && high == n.saturating_sub(1);
// Segments must be consecutive, or the wrap-around pair above.
if !(high == low + 1 || wrap) {
let closed = p1.is_closed;
let seg_count = if closed { n } else { n.saturating_sub(1) };
let next = |seg: usize| if closed { (seg + 1) % n } else { seg + 1 };
let straight = |seg: usize| seg < seg_count && p1.vertices[seg].bulge.abs() < 1e-12;
// `before` runs into the corner and `after` leaves it. Between them
// sits nothing (a sharp corner), an old fillet arc being replaced,
// or — on an open polyline — the gap between its two ends, which
// the fillet closes. Arc segments themselves are never filleted:
// treating one as its chord bent the result across the shape.
enum Between {
Corner,
Arc(usize),
OpenEnds,
}
let order = |a: usize, b: usize| {
if next(a) == b {
Some((a, b, Between::Corner))
} else if next(a) < seg_count
&& next(next(a)) == b
&& !straight(next(a))
{
Some((a, b, Between::Arc(next(a))))
} else if !closed && seg_count >= 2 && a == seg_count - 1 && b == 0 {
Some((a, b, Between::OpenEnds))
} else {
None
}
};
let (before_seg, after_seg, between) = order(*s1, *s2).or_else(|| order(*s2, *s1))?;
if !straight(before_seg) || !straight(after_seg) {
return None;
}
// `before_seg` ends at the shared corner vertex, `after_seg` starts
// there. For the wrap corner that is seg n-1 → v0 → seg 0; for a
// normal corner it is seg low → v[high] → seg high.
let (before_seg, after_seg, corner_idx) = if wrap {
(high, low, 0)
} else {
(low, high, high)
};
let l1 = lwpoly_seg_as_line(p1, before_seg);
let l2 = lwpoly_seg_as_line(p1, after_seg);
// Re-map click to whichever segment each was picked on.
@@ -874,23 +897,49 @@ fn compute_fillet_entities(
} else {
(click2, click1)
};
match compute_fillet(&l1, c1, &l2, c2, radius)? {
(EntityType::Line(tl1), EntityType::Line(tl2), maybe_arc) => {
let t1 = [tl1.end.x, tl1.end.y]; // trimmed end of seg before corner
let t2 = [tl2.start.x, tl2.start.y]; // trimmed start of seg after corner
let bulge = if let Some(EntityType::Arc(ref fa)) = maybe_arc {
// center from arc entity
compute_bulge(t1, t2, [fa.center.x, fa.center.y])
} else {
0.0 // r=0, sharp corner
};
let new_poly = lwpoly_replace_corner(p1, corner_idx, t1, t2, bulge);
let et = EntityType::LwPolyline(new_poly);
// Return same rebuilt poly for both slots; caller uses only h1.
Some((et.clone(), et, None))
let (EntityType::Line(tl1), EntityType::Line(tl2), maybe_arc) =
compute_fillet(&l1, c1, &l2, c2, radius)?
else {
return None;
};
let t1 = [tl1.end.x, tl1.end.y]; // trimmed end of seg before corner
let t2 = [tl2.start.x, tl2.start.y]; // trimmed start of seg after corner
let bulge = match maybe_arc {
Some(EntityType::Arc(ref fa)) => compute_bulge(t1, t2, [fa.center.x, fa.center.y]),
_ => 0.0, // r=0, sharp corner
};
let new_poly = match between {
Between::Corner => lwpoly_replace_corner(p1, after_seg, t1, t2, bulge),
// The old arc runs from its own vertex to the `after` vertex:
// move both onto the new tangent points.
Between::Arc(arc_seg) => {
let mut poly = p1.clone();
poly.common.handle = Handle::NULL;
poly.vertices[arc_seg].location.x = t1[0];
poly.vertices[arc_seg].location.y = t1[1];
poly.vertices[arc_seg].bulge = bulge;
poly.vertices[after_seg].location.x = t2[0];
poly.vertices[after_seg].location.y = t2[1];
poly
}
_ => None,
}
// The last vertex becomes the start of the fillet and the
// closing segment is the fillet itself.
Between::OpenEnds => {
let mut poly = p1.clone();
poly.common.handle = Handle::NULL;
let last = n - 1;
poly.vertices[last].location.x = t1[0];
poly.vertices[last].location.y = t1[1];
poly.vertices[last].bulge = bulge;
poly.vertices[0].location.x = t2[0];
poly.vertices[0].location.y = t2[1];
poly.is_closed = true;
poly
}
};
let et = EntityType::LwPolyline(new_poly);
// Return same rebuilt poly for both slots; caller uses only h1.
Some((et.clone(), et, None))
}
// ── LwPoly × LwPoly (different entities) ──────────────────────────
(
@@ -1305,6 +1354,9 @@ enum FilletStep {
// Radius may be typed directly or measured by picking two points.
WaitingForRadius,
/// Trim / No trim: whether the picked objects are cut back to the arc.
TrimMode,
RadiusSecondPoint {
first: DVec3,
},
@@ -1349,12 +1401,16 @@ impl FilletCommand {
/// Switch to the radius sub-step, remembering the first pick (if any) so
/// it can be restored afterwards.
fn enter_radius_substep(&mut self) {
self.enter_substep(FilletStep::WaitingForRadius);
}
fn enter_substep(&mut self, step: FilletStep) {
self.resume_second = if let FilletStep::Second { h1, e1, click1 } = &self.step {
Some((*h1, e1.clone(), *click1))
} else {
None
};
self.step = FilletStep::WaitingForRadius;
self.step = step;
}
/// Leave the radius sub-step, resuming the second pick when a first object
@@ -1366,7 +1422,12 @@ impl FilletCommand {
};
}
fn continue_after_fillet(&mut self, replacements: Vec<(Handle, Vec<EntityType>)>) -> CmdResult {
fn continue_after_fillet(&mut self, mut replacements: Vec<(Handle, Vec<EntityType>)>) -> CmdResult {
for entity in replacements.iter_mut().flat_map(|(_, entities)| entities.iter_mut()) {
if let EntityType::LwPolyline(poly) = entity {
drop_zero_length_segments(poly);
}
}
self.all_entities.retain(|entity| {
!replacements
.iter()
@@ -1439,6 +1500,12 @@ impl CadCommand for FilletCommand {
FilletStep::RadiusSecondPoint { .. } => {
crate::t!("FILLET Specify second point for radius:").into_owned()
}
FilletStep::TrimMode => if defaults::get_fillet_trim() {
crate::t!("FILLET Enter Trim mode option [Trim/No trim] <Trim>:")
} else {
crate::t!("FILLET Enter Trim mode option [Trim/No trim] <No trim>:")
}
.into_owned(),
FilletStep::Second { .. } => {
crate::tf!(
"FILLET Select second object (Line/Arc/LwPolyline) [R={:.4}]:",
@@ -1460,10 +1527,14 @@ impl CadCommand for FilletCommand {
}
opts.push(CmdOption::new("Polyline", "P"));
opts.push(CmdOption::new("Radius", "R"));
opts.push(CmdOption::new("Trim", "T"));
opts
}
FilletStep::Second { .. } => {
vec![CmdOption::new("Radius", "R")]
vec![CmdOption::new("Radius", "R"), CmdOption::new("Trim", "T")]
}
FilletStep::TrimMode => {
vec![CmdOption::new("Trim", "T"), CmdOption::new("No trim", "N")]
}
FilletStep::Polyline
| FilletStep::WaitingForRadius
@@ -1472,7 +1543,7 @@ impl CadCommand for FilletCommand {
}
fn wants_text_input(&self) -> bool {
matches!(self.step, FilletStep::WaitingForRadius)
matches!(self.step, FilletStep::WaitingForRadius | FilletStep::TrimMode)
}
fn dyn_field(&self) -> crate::command::DynField {
@@ -1503,6 +1574,16 @@ impl CadCommand for FilletCommand {
// Invalid — stay and re-prompt
Some(CmdResult::NeedPoint)
}
FilletStep::TrimMode => {
match text.trim().to_uppercase().as_str() {
"" => {}
"T" | "TRIM" => defaults::set_fillet_trim(true),
"N" | "NO TRIM" | "NOTRIM" => defaults::set_fillet_trim(false),
_ => return Some(CmdResult::NeedPoint),
}
self.resume_after_radius();
Some(CmdResult::NeedPoint)
}
FilletStep::First | FilletStep::Second { .. } => {
let t = text.trim();
let upper = t.to_uppercase();
@@ -1513,6 +1594,10 @@ impl CadCommand for FilletCommand {
if upper == "U" || upper == "UNDO" {
return self.undo_last();
}
if upper == "T" || upper == "TRIM" {
self.enter_substep(FilletStep::TrimMode);
return Some(CmdResult::NeedPoint);
}
// "R" alone → enter sub-step to collect radius
if upper == "R" {
self.enter_radius_substep();
@@ -1545,6 +1630,7 @@ impl CadCommand for FilletCommand {
self.step,
FilletStep::WaitingForRadius
| FilletStep::RadiusSecondPoint { .. }
| FilletStep::TrimMode
)
}
@@ -1552,7 +1638,7 @@ impl CadCommand for FilletCommand {
self.undo_last()
}
fn on_document_undone(&mut self, document: &acadrust::CadDocument) {
fn on_document_undone(&mut self, document: &codec::CadDocument) {
self.all_entities = document
.entities()
.map(crate::entities::curve::entity_with_lwpolyline_world_xy)
@@ -1570,7 +1656,8 @@ impl CadCommand for FilletCommand {
match &self.step {
FilletStep::WaitingForRadius
| FilletStep::RadiusSecondPoint { .. } => {
| FilletStep::RadiusSecondPoint { .. }
| FilletStep::TrimMode => {
return CmdResult::NeedPoint;
}
@@ -1640,6 +1727,17 @@ impl CadCommand for FilletCommand {
if let Some(e2) = e2 {
match compute_fillet_entities(&e1, click1, &e2, click, self.radius) {
// No trim: the objects stay as they are; only the arc
// is added (#1558).
Some((_, _, maybe_arc)) if !defaults::get_fillet_trim() => {
let Some(arc) = maybe_arc else {
return CmdResult::NeedPoint;
};
self.step = FilletStep::First;
self.resume_second = None;
self.made += 1;
CmdResult::CommitEntity(arc)
}
Some((new_e1, new_e2, maybe_arc)) => {
let mut first_replacements = vec![new_e1];
if let Some(arc) = maybe_arc {
@@ -1670,7 +1768,8 @@ impl CadCommand for FilletCommand {
match &self.step {
FilletStep::WaitingForRadius
| FilletStep::RadiusSecondPoint { .. } => vec![],
| FilletStep::RadiusSecondPoint { .. }
| FilletStep::TrimMode => vec![],
FilletStep::Polyline => {
let preview = self
.entity_index
@@ -1731,20 +1830,21 @@ impl CadCommand for FilletCommand {
if let Some((new_e1, new_e2, maybe_arc)) =
compute_fillet_entities(&e1, click1, &e2, click, self.radius)
{
let mut out = vec![
WireModel::solid(
let mut out = Vec::new();
if defaults::get_fillet_trim() {
out.push(WireModel::solid(
"fillet_e1".into(),
entity_pts(&new_e1),
WireModel::CYAN,
false,
),
WireModel::solid(
));
out.push(WireModel::solid(
"fillet_e2".into(),
entity_pts(&new_e2),
WireModel::CYAN,
false,
),
];
));
}
if let Some(arc) = maybe_arc {
out.push(WireModel::solid(
"fillet_arc".into(),
@@ -1807,6 +1907,29 @@ impl CadCommand for FilletCommand {
}
}
/// A radius-0 fillet over an existing arc leaves its two ends on one point;
/// the zero-length segment between them blocked every later fillet of that
/// corner (#1422). The later vertex carries the next segment, so it survives.
fn drop_zero_length_segments(poly: &mut LwPolyline) {
let same = |a: &LwVertex, b: &LwVertex| {
(a.location.x - b.location.x).abs() < 1e-9 && (a.location.y - b.location.y).abs() < 1e-9
};
let mut i = 0;
while i + 1 < poly.vertices.len() && poly.vertices.len() > 2 {
if same(&poly.vertices[i], &poly.vertices[i + 1]) {
poly.vertices.remove(i);
} else {
i += 1;
}
}
if poly.is_closed && poly.vertices.len() > 2 {
let n = poly.vertices.len();
if same(&poly.vertices[n - 1], &poly.vertices[0]) {
poly.vertices.pop();
}
}
}
// ══════════════════════════════════════════════════════════════════════════
// ChamferCommand
// ══════════════════════════════════════════════════════════════════════════
@@ -2147,7 +2270,7 @@ impl CadCommand for ChamferCommand {
self.undo_last()
}
fn on_document_undone(&mut self, document: &acadrust::CadDocument) {
fn on_document_undone(&mut self, document: &codec::CadDocument) {
self.all_entities = document
.entities()
.map(crate::entities::curve::entity_with_lwpolyline_world_xy)
@@ -2377,7 +2500,7 @@ mod tests {
/// by an infinite factor: the center becomes (inf, NaN).
fn overflowed_arc(sweep_sign: f64) -> ArcEnt {
let mut arc = ArcEnt::new();
arc.center = acadrust::types::Vector3::new(f64::INFINITY, f64::NAN, 0.0);
arc.center = codec::types::Vector3::new(f64::INFINITY, f64::NAN, 0.0);
arc.radius = f64::INFINITY;
arc.start_angle = 0.0;
arc.end_angle = sweep_sign * std::f64::consts::PI;
@@ -2399,18 +2522,18 @@ mod tests {
1.0,
vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)],
);
assert_eq!(keywords(&command), ["P", "R"]);
assert_eq!(keywords(&command), ["P", "R", "T"]);
assert!(command.on_text_input("U").is_none(), "nothing to undo yet");
command.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0));
assert!(matches!(
command.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)),
CmdResult::ReplaceManyContinue(_)
));
assert_eq!(keywords(&command), ["U", "P", "R"]);
assert_eq!(keywords(&command), ["U", "P", "R", "T"]);
assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument)));
assert_eq!(keywords(&command), ["P", "R"]);
assert_eq!(keywords(&command), ["P", "R", "T"]);
// The host hands the restored document back; the cache follows it.
let mut doc = acadrust::CadDocument::new();
let mut doc = codec::CadDocument::new();
let _ = doc.add_entity(line(0.0, 0.0, 20.0, 0.0, 7));
command.on_document_undone(&doc);
assert_eq!(command.all_entities.len(), 1);
+472
View File
@@ -0,0 +1,472 @@
//! FLATTEN: project entities onto the XY plane (Z=0).
//!
//! Extracted verbatim from `app/commands/inquiry.rs` so the command arm and
//! the COMMANDS benchmark share one implementation.
use codec::{EntityType, Handle};
use crate::scene::Scene;
pub fn flatten_entity_z(entity: &codec::EntityType) -> Option<codec::EntityType> {
use codec::types::Vector3;
let flattened = match entity {
codec::EntityType::Circle(_)
| codec::EntityType::Arc(_)
| codec::EntityType::Ellipse(_)
| codec::EntityType::LwPolyline(_)
| codec::EntityType::Polyline2D(_) => flatten_curve_entity(entity)?,
codec::EntityType::Line(source) => {
let mut line = source.clone();
line.start = flatten_point(line.start)?;
line.end = flatten_point(line.end)?;
line.thickness = 0.0;
line.normal = Vector3::UNIT_Z;
codec::EntityType::Line(line)
}
codec::EntityType::Polyline(source) => {
let mut polyline = source.clone();
for vertex in &mut polyline.vertices {
vertex.location = flatten_point(vertex.location)?;
}
codec::EntityType::Polyline(polyline)
}
codec::EntityType::Polyline3D(source) => {
let mut polyline = source.clone();
polyline.elevation = 0.0;
polyline.normal = Vector3::UNIT_Z;
for vertex in &mut polyline.vertices {
vertex.position = flatten_point(vertex.position)?;
}
codec::EntityType::Polyline3D(polyline)
}
codec::EntityType::Text(source) if positive_z_normal(source.normal) => {
let mut text = source.clone();
text.insertion_point = flatten_point(text.insertion_point)?;
if let Some(alignment) = text.alignment_point {
text.alignment_point = Some(flatten_point(alignment)?);
}
text.thickness = 0.0;
text.normal = Vector3::UNIT_Z;
codec::EntityType::Text(text)
}
codec::EntityType::MText(source) if positive_z_normal(source.normal) => {
let mut text = source.clone();
text.insertion_point = flatten_point(text.insertion_point)?;
text.normal = Vector3::UNIT_Z;
codec::EntityType::MText(text)
}
codec::EntityType::Point(source) => {
let mut point = source.clone();
point.location = flatten_point(point.location)?;
point.thickness = 0.0;
point.normal = Vector3::UNIT_Z;
codec::EntityType::Point(point)
}
codec::EntityType::Spline(source) => {
let mut spline = source.clone();
for point in &mut spline.control_points {
*point = flatten_point(*point)?;
}
for point in &mut spline.fit_points {
*point = flatten_point(*point)?;
}
spline.begin_tangent = flatten_vector(spline.begin_tangent)?;
spline.end_tangent = flatten_vector(spline.end_tangent)?;
spline.normal = Vector3::UNIT_Z;
spline.flags.planar = true;
codec::EntityType::Spline(spline)
}
codec::EntityType::Solid(source) => {
let corners = crate::entities::solid::wcs_corners(source);
let mut solid = source.clone();
solid.first_corner = flatten_array(corners[0])?;
solid.second_corner = flatten_array(corners[1])?;
solid.third_corner = flatten_array(corners[2])?;
solid.fourth_corner = flatten_array(corners[3])?;
solid.normal = Vector3::UNIT_Z;
solid.thickness = 0.0;
codec::EntityType::Solid(solid)
}
codec::EntityType::Face3D(source) => {
let mut face = source.clone();
face.first_corner = flatten_point(face.first_corner)?;
face.second_corner = flatten_point(face.second_corner)?;
face.third_corner = flatten_point(face.third_corner)?;
face.fourth_corner = flatten_point(face.fourth_corner)?;
codec::EntityType::Face3D(face)
}
_ => return None,
};
(flattened != *entity).then_some(flattened)
}
fn flatten_curve_entity(entity: &codec::EntityType) -> Option<codec::EntityType> {
use codec::types::{Vector2, Vector3};
use kernel::geom2d::Curve;
match entity {
codec::EntityType::Ellipse(source)
if source.center.z == 0.0
&& source.major_axis.z == 0.0
&& source.normal == Vector3::UNIT_Z =>
{
return None;
}
codec::EntityType::LwPolyline(source)
if !z_axis_normal(source.normal)
&& (source.constant_width != 0.0
|| source
.vertices
.iter()
.any(|vertex| vertex.start_width != 0.0 || vertex.end_width != 0.0)) =>
{
return None;
}
codec::EntityType::Polyline2D(source)
if !z_axis_normal(source.normal)
&& (source.start_width != 0.0
|| source.end_width != 0.0
|| source
.vertices
.iter()
.any(|vertex| vertex.start_width != 0.0 || vertex.end_width != 0.0)) =>
{
return None;
}
codec::EntityType::LwPolyline(source) if source.vertices.len() < 2 => {
let plane = crate::entities::curve::ocs_plane(source.normal, source.elevation);
let mut polyline = source.clone();
for vertex in &mut polyline.vertices {
let point = flatten_array(plane.point_at([
vertex.location.x,
vertex.location.y,
]))?;
vertex.location = Vector2::new(point.x, point.y);
}
polyline.elevation = 0.0;
polyline.thickness = 0.0;
polyline.normal = Vector3::UNIT_Z;
return Some(codec::EntityType::LwPolyline(polyline));
}
codec::EntityType::Polyline2D(source) if source.vertices.len() < 2 => {
let plane = crate::entities::curve::ocs_plane(source.normal, source.elevation);
let mut polyline = source.clone();
for vertex in &mut polyline.vertices {
let point = flatten_array(plane.point_at([
vertex.location.x,
vertex.location.y,
]))?;
vertex.location = point;
}
polyline.elevation = 0.0;
polyline.thickness = 0.0;
polyline.normal = Vector3::UNIT_Z;
return Some(codec::EntityType::Polyline2D(polyline));
}
_ => {}
}
let curve = crate::entities::curve::entity_curve_xy(entity)?;
match (entity, curve) {
(codec::EntityType::Circle(source), Curve::Circle(curve)) => {
let mut circle = source.clone();
circle.center = Vector3::new(curve.centre[0], curve.centre[1], 0.0);
circle.radius = curve.radius;
circle.thickness = 0.0;
circle.normal = Vector3::UNIT_Z;
Some(codec::EntityType::Circle(circle))
}
(codec::EntityType::Arc(source), Curve::Arc(curve)) => {
let mut arc = source.clone();
arc.center = Vector3::new(curve.centre[0], curve.centre[1], 0.0);
arc.radius = curve.radius;
arc.start_angle = curve.start_angle;
arc.end_angle = curve.end_angle;
arc.thickness = 0.0;
arc.normal = Vector3::UNIT_Z;
Some(codec::EntityType::Arc(arc))
}
(codec::EntityType::Circle(source), Curve::Ellipse(curve)) => {
flattened_ellipse(source.common.clone(), curve)
}
(codec::EntityType::Arc(source), Curve::Ellipse(curve)) => {
flattened_ellipse(source.common.clone(), curve)
}
(codec::EntityType::Ellipse(source), Curve::Ellipse(curve)) => {
flattened_ellipse(source.common.clone(), curve)
}
(codec::EntityType::LwPolyline(source), Curve::Polyline(curve)) => {
if curve.vertices.len() != source.vertices.len() {
return None;
}
let mut polyline = source.clone();
for (target, projected) in polyline.vertices.iter_mut().zip(curve.vertices) {
target.location = Vector2::new(projected.position[0], projected.position[1]);
target.bulge = projected.bulge;
}
polyline.elevation = 0.0;
polyline.thickness = 0.0;
polyline.normal = Vector3::UNIT_Z;
Some(codec::EntityType::LwPolyline(polyline))
}
(codec::EntityType::Polyline2D(source), Curve::Polyline(curve)) => {
if curve.vertices.len() != source.vertices.len() {
return None;
}
let mut polyline = source.clone();
for (target, projected) in polyline.vertices.iter_mut().zip(curve.vertices) {
target.location =
Vector3::new(projected.position[0], projected.position[1], 0.0);
target.bulge = projected.bulge;
}
polyline.elevation = 0.0;
polyline.thickness = 0.0;
polyline.normal = Vector3::UNIT_Z;
Some(codec::EntityType::Polyline2D(polyline))
}
_ => None,
}
}
fn flattened_ellipse(
common: codec::entities::EntityCommon,
curve: kernel::geom2d::EllipseArc,
) -> Option<codec::EntityType> {
use codec::types::Vector3;
let geometry = curve.ellipse;
if !geometry.major_radius.is_finite() || geometry.major_radius <= 0.0 {
return None;
}
let mut ellipse = codec::entities::Ellipse::new();
ellipse.common = common;
ellipse.center = Vector3::new(geometry.centre[0], geometry.centre[1], 0.0);
ellipse.major_axis = Vector3::new(
geometry.major_axis[0] * geometry.major_radius,
geometry.major_axis[1] * geometry.major_radius,
0.0,
);
ellipse.minor_axis_ratio = geometry.minor_radius / geometry.major_radius;
ellipse.start_parameter = curve.start_parameter;
ellipse.end_parameter = curve.end_parameter;
ellipse.normal = Vector3::UNIT_Z;
Some(codec::EntityType::Ellipse(ellipse))
}
fn flatten_point(point: codec::types::Vector3) -> Option<codec::types::Vector3> {
flatten_array([point.x, point.y, point.z])
}
fn flatten_array(point: [f64; 3]) -> Option<codec::types::Vector3> {
use kernel::space::Plane;
let point = Plane::XY.point_at(Plane::XY.project(point)?);
Some(codec::types::Vector3::new(point[0], point[1], point[2]))
}
fn flatten_vector(vector: codec::types::Vector3) -> Option<codec::types::Vector3> {
use kernel::space::Plane;
let vector = Plane::XY.vector_at(Plane::XY.project_vector([
vector.x, vector.y, vector.z,
])?);
Some(codec::types::Vector3::new(
vector[0], vector[1], vector[2],
))
}
fn positive_z_normal(normal: codec::types::Vector3) -> bool {
normal.x == 0.0 && normal.y == 0.0 && normal.z > 0.0
}
fn z_axis_normal(normal: codec::types::Vector3) -> bool {
normal.x == 0.0 && normal.y == 0.0 && normal.z != 0.0
}
/// Selection-or-all candidate handles: every entity in the active space that
/// sits on an unlocked layer (verbatim from the FLATTEN command arm).
pub fn collect_flatten_handles(scene: &Scene) -> Vec<Handle> {
let sel = scene.selected_entities();
if sel.is_empty() {
let mut out = Vec::with_capacity(scene.document.entities().size_hint().0);
for handle in scene.document.entities().map(|e| e.common().handle) {
if scene.entity_belongs_to_active_space(handle) && !scene.is_layer_locked(handle) {
out.push(handle);
}
}
out
} else {
let mut out = Vec::with_capacity(sel.len());
for (handle, _) in sel {
if scene.entity_belongs_to_active_space(handle) && !scene.is_layer_locked(handle) {
out.push(handle);
}
}
out
}
}
/// The replacement entities for candidates that actually move (None for
/// unsupported / already-flat entities, exactly as the arm filtered them).
pub fn plan_flatten(scene: &Scene, handles: &[Handle]) -> Vec<EntityType> {
let mut out = Vec::with_capacity(handles.len());
for &handle in handles {
if let Some(entity) = scene.document.get_entity(handle) {
if let Some(flattened) = flatten_entity_z(entity) {
out.push(flattened);
}
}
}
out
}
/// Publish flattened replacements; returns how many entities moved.
pub fn apply_flatten_updates(scene: &mut Scene, updates: Vec<EntityType>) -> usize {
scene.update_entities(updates)
}
#[cfg(test)]
mod flatten_tests {
use super::flatten_entity_z;
use codec::types::Vector3;
use codec::EntityType;
#[test]
fn flattens_3d_polyline_vertices() {
let mut pl = codec::entities::Polyline3D::new();
for z in [5.0, -2.0, 7.5] {
pl.vertices
.push(codec::entities::Vertex3DPolyline::new(Vector3::new(1.0, 2.0, z)));
}
pl.elevation = 5.0;
let entity = EntityType::Polyline3D(pl);
let entity = flatten_entity_z(&entity).expect("projection");
let EntityType::Polyline3D(pl) = entity else {
panic!("wrong variant")
};
assert_eq!(pl.elevation, 0.0);
assert!(pl.vertices.iter().all(|v| v.position.z == 0.0));
// X/Y must survive the projection.
assert!(pl.vertices.iter().all(|v| v.position.x == 1.0 && v.position.y == 2.0));
}
#[test]
fn flattens_2d_polyline_vertices_and_elevation() {
let mut pl = codec::entities::Polyline2D::new();
pl.vertices
.push(codec::entities::Vertex2D::new(Vector3::new(3.0, 4.0, 9.0)));
pl.elevation = 9.0;
let entity = EntityType::Polyline2D(pl);
let entity = flatten_entity_z(&entity).expect("projection");
let EntityType::Polyline2D(pl) = entity else {
panic!("wrong variant")
};
assert_eq!(pl.elevation, 0.0);
assert_eq!(pl.vertices[0].location.z, 0.0);
assert_eq!(pl.vertices[0].location.x, 3.0);
}
#[test]
fn flattens_solid_corners() {
let solid = codec::entities::Solid::new(
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 2.0),
Vector3::new(1.0, 1.0, 3.0),
Vector3::new(0.0, 1.0, 4.0),
);
let entity = EntityType::Solid(solid);
let entity = flatten_entity_z(&entity).expect("projection");
let EntityType::Solid(s) = entity else {
panic!("wrong variant")
};
assert_eq!(
[s.first_corner.z, s.second_corner.z, s.third_corner.z, s.fourth_corner.z],
[0.0; 4]
);
}
#[test]
fn reports_unsupported_entities_as_not_moved() {
let entity = EntityType::Ray(codec::entities::Ray::new(
Vector3::new(0.0, 0.0, 5.0),
Vector3::new(1.0, 0.0, 0.0),
));
assert!(flatten_entity_z(&entity).is_none());
}
#[test]
fn still_flattens_a_line() {
let mut line = codec::entities::Line::new();
line.start = Vector3::new(0.0, 0.0, 3.0);
line.end = Vector3::new(1.0, 1.0, 4.0);
let entity = EntityType::Line(line);
let entity = flatten_entity_z(&entity).expect("projection");
let EntityType::Line(l) = entity else { panic!("wrong variant") };
assert_eq!((l.start.z, l.end.z), (0.0, 0.0));
}
#[test]
fn already_flat_line_is_not_moved() {
let mut line = codec::entities::Line::new();
line.start = Vector3::new(0.0, 0.0, 0.0);
line.end = Vector3::new(1.0, 1.0, 0.0);
let entity = EntityType::Line(line);
assert!(flatten_entity_z(&entity).is_none());
}
#[test]
fn tilted_circle_projects_to_ellipse() {
let mut circle = codec::entities::Circle::from_center_radius(
Vector3::new(2.0, 3.0, 4.0),
5.0,
);
circle.normal = Vector3::new(0.0, 0.6, 0.8);
circle.thickness = 2.0;
let entity = EntityType::Circle(circle);
let projected = flatten_entity_z(&entity).expect("projection");
let EntityType::Ellipse(ellipse) = projected else {
panic!("wrong variant")
};
assert_eq!(ellipse.center.z, 0.0);
assert_eq!(ellipse.major_axis.z, 0.0);
assert_eq!(ellipse.normal, Vector3::UNIT_Z);
assert!(ellipse.minor_axis_ratio < 1.0);
}
#[test]
fn tilted_solid_uses_world_projection() {
let mut solid = codec::entities::Solid::new(
Vector3::new(0.0, 0.0, 2.0),
Vector3::new(1.0, 0.0, 2.0),
Vector3::new(1.0, 1.0, 2.0),
Vector3::new(0.0, 1.0, 2.0),
);
solid.normal = Vector3::new(0.0, 0.6, 0.8);
solid.thickness = 3.0;
let entity = EntityType::Solid(solid);
let projected = flatten_entity_z(&entity).expect("projection");
let EntityType::Solid(solid) = projected else {
panic!("wrong variant")
};
assert_eq!(solid.normal, Vector3::UNIT_Z);
assert_eq!(solid.thickness, 0.0);
assert_eq!(
[
solid.first_corner.z,
solid.second_corner.z,
solid.third_corner.z,
solid.fourth_corner.z,
],
[0.0; 4]
);
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
//! Adapters between the editing commands and `cadkernel`'s plane geometry.
//! Adapters between the editing commands and `opencadkernel`'s plane geometry.
//!
//! The kernel takes points as `[f64; 2]` and hands tessellation back as
//! `[f64; 3]`. The commands here grew up passing loose `x, y` scalars and
@@ -11,11 +11,11 @@
//! resident render path splits the `f64` into a high/low pair instead. Only a
//! caller knows which of the two it is.
use cadkernel::geom2d::{self, Ellipse};
use kernel::geom2d::{self, Ellipse};
/// Re-exported unchanged: these already speak in plain `f64`, so there is no
/// call-shape difference for this module to absorb.
pub use cadkernel::geom2d::{arc_parameter, lerp, normalize_angle};
pub use kernel::geom2d::{arc_parameter, ellipse_closest_parameter, lerp, normalize_angle};
/// Preview geometry keeps the density the commands have always used.
const SEGMENTS_PER_RADIAN: f64 = geom2d::DEFAULT_SEGMENTS_PER_RADIAN;
+28 -28
View File
@@ -9,8 +9,8 @@
//
// Workflow: select objects then press Enter to join.
use acadrust::types::{Vector2, Vector3};
use acadrust::{EntityType, Handle};
use codec::types::{Vector2, Vector3};
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
@@ -48,7 +48,7 @@ impl CadCommand for JoinCommand {
fn on_text_input(&mut self,text:&str) -> Option<CmdResult> {
if matches!(text.trim().to_ascii_uppercase().as_str(),"L"|"CLOSE") {
if let Some((handle,EntityType::Arc(arc)))=&self.source {
let mut circle=acadrust::entities::Circle::new();
let mut circle=codec::entities::Circle::new();
circle.common=arc.common.clone();circle.common.handle=Handle::NULL;
circle.center=arc.center.clone();circle.normal=arc.normal.clone();circle.radius=arc.radius;circle.thickness=arc.thickness;
return Some(CmdResult::ReplaceMany(vec![(*handle,vec![EntityType::Circle(circle)])],Vec::new()));
@@ -95,23 +95,23 @@ pub fn join_to_source(source: &EntityType, candidates: &[(Handle, &EntityType)])
let next = match (&result, *candidate) {
(EntityType::Line(a), EntityType::Line(b)) => {
let point = |p: &Vector3| [p.x,p.y,p.z];
cadkernel::space::source_join::join_collinear_lines([point(&a.start),point(&a.end)],[point(&b.start),point(&b.end)],JOIN_EPS).map(|span| {
kernel::space::source_join::join_collinear_lines([point(&a.start),point(&a.end)],[point(&b.start),point(&b.end)],JOIN_EPS).map(|span| {
let mut line = a.clone(); line.start = Vector3::new(span[0][0],span[0][1],span[0][2]); line.end = Vector3::new(span[1][0],span[1][1],span[1][2]); EntityType::Line(line)
})
}
(EntityType::Arc(a), EntityType::Arc(b)) => {
let data = |a: &acadrust::entities::Arc| ([a.center.x,a.center.y,a.center.z],[a.normal.x,a.normal.y,a.normal.z],a.radius,[a.start_angle,a.end_angle]);
cadkernel::space::source_join::join_cocircular_arcs(data(a),data(b),JOIN_EPS).map(|span| {
let data = |a: &codec::entities::Arc| ([a.center.x,a.center.y,a.center.z],[a.normal.x,a.normal.y,a.normal.z],a.radius,[a.start_angle,a.end_angle]);
kernel::space::source_join::join_cocircular_arcs(data(a),data(b),JOIN_EPS).map(|span| {
if span[1]-span[0] >= std::f64::consts::TAU {
let mut circle = acadrust::entities::Circle::new(); circle.common = a.common.clone(); circle.center = a.center.clone(); circle.normal = a.normal.clone(); circle.radius = a.radius; circle.thickness = a.thickness; EntityType::Circle(circle)
let mut circle = codec::entities::Circle::new(); circle.common = a.common.clone(); circle.center = a.center.clone(); circle.normal = a.normal.clone(); circle.radius = a.radius; circle.thickness = a.thickness; EntityType::Circle(circle)
} else { let mut arc = a.clone(); arc.start_angle = span[0]; arc.end_angle = span[1].rem_euclid(std::f64::consts::TAU); EntityType::Arc(arc) }
})
}
(EntityType::Spline(source), candidate) => {
let curve = |s:&acadrust::entities::Spline| {
let curve = |s:&codec::entities::Spline| {
if s.flags.closed || s.flags.periodic {return None;}
if s.control_points.is_empty() && s.fit_points.len() >= 2 {
use cadkernel::space::{NurbsCurve3, Parameterization};
use kernel::space::{NurbsCurve3, Parameterization};
let points: Vec<_> = s.fit_points.iter().map(|point| [point.x,point.y,point.z]).collect();
let parameterization = match s.knot_parameterization {
1 => Parameterization::Centripetal, 2 => Parameterization::Uniform, _ => Parameterization::Chord,
@@ -122,22 +122,22 @@ pub fn join_to_source(source: &EntityType, candidates: &[(Handle, &EntityType)])
.compact_knots(s.control_tolerance.max(1e-9));
}
let weights=if s.weights.is_empty(){vec![1.0;s.control_points.len()]}else{s.weights.clone()};
cadkernel::space::NurbsCurve3::new_strict(s.degree as usize,s.control_points.iter().map(|p|[p.x,p.y,p.z]).collect(),s.knots.clone(),weights)
kernel::space::NurbsCurve3::new_strict(s.degree as usize,s.control_points.iter().map(|p|[p.x,p.y,p.z]).collect(),s.knots.clone(),weights)
};
curve(source).and_then(|a| {
let b=match candidate {
EntityType::Spline(s)=>curve(s),
EntityType::Line(line)=>cadkernel::space::source_join::line_as_nurbs([[line.start.x,line.start.y,line.start.z],[line.end.x,line.end.y,line.end.z]],a.degree()),
EntityType::Line(line)=>kernel::space::source_join::line_as_nurbs([[line.start.x,line.start.y,line.start.z],[line.end.x,line.end.y,line.end.z]],a.degree()),
_=>None,
}?;
let joined=cadkernel::space::source_join::join_nurbs_curves(&a,&b,JOIN_EPS)?;
let joined=kernel::space::source_join::join_nurbs_curves(&a,&b,JOIN_EPS)?;
let mut spline=source.clone();
spline.degree=joined.degree() as i32;
spline.control_points=joined.control_points().iter().map(|p|Vector3::new(p[0],p[1],p[2])).collect();
spline.knots=joined.knots().to_vec();spline.weights=joined.weights().to_vec();
spline.fit_points.clear();spline.flags.rational=joined.is_rational();
spline.dwg_flags1 &= !1;spline.dxf_flags &= !(32 | 1024);
spline.flags.planar=cadkernel::space::are_coplanar(joined.control_points(),&[]);
spline.flags.planar=kernel::space::are_coplanar(joined.control_points(),&[]);
spline.begin_tangent=Vector3::new(0.0,0.0,0.0);spline.end_tangent=Vector3::new(0.0,0.0,0.0);
Some(EntityType::Spline(spline))
})
@@ -261,7 +261,7 @@ fn segs_of(e: &EntityType) -> Option<Vec<Seg>> {
if normal.x.abs() > 1e-12 || normal.y.abs() > 1e-12 {
return None;
}
let cadkernel::geom2d::Curve::Polyline(curve) =
let kernel::geom2d::Curve::Polyline(curve) =
crate::entities::curve::entity_curve_xy(&EntityType::Polyline2D(p.clone()))?
else {
return None;
@@ -327,7 +327,7 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
// An open run of collinear straight segments collapses back to one Line.
if !closed && !has_arc && chain.iter().all(|segment| segment.widths == Some((0.0,0.0))) && !matches!(entities[0].1, EntityType::LwPolyline(_) | EntityType::Polyline2D(_)) && is_collinear(&verts) {
let mut line = acadrust::entities::Line::new();
let mut line = codec::entities::Line::new();
line.common = common;
line.common.handle = Handle::NULL;
line.start = v3(verts.first().unwrap().0);
@@ -342,10 +342,10 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
return None;
}
let flipped = thickness != 0.0 && normal.z < 0.0;
let lw_verts: Vec<acadrust::entities::LwVertex> = verts
let lw_verts: Vec<codec::entities::LwVertex> = verts
.iter().enumerate()
.map(|(index,(p, bulge))| {
let mut v = acadrust::entities::LwVertex::new(Vector2::new(
let mut v = codec::entities::LwVertex::new(Vector2::new(
if flipped { -p.x } else { p.x },
p.y,
));
@@ -356,7 +356,7 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
v
})
.collect();
let mut pl = acadrust::entities::LwPolyline::new();
let mut pl = codec::entities::LwPolyline::new();
pl.common = common;
pl.common.handle = Handle::NULL;
pl.vertices = lw_verts;
@@ -372,8 +372,8 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
let output = crate::entities::curve::ocs_plane(source.normal.clone(),source.elevation);
polyline.vertices = pl.vertices.iter().map(|vertex| {
let point = output.project(input.point_at([vertex.location.x,vertex.location.y]))?;
let mut result = acadrust::entities::polyline::Vertex2D::new(Vector3::new(point[0],point[1],source.elevation));
result.bulge = if cadkernel::space::Vec3::from(input.normal()?).dot(cadkernel::space::Vec3::from(output.normal()?)) < 0.0 {-vertex.bulge} else {vertex.bulge}; result.start_width = vertex.start_width; result.end_width = vertex.end_width;
let mut result = codec::entities::polyline::Vertex2D::new(Vector3::new(point[0],point[1],source.elevation));
result.bulge = if kernel::space::Vec3::from(input.normal()?).dot(kernel::space::Vec3::from(output.normal()?)) < 0.0 {-vertex.bulge} else {vertex.bulge}; result.start_width = vertex.start_width; result.end_width = vertex.end_width;
Some(result)
}).collect::<Option<Vec<_>>>()?;
polyline.start_width = 0.0; polyline.end_width = 0.0;
@@ -397,12 +397,12 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
if thickness != 0.0 {
return None;
}
let mut pl = acadrust::entities::Polyline3D::new();
let mut pl = codec::entities::Polyline3D::new();
pl.common = common;
pl.common.handle = Handle::NULL;
pl.vertices = verts
.iter()
.map(|(p, _)| acadrust::entities::Vertex3DPolyline::new(v3(*p)))
.map(|(p, _)| codec::entities::Vertex3DPolyline::new(v3(*p)))
.collect();
if closed {
pl.close();
@@ -460,8 +460,8 @@ fn stitch(mut segs: Vec<Seg>) -> Option<(Vec<Seg>, bool)> {
#[cfg(test)]
mod join_tests {
use super::*;
use acadrust::entities::{Line as LineEnt, LwVertex};
use acadrust::Handle;
use codec::entities::{Line as LineEnt, LwVertex};
use codec::Handle;
fn line(x0: f64, y0: f64, x1: f64, y1: f64, thickness: f64) -> EntityType {
let mut l = LineEnt::new();
@@ -472,7 +472,7 @@ mod join_tests {
}
fn lw_2pts(p0: (f64, f64), p1: (f64, f64), thickness: f64) -> EntityType {
let mut pl = acadrust::entities::LwPolyline::new();
let mut pl = codec::entities::LwPolyline::new();
pl.vertices = vec![
LwVertex::new(Vector2::new(p0.0, p0.1)),
LwVertex::new(Vector2::new(p1.0, p1.1)),
@@ -554,7 +554,7 @@ mod join_tests {
fn source_join_elevates_splines_and_clears_fit_representation() {
let source_handle = Handle::new(21);
let candidate_handle = Handle::new(22);
let mut source = acadrust::entities::Spline::new();
let mut source = codec::entities::Spline::new();
source.common.handle = source_handle;
source.degree = 1;
source.control_points = vec![
@@ -565,7 +565,7 @@ mod join_tests {
source.fit_points = vec![Vector3::new(0.0, 0.0, 0.0)];
source.dwg_flags1 = 1;
source.dxf_flags = 32 | 1024;
let mut candidate = acadrust::entities::Spline::new();
let mut candidate = codec::entities::Spline::new();
candidate.degree = 3;
candidate.control_points = vec![
Vector3::new(1.0, 0.0, 0.0),
@@ -595,7 +595,7 @@ mod join_tests {
#[test]
fn arc_source_accepts_the_typed_close_option() {
let handle = Handle::new(31);
let mut arc = acadrust::entities::Arc::new();
let mut arc = codec::entities::Arc::new();
arc.common.handle = handle;
arc.center = Vector3::new(2.0, 3.0, 4.0);
arc.radius = 5.0;
+12 -12
View File
@@ -8,12 +8,12 @@
// The entity is modified at whichever end is closest to the pick point.
use crate::modules::draw::modify::spline_ops::{spline_cut, spline_to_nurbs};
use acadrust::entities::{
use codec::entities::{
Spline as SplineEnt,
};
use cadkernel::geom2d::Curve;
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use kernel::geom2d::Curve;
use codec::types::Vector3;
use codec::{EntityType, Handle};
use glam::{DVec3, Vec3};
use crate::t;
@@ -249,7 +249,7 @@ pub fn lengthen_entity(entity: &EntityType, pick_pt: Vec3, mode: &LenMode) -> Op
}
fn lengthen_entity_precise(entity: &EntityType, pick: DVec3, mode: &LenMode) -> Option<EntityType> {
use cadkernel::space::lengthen::{LengthChange, lengthen_line, lengthen_arc};
use kernel::space::lengthen::{LengthChange, lengthen_line, lengthen_arc};
let change = match mode {
LenMode::Delta(value) => LengthChange::Delta(*value),
LenMode::Total(value) => LengthChange::Total(*value),
@@ -279,7 +279,7 @@ fn lengthen_entity_precise(entity: &EntityType, pick: DVec3, mode: &LenMode) ->
}
EntityType::Ellipse(ellipse) => {
let curve = crate::entities::curve::ellipse_curve(ellipse)?;
let (start, end) = cadkernel::space::lengthen::lengthen_ellipse(&curve, pick.to_array(), change)?;
let (start, end) = kernel::space::lengthen::lengthen_ellipse(&curve, pick.to_array(), change)?;
let mut result = ellipse.clone();
result.start_parameter = start;
result.end_parameter = end;
@@ -288,7 +288,7 @@ fn lengthen_entity_precise(entity: &EntityType, pick: DVec3, mode: &LenMode) ->
EntityType::Spline(s) => lengthen_spline(s, pick.as_vec3(), mode),
EntityType::LwPolyline(polyline) => {
let curve = crate::entities::curve::lwpolyline_curve(polyline)?;
let vertices = cadkernel::space::lengthen::lengthen_polyline(&curve, pick.to_array(), change)?;
let vertices = kernel::space::lengthen::lengthen_polyline(&curve, pick.to_array(), change)?;
let mut result = polyline.clone();
result.vertices = vertices.into_iter().map(|vertex| {
let mut value = polyline.vertices[vertex.source].clone();
@@ -365,7 +365,7 @@ mod tests {
use super::*;
fn spatial_line() -> EntityType {
EntityType::Line(acadrust::entities::Line::from_points(
EntityType::Line(codec::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 4.0),
))
@@ -422,7 +422,7 @@ mod tests {
#[test]
fn circular_arc_accepts_total_angle_in_radians() {
let arc = acadrust::entities::Arc::from_center_radius_angles(
let arc = codec::entities::Arc::from_center_radius_angles(
Vector3::ZERO,
2.0,
0.0,
@@ -447,13 +447,13 @@ mod tests {
#[test]
fn polyline_lengthening_preserves_plane_and_interpolates_widths() {
let mut polyline = acadrust::entities::LwPolyline::new();
let mut polyline = codec::entities::LwPolyline::new();
polyline.normal = Vector3::new(0.0, 1.0, 0.0);
polyline.elevation = 5.0;
let mut start = acadrust::entities::LwVertex::from_coords(0.0, 0.0);
let mut start = codec::entities::LwVertex::from_coords(0.0, 0.0);
start.start_width = 2.0;
start.end_width = 4.0;
polyline.vertices = vec![start, acadrust::entities::LwVertex::from_coords(10.0, 0.0)];
polyline.vertices = vec![start, codec::entities::LwVertex::from_coords(10.0, 0.0)];
let entity = EntityType::LwPolyline(polyline.clone());
let pick = DVec3::from_array(crate::entities::curve::lwpolyline_curve(&polyline)
.unwrap().point_at(1.0));
+1 -1
View File
@@ -8,7 +8,7 @@
// No → keep the original, add a mirrored copy
// Yes → flip the original in place (no copy kept)
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
+18 -18
View File
@@ -1,8 +1,8 @@
use acadrust::entities::MLine;
use acadrust::objects::MLineStyle;
use acadrust::{EntityType, Handle};
use cadkernel::geom2d::{Curve, Line};
use cadkernel::space::{PlanarCurve, Plane, Vec3 as KernelVec3};
use codec::entities::MLine;
use codec::objects::MLineStyle;
use codec::{EntityType, Handle};
use kernel::geom2d::{Curve, Line};
use kernel::space::{PlanarCurve, Plane, Vec3 as KernelVec3};
use glam::DVec3;
use rustc_hash::FxHashMap as HashMap;
@@ -95,7 +95,7 @@ impl MlineEditCommand {
}
let insert = segment + 1;
let mut vertex = mline.vertices[segment].clone();
vertex.position = acadrust::types::Vector3::new(
vertex.position = codec::types::Vector3::new(
projected.x,
projected.y,
projected.z,
@@ -710,7 +710,7 @@ fn snapshot_segment_data(
mline: &MLine,
segment: usize,
normal: DVec3,
) -> Option<Vec<(acadrust::entities::MLineSegment, f64)>> {
) -> Option<Vec<(codec::entities::MLineSegment, f64)>> {
(0..mline.style_element_count)
.map(|element| {
let endpoints = element_segment(mline, segment, element)?;
@@ -726,7 +726,7 @@ fn restore_shifted_segment_data(
mline: &mut MLine,
segment: usize,
normal: DVec3,
source: Vec<(acadrust::entities::MLineSegment, f64)>,
source: Vec<(codec::entities::MLineSegment, f64)>,
moved_start: bool,
) {
for (element, (source, source_length)) in source.into_iter().enumerate() {
@@ -1057,7 +1057,7 @@ fn move_closest_end(
};
let peripheral = peripheral_segment
.and_then(|segment| snapshot_segment_data(mline, segment, normal).map(|data| (segment, data)));
mline.vertices[index].position = acadrust::types::Vector3::new(
mline.vertices[index].position = codec::types::Vector3::new(
intersection.x,
intersection.y,
intersection.z,
@@ -1096,11 +1096,11 @@ fn segment_intersection(
second: (DVec3, DVec3),
normal: DVec3,
) -> Option<(DVec3, f64, f64, f64)> {
let axis = (cadkernel::space::Vec3::from(first.1.to_array())
- cadkernel::space::Vec3::from(first.0.to_array()))
let axis = (kernel::space::Vec3::from(first.1.to_array())
- kernel::space::Vec3::from(first.0.to_array()))
.normalize()?
.to_array();
let plane = cadkernel::space::Plane::orthonormal(
let plane = kernel::space::Plane::orthonormal(
first.0.to_array(),
axis,
normal.to_array(),
@@ -1111,15 +1111,15 @@ fn segment_intersection(
second.0.to_array(),
second.1.to_array(),
];
let tolerance = cadkernel::space::coplanarity_tolerance(&points);
let tolerance = kernel::space::coplanarity_tolerance(&points);
if !plane.contains(points[2], tolerance) || !plane.contains(points[3], tolerance) {
return None;
}
let p = cadkernel::geom2d::Vec2::from(plane.project(points[0])?);
let q = cadkernel::geom2d::Vec2::from(plane.project(points[2])?);
let r = cadkernel::geom2d::Vec2::from(plane.project(points[1])?) - p;
let s = cadkernel::geom2d::Vec2::from(plane.project(points[3])?) - q;
let (t, u) = cadkernel::geom2d::intersect::line_line(
let p = kernel::geom2d::Vec2::from(plane.project(points[0])?);
let q = kernel::geom2d::Vec2::from(plane.project(points[2])?);
let r = kernel::geom2d::Vec2::from(plane.project(points[1])?) - p;
let s = kernel::geom2d::Vec2::from(plane.project(points[3])?) - q;
let (t, u) = kernel::geom2d::intersect::line_line(
p.to_array(),
r.to_array(),
q.to_array(),
+1
View File
@@ -6,6 +6,7 @@ pub mod break_cmd;
pub mod copy;
pub mod delete;
mod entity_index;
pub mod flatten;
pub mod explode;
pub mod fillet;
pub mod geom;
+7 -7
View File
@@ -1,6 +1,6 @@
use acadrust::{CadDocument, EntityType, Handle};
use codec::{CadDocument, EntityType, Handle};
use glam::DVec3;
use acadrust::nested_copy::{NestedCopyMode, NestedCopySymbolNames};
use codec::nested_copy::{NestedCopyMode, NestedCopySymbolNames};
use std::collections::{HashMap, HashSet};
use crate::command::{CadCommand, CmdOption, CmdResult, EntityTransform};
use crate::entities::traits::EntityTypeOps;
@@ -101,13 +101,13 @@ impl CadCommand for NcopyCommand {
let nearest = entities.iter().enumerate().filter_map(|(index, entity)| {
let distance = if let Some(curve) = crate::entities::curve::entity_curve(entity) {
let projected = curve.plane.project([point.x, point.y, point.z])?;
cadkernel::geom2d::closest_point(&curve.curve, projected).distance
kernel::geom2d::closest_point(&curve.curve, projected).distance
.hypot(curve.plane.distance_to(point.to_array())?)
} else {
let path = self.hit_paths.get(&handle)?.get(index)?;
let point = cadkernel::space::Vec3::from(point.to_array());
let point = kernel::space::Vec3::from(point.to_array());
if path.len() == 1 {
point.distance(cadkernel::space::Vec3::from(path[0]))
point.distance(kernel::space::Vec3::from(path[0]))
} else {
path.windows(2).filter(|segment| segment.iter().flatten().all(|v| v.is_finite()))
.map(|segment| point.distance_to_segment(segment[0].into(), segment[1].into()))
@@ -197,8 +197,8 @@ impl CadCommand for NcopyCommand {
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::Line;
use acadrust::types::Vector3;
use codec::entities::Line;
use codec::types::Vector3;
#[test]
fn placement_moves_only_geometry_and_keeps_source_style() {
+335 -119
View File
@@ -9,19 +9,23 @@
// 2. Pick object to offset (Line, Arc, Circle, LwPolyline)
// 3. Pick a point on the side to offset toward, or choose Multiple
// to keep offsetting the newly created result at the same distance
//
// With objects already selected, the command opens at step 3 instead, the
// preview following the cursor. At step 3 a typed distance places the
// offset at once on the cursor's side; a negative one, on the other side.
use crate::entities::curve::{entity_curve, lwpolyline_world_xy};
use crate::modules::draw::modify::spline_ops::spline_sample_xy;
use acadrust::entities::LwVertex;
use acadrust::entities::{
use codec::entities::LwVertex;
use codec::entities::{
Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline,
Spline as SplineEnt, XLine as XLineEnt,
};
use acadrust::{EntityType, Handle};
use codec::{EntityType, Handle};
// Polyline offsetting, and the angle normalisation that goes with it, come
// from the kernel; only the entity conversion stays here.
use cadkernel::geom2d::nurbs::clamped_uniform_knots;
use cadkernel::geom2d::{
use kernel::geom2d::nurbs::clamped_uniform_knots;
use kernel::geom2d::{
offset_polyline, Polyline as KernelPolyline, PolylineVertex as KernelVertex,
};
use glam::{DVec3, Vec3};
@@ -269,7 +273,7 @@ fn offset_spline(spl: &SplineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType
})?;
// Offset each sample point along the local normal.
let offset_pts: Vec<acadrust::types::Vector3> = pts
let offset_pts: Vec<codec::types::Vector3> = pts
.iter()
.enumerate()
.map(|(i, p)| {
@@ -290,14 +294,14 @@ fn offset_spline(spl: &SplineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType
let nx = -dy / len; // left perpendicular
let ny = dx / len;
let z = spl.control_points.first().map(|v| v.z).unwrap_or(0.0);
acadrust::types::Vector3::new(p[0] + sign * nx * dist, p[1] + sign * ny * dist, z)
codec::types::Vector3::new(p[0] + sign * nx * dist, p[1] + sign * ny * dist, z)
})
.collect();
let _ = ts_knot;
// Build a new spline from the offset control points (treat sample pts as fit pts → ctrl pts).
let degree = spl.degree.max(1) as usize;
let new_ctrl: Vec<acadrust::types::Vector3> = offset_pts;
let new_ctrl: Vec<codec::types::Vector3> = offset_pts;
let n_ctrl = new_ctrl.len();
let mut new_spl = spl.clone();
new_spl.common.handle = Handle::NULL;
@@ -331,13 +335,18 @@ fn compute_offsets(entity: &EntityType, dist: f64, side_pt: Vec3) -> Vec<EntityT
// distance for every supported entity type.
fn perp_distance(entity: &EntityType, pt: Vec3) -> f64 {
closest_on_wire(entity, pt).map_or(0.0, |(_, d)| d)
}
/// Nearest point of the entity outline to `pt`, and its distance.
fn closest_on_wire(entity: &EntityType, pt: Vec3) -> Option<([f64; 2], f64)> {
let pts = entity_wire_pts(entity);
if pts.len() < 2 {
return 0.0;
return None;
}
let px = pt.x as f64;
let py = pt.y as f64;
let mut best = f64::INFINITY;
let mut best = ([px, py], f64::INFINITY);
for w in pts.windows(2) {
let ax = w[0][0] as f64;
let ay = w[0][1] as f64;
@@ -354,17 +363,41 @@ fn perp_distance(entity: &EntityType, pt: Vec3) -> f64 {
let cx = ax + t * dx;
let cy = ay + t * dy;
let d = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
if d < best {
best = d;
if d < best.1 {
best = ([cx, cy], d);
}
}
best
Some(best)
}
// ── Opposite side ─────────────────────────────────────────────────────────
//
// A negative typed distance offsets away from the cursor. Rather than teach
// every offset function a sign, the cursor is mirrored through its nearest
// point on the outline, which puts it on the other side for every type: the
// other half-plane of a line, inside instead of outside a circle or closed
// loop. The mirror stops at half the offset distance so that, in a narrow
// part of a shape, it cannot cross the far wall and come back to the
// cursor's own side.
fn far_side(entity: &EntityType, pt: Vec3, dist: f64) -> Option<Vec3> {
let ([qx, qy], d) = closest_on_wire(entity, pt)?;
if d < 1e-9 {
// On the outline itself: there is no side to be opposite to.
return None;
}
let step = d.min(dist * 0.5) / d;
Some(Vec3::new(
(qx + (qx - pt.x as f64) * step) as f32,
(qy + (qy - pt.y as f64) * step) as f32,
pt.z,
))
}
// ── Wire preview points ─────────────────────────────────────────────────────
/// Preview density, matching the figure the drawn wires use.
const PREVIEW_SEGMENTS_PER_RADIAN: f64 = cadkernel::geom2d::DEFAULT_SEGMENTS_PER_RADIAN;
const PREVIEW_SEGMENTS_PER_RADIAN: f64 = kernel::geom2d::DEFAULT_SEGMENTS_PER_RADIAN;
/// Preview points for an entity, from its own curve.
///
@@ -440,9 +473,13 @@ pub struct OffsetCommand {
/// Unlike the command's opening snapshot, this also includes objects
/// created by earlier offsets while the command remains active.
picked: Option<EntityType>,
/// Pre-selected offsettable objects (pick-first, #422); consumed when the
/// distance step resolves.
preselected: Vec<EntityType>,
/// Last cursor position the preview saw. A distance typed at the side
/// step places the offset relative to it.
cursor: Option<DVec3>,
/// The command opened on pre-selected objects (#422) and has made nothing
/// yet: Enter takes the last distance, and Erase / Layer, which the
/// skipped distance step would have offered, are offered here.
drag_start: bool,
/// A Yes/No or Current/Source answer is being collected for this option.
awaiting: Option<Await>,
/// Erase option: replace the source object with its offset.
@@ -485,7 +522,8 @@ impl OffsetCommand {
all_entities,
entity_index,
picked: None,
preselected: Vec::new(),
cursor: None,
drag_start: false,
awaiting: None,
erase_source: false,
layer_current: false,
@@ -494,38 +532,28 @@ impl OffsetCommand {
}
}
/// Pick-first flow (#422): the distance step still comes first, then the
/// pre-selected objects go straight to the side step.
/// Pick-first flow (#422): the objects are already chosen, so the command
/// opens at the side step with the preview following the cursor. Clicking
/// offsets through the cursor; typing a distance places it at that
/// distance; Enter repeats the last distance (#418).
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
step: Step::Distance,
all_entities,
entity_index,
picked: None,
preselected: targets,
awaiting: None,
erase_source: false,
layer_current: false,
targets_are_sources: false,
made: 0,
let mut cmd = Self::new(all_entities);
if !targets.is_empty() {
cmd.step = Step::PickSide {
targets,
locked: None,
multiple: false,
};
cmd.targets_are_sources = true;
cmd.drag_start = true;
}
cmd
}
/// Leave the distance step with the given mode (Some = locked distance,
/// None = through): pre-selected objects jump to the side step, otherwise
/// the object-pick loop starts.
/// None = through) for the object-pick loop.
fn advance_from_distance(&mut self, locked: Option<f64>) -> CmdResult {
if self.preselected.is_empty() {
self.step = Step::SelectObject { locked };
} else {
self.step = Step::PickSide {
targets: std::mem::take(&mut self.preselected),
locked,
multiple: false,
};
self.targets_are_sources = true;
}
self.step = Step::SelectObject { locked };
CmdResult::NeedPoint
}
@@ -554,6 +582,73 @@ impl OffsetCommand {
t!("OFFSET distance = %{d}", d = d).into_owned()
)
}
/// Offset the side step's targets with `pt` choosing the side: toward it,
/// or away from it when `flip` is set. Each target offsets by its own
/// through-distance, or by the locked magnitude.
fn place(&mut self, pt: DVec3, flip: bool) -> CmdResult {
let (locked, targets, multiple) = match &self.step {
Step::PickSide {
locked,
targets,
multiple,
} => (*locked, targets.clone(), *multiple),
_ => return CmdResult::NeedPoint,
};
let per_target: Vec<(Handle, Vec<EntityType>)> = targets
.iter()
.filter_map(|entity| {
let mag = locked.unwrap_or_else(|| perp_distance(entity, pt.as_vec3()));
if mag < 1e-9 {
return None;
}
let side = if flip {
far_side(entity, pt.as_vec3(), mag)?
} else {
pt.as_vec3()
};
let offsets = compute_offsets(entity, mag, side);
(!offsets.is_empty()).then(|| (entity.common().handle, offsets))
})
.collect();
let mut news: Vec<EntityType> = per_target
.iter()
.flat_map(|(_, offsets)| offsets.iter().cloned())
.collect();
if news.is_empty() {
return CmdResult::NeedPoint;
}
self.made += 1;
self.drag_start = false;
// Erase option: the offset replaces its source object. Only the
// objects picked from the drawing qualify — a Multiple chain's
// targets are the offsets just made, which are not replaced again.
let erase = self.erase_source && self.targets_are_sources;
self.targets_are_sources = false;
if multiple {
// Multiple mode chains from the result just created, matching
// OFFSET's repeated fixed-distance behavior (parallel/concentric
// series instead of duplicate copies at the first offset).
self.step = Step::PickSide {
targets: news.clone(),
locked,
multiple: true,
};
} else {
// Classic loop (#418): commit this offset and go back to the object
// pick at the same distance, until Enter / Esc finishes.
self.step = Step::SelectObject { locked };
}
if erase {
return CmdResult::ReplaceManyContinue(per_target);
}
if news.len() == 1 {
CmdResult::CommitEntity(news.pop().unwrap())
} else {
CmdResult::CommitEntities(news)
}
}
}
impl CadCommand for OffsetCommand {
@@ -602,6 +697,23 @@ impl CadCommand for OffsetCommand {
} else {
std::borrow::Cow::Borrowed("")
};
if self.drag_start {
let d = format!("{:.4}", defaults::get_offset_dist());
return if *multiple {
t!(
"OFFSET%{n} Multiple Click through point or type distance, negative for the other side [Erase/Layer] <%{d}>:",
n = n,
d = d
)
} else {
t!(
"OFFSET%{n} Click through point or type distance, negative for the other side [Multiple/Erase/Layer] <%{d}>:",
n = n,
d = d
)
}
.into_owned();
}
match (locked, multiple) {
(Some(d), false) => {
let d = format!("{:.4}", d);
@@ -658,6 +770,16 @@ impl CadCommand for OffsetCommand {
opts.push(CmdOption::enter("Exit"));
opts
}
Step::PickSide { multiple, .. } if self.drag_start => {
let mut opts = Vec::new();
if !multiple {
opts.push(CmdOption::new("Multiple", "M"));
}
opts.push(CmdOption::new("Erase", "E"));
opts.push(CmdOption::new("Layer", "L"));
opts.push(CmdOption::enter(&format!("{:.4}", defaults::get_offset_dist())));
opts
}
Step::PickSide { multiple, .. } => {
let mut opts = Vec::new();
if !multiple {
@@ -777,19 +899,23 @@ impl CadCommand for OffsetCommand {
{
return self.undo_last();
}
// Erase / Layer belong to the distance step, or to the side step when
// a pre-selection skipped the distance step.
if matches!(self.step, Step::Distance) || self.drag_start {
if upper == "E" || upper == "ERASE" {
self.awaiting = Some(Await::Erase);
return Some(CmdResult::NeedPoint);
}
if upper == "L" || upper == "LAYER" {
self.awaiting = Some(Await::Layer);
return Some(CmdResult::NeedPoint);
}
}
match &mut self.step {
Step::Distance => {
if t.eq_ignore_ascii_case("t") || t.eq_ignore_ascii_case("through") {
return Some(self.advance_from_distance(None));
}
if upper == "E" || upper == "ERASE" {
self.awaiting = Some(Await::Erase);
return Some(CmdResult::NeedPoint);
}
if upper == "L" || upper == "LAYER" {
self.awaiting = Some(Await::Layer);
return Some(CmdResult::NeedPoint);
}
if let Some(d) = crate::entities::common::parse_typed_length(&t) {
return Some(self.accept_distance(d));
}
@@ -804,20 +930,30 @@ impl CadCommand for OffsetCommand {
*multiple = true;
return Some(CmdResult::NeedPoint);
}
if !t.is_empty() {
if let Some(d) = crate::entities::common::parse_typed_length(&t) {
let d = d.abs().max(1e-9);
defaults::set_offset_dist(d);
*locked = Some(d);
}
let Some(d) = crate::entities::common::parse_typed_length(&t) else {
return Some(CmdResult::NeedPoint);
};
let mag = d.abs().max(1e-9);
defaults::set_offset_dist(mag);
*locked = Some(mag);
// The cursor has already chosen the side, so the value places
// the offset now: on the cursor's side, or the other side when
// negative. Before the cursor has been seen, the click still
// chooses.
match self.cursor {
Some(cursor) => Some(self.place(cursor, d < 0.0)),
None => Some(CmdResult::NeedPoint),
}
// Stay on the side step — the click chooses which side.
Some(CmdResult::NeedPoint)
}
_ => None,
}
}
fn on_entities_committed(&mut self, entities: &[EntityType]) {
self.all_entities.extend(entities.iter().cloned());
self.entity_index = ModifyEntityIndex::build(&self.all_entities);
}
fn on_hover_entity(&mut self, handle: Handle, _pt: DVec3) -> Vec<WireModel> {
if handle.is_null() || !matches!(self.step, Step::SelectObject { .. }) {
return vec![];
@@ -858,66 +994,11 @@ impl CadCommand for OffsetCommand {
_ => {}
}
let (locked, targets, multiple) = match &self.step {
Step::PickSide {
locked,
targets,
multiple,
} => (*locked, targets.clone(), *multiple),
_ => return CmdResult::NeedPoint,
};
// Each target offsets by its own through-distance (or the locked
// magnitude), toward the clicked side.
let per_target: Vec<(Handle, Vec<EntityType>)> = targets
.iter()
.filter_map(|entity| {
let mag = locked.unwrap_or_else(|| perp_distance(entity, pt.as_vec3()));
if mag < 1e-9 {
return None;
}
let offsets = compute_offsets(entity, mag, pt.as_vec3());
(!offsets.is_empty()).then(|| (entity.common().handle, offsets))
})
.collect();
let mut news: Vec<EntityType> = per_target
.iter()
.flat_map(|(_, offsets)| offsets.iter().cloned())
.collect();
if news.is_empty() {
return CmdResult::NeedPoint;
}
self.made += 1;
// Erase option: the offset replaces its source object. Only the
// objects picked from the drawing qualify — a Multiple chain's
// targets are the offsets just made, which are not replaced again.
let erase = self.erase_source && self.targets_are_sources;
self.targets_are_sources = false;
if multiple {
// Multiple mode chains from the result just created, matching
// OFFSET's repeated fixed-distance behavior (parallel/concentric
// series instead of duplicate copies at the first offset).
self.step = Step::PickSide {
targets: news.clone(),
locked,
multiple: true,
};
} else {
// Classic loop (#418): commit this offset and go back to the object
// pick at the same distance, until Enter / Esc finishes.
self.step = Step::SelectObject { locked };
}
if erase {
return CmdResult::ReplaceManyContinue(per_target);
}
if news.len() == 1 {
CmdResult::CommitEntity(news.pop().unwrap())
} else {
CmdResult::CommitEntities(news)
}
self.place(pt, false)
}
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
self.cursor = Some(pt);
if let Step::ReferenceSecond { first } = &self.step {
return vec![WireModel::solid(
"offset_reference_distance".into(),
@@ -974,6 +1055,17 @@ impl CadCommand for OffsetCommand {
let d = defaults::get_offset_dist();
self.accept_distance(d)
}
// Opened on a pre-selection, the same Enter offsets by the last
// distance on the cursor's side.
Step::PickSide { .. } if self.drag_start => {
let Some(cursor) = self.cursor else {
return CmdResult::NeedPoint;
};
if let Step::PickSide { locked, .. } = &mut self.step {
*locked = Some(defaults::get_offset_dist());
}
self.place(cursor, false)
}
_ => CmdResult::Cancel,
}
}
@@ -989,7 +1081,7 @@ inventory::submit!(crate::command::CommandRegistration { names: &["OFFSET"] });
#[cfg(test)]
mod offset_tests {
use super::*;
use acadrust::types::Vector2;
use codec::types::Vector2;
fn rect(corners: &[[f64; 2]]) -> LwPolyline {
LwPolyline {
@@ -1049,7 +1141,7 @@ mod option_tests {
use super::*;
fn line(x1: f64, y1: f64, x2: f64, y2: f64, handle: u64) -> EntityType {
let mut line = acadrust::entities::Line::from_coords(x1, y1, 0.0, x2, y2, 0.0);
let mut line = codec::entities::Line::from_coords(x1, y1, 0.0, x2, y2, 0.0);
line.common.handle = Handle::new(handle);
EntityType::Line(line)
}
@@ -1095,3 +1187,127 @@ mod option_tests {
assert!(matches!(cmd.on_point(DVec3::new(5.0, 5.0, 0.0)), CmdResult::CommitEntity(_)));
}
}
#[cfg(test)]
mod drag_tests {
use super::*;
use codec::types::Vector2;
fn line_y0() -> EntityType {
let mut line = codec::entities::Line::from_coords(0.0, 0.0, 0.0, 10.0, 0.0, 0.0);
line.common.handle = Handle::new(1);
EntityType::Line(line)
}
/// OFFSET opened on `target`, with the cursor hovering at `cursor`.
fn dragging(target: EntityType, cursor: [f64; 2]) -> OffsetCommand {
let mut cmd = OffsetCommand::with_selection(vec![target.clone()], vec![target]);
cmd.on_preview_wires(DVec3::new(cursor[0], cursor[1], 0.0));
cmd
}
fn committed_line_y(result: Option<CmdResult>) -> f64 {
match result {
Some(CmdResult::CommitEntity(EntityType::Line(l))) => l.start.y,
_ => panic!("expected one committed line"),
}
}
#[test]
fn preselection_opens_on_the_live_preview() {
let mut cmd = OffsetCommand::with_selection(vec![line_y0()], vec![line_y0()]);
assert!(matches!(cmd.step, Step::PickSide { locked: None, .. }));
assert_eq!(cmd.options().into_iter().map(|o| o.keyword).collect::<Vec<_>>(), ["M", "E", "L", ""]);
// The preview passes through the cursor before anything is typed.
assert_eq!(cmd.on_preview_wires(DVec3::new(5.0, 3.0, 0.0)).len(), 1);
assert_eq!(cmd.dyn_live_value(DVec3::new(5.0, 3.0, 0.0)), Some(3.0));
// A click still offsets through the cursor.
assert!(matches!(cmd.on_point(DVec3::new(5.0, 3.0, 0.0)), CmdResult::CommitEntity(_)));
}
#[test]
fn typed_distance_places_on_the_cursor_side() {
let mut cmd = dragging(line_y0(), [5.0, 3.0]);
assert!((committed_line_y(cmd.on_text_input("2")) - 2.0).abs() < 1e-9);
// The loop continues at the typed distance.
assert!(matches!(cmd.step, Step::SelectObject { locked: Some(d) } if d == 2.0));
}
#[test]
fn negative_distance_places_on_the_other_side() {
let mut cmd = dragging(line_y0(), [5.0, 3.0]);
assert!((committed_line_y(cmd.on_text_input("-2")) + 2.0).abs() < 1e-9);
assert_eq!(defaults::get_offset_dist(), 2.0);
}
#[test]
fn negative_distance_inside_a_circle_grows_it() {
let circle = EntityType::Circle(CircleEnt {
center: codec::types::Vector3::new(0.0, 0.0, 0.0),
radius: 10.0,
..Default::default()
});
let mut cmd = dragging(circle.clone(), [3.0, 0.0]);
match cmd.on_text_input("-1") {
Some(CmdResult::CommitEntity(EntityType::Circle(c))) => assert!((c.radius - 11.0).abs() < 1e-9),
_ => panic!("expected one committed circle"),
}
let mut cmd = dragging(circle, [3.0, 0.0]);
match cmd.on_text_input("1") {
Some(CmdResult::CommitEntity(EntityType::Circle(c))) => assert!((c.radius - 9.0).abs() < 1e-9),
_ => panic!("expected one committed circle"),
}
}
// A U whose arms sit 1 apart. From inside the left arm the nearest wall
// faces the gap; mirroring the cursor its full 1.9 across would land in
// the right arm, back inside the loop. -0.25 must still go outward.
#[test]
fn negative_distance_does_not_mirror_across_a_gap() {
let u = EntityType::LwPolyline(LwPolyline {
vertices: [
[0.0, 0.0], [9.0, 0.0], [9.0, 10.0], [5.0, 10.0],
[5.0, 2.0], [4.0, 2.0], [4.0, 10.0], [0.0, 10.0],
]
.iter()
.map(|&[x, y]| LwVertex::new(Vector2::new(x, y)))
.collect(),
is_closed: true,
..Default::default()
});
let mut cmd = dragging(u, [2.1, 6.0]);
match cmd.on_text_input("-0.25") {
Some(CmdResult::CommitEntity(EntityType::LwPolyline(p))) => {
let min_x = p.vertices.iter().map(|v| v.location.x).fold(f64::INFINITY, f64::min);
assert!((min_x + 0.25).abs() < 1e-6, "offset went inward: min x {min_x}");
}
_ => panic!("expected one committed polyline"),
}
}
#[test]
fn enter_repeats_the_last_distance_on_the_cursor_side() {
defaults::set_offset_dist(4.0);
let mut cmd = dragging(line_y0(), [5.0, -1.0]);
match cmd.on_enter() {
CmdResult::CommitEntity(EntityType::Line(l)) => assert!((l.start.y + 4.0).abs() < 1e-9),
_ => panic!("expected one committed line"),
}
// After that, Enter finishes as before.
assert!(matches!(cmd.on_enter(), CmdResult::Cancel));
}
#[test]
fn erase_is_offered_without_the_distance_step() {
let mut cmd = dragging(line_y0(), [5.0, 3.0]);
cmd.on_text_input("E");
cmd.on_text_input("Y");
assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::ReplaceManyContinue(_))));
}
#[test]
fn without_preselection_the_distance_comes_first() {
let cmd = OffsetCommand::new(vec![line_y0()]);
assert!(matches!(cmd.step, Step::Distance));
}
}
+5 -5
View File
@@ -1,4 +1,4 @@
use acadrust::{EntityType, Handle};
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult};
@@ -104,7 +104,7 @@ impl CadCommand for OverkillCommand {
pub fn normalized(entity:&EntityType, ignore:u16) -> EntityType {
let mut e=entity.clone();
let common=e.common_mut();
let default=acadrust::entities::EntityCommon::new();
let default=codec::entities::EntityCommon::new();
common.handle=Handle::NULL;
if ignore&1!=0 {common.color=default.color;common.color_name=None;common.color_book_handle=None;}
if ignore&2!=0 {common.layer.clear();}
@@ -125,7 +125,7 @@ pub fn optimize(entity:&mut EntityType, tolerance:f64) {
// Width changes and bulged spans carry geometry that must be retained.
if p.vertices.iter().any(|v|v.bulge!=0.0||v.start_width!=0.0||v.end_width!=0.0) {return;}
let points:Vec<_>=p.vertices.iter().map(|v|[v.location.x,v.location.y,p.elevation]).collect();
let indices=cadkernel::space::simplify_linear_chain(&points,tolerance);
let indices=kernel::space::simplify_linear_chain(&points,tolerance);
if indices.len()>=if p.is_closed{3}else{2} {
p.vertices=indices.into_iter().map(|i|p.vertices[i].clone()).collect();
}
@@ -135,8 +135,8 @@ pub fn optimize(entity:&mut EntityType, tolerance:f64) {
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::{Line, LwPolyline};
use acadrust::types::{Color, Vector2};
use codec::entities::{Line, LwPolyline};
use codec::types::{Color, Vector2};
#[test]
fn normalization_ignores_only_the_requested_properties() {
+99 -46
View File
@@ -1,18 +1,19 @@
// PEDIT edits polylines and polygon meshes.
use acadrust::entities::LwVertex;
use acadrust::types::{Vector2, Vector3};
use acadrust::{EntityType, Handle};
use cadkernel::geom2d::nurbs::clamped_uniform_knots;
use cadkernel::geom2d::NurbsCurve;
use codec::entities::LwVertex;
use codec::types::{Vector2, Vector3};
use codec::{EntityType, Handle};
use kernel::geom2d::nurbs::clamped_uniform_knots;
use kernel::geom2d::NurbsCurve;
use glam::DVec3;
use rustc_hash::FxHashMap as HashMap;
use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel;
use crate::t;
const TAU: f64 = std::f64::consts::TAU;
pub use cadkernel::space::endpoint_join::JoinType;
pub use kernel::space::endpoint_join::JoinType;
static JOIN_TYPE: std::sync::Mutex<JoinType> = std::sync::Mutex::new(JoinType::Extend);
static JOIN_FUZZ: std::sync::Mutex<f64> = std::sync::Mutex::new(0.0);
@@ -70,7 +71,7 @@ pub struct PeditCommand {
vertex_tangents: HashMap<usize, f64>,
pending_tangents: Option<HashMap<usize, f64>>,
tangent_history: Vec<HashMap<usize, f64>>,
mesh_smooth_type: acadrust::entities::polygon_mesh::SurfaceSmoothType,
mesh_smooth_type: codec::entities::polygon_mesh::SurfaceSmoothType,
mesh_smooth_density: (i16, i16),
mesh_vertex_default: isize,
pending_mesh_closed: Option<(bool, bool)>,
@@ -84,7 +85,7 @@ impl PeditCommand {
surface_u_density: i16,
surface_v_density: i16,
) -> Self {
use acadrust::entities::polygon_mesh::SurfaceSmoothType;
use codec::entities::polygon_mesh::SurfaceSmoothType;
let mesh_smooth_type = match surface_type {
5 => SurfaceSmoothType::Quadratic,
@@ -188,6 +189,24 @@ impl PeditCommand {
}
}
/// WCS location of a vertex of the polyline being edited.
fn vertex_world(&self, index: usize) -> Option<DVec3> {
let entity = self.target.and_then(|handle| self.entities.get(&handle.value()))?;
let (ocs, n) = match entity {
EntityType::LwPolyline(p) => {
let v = p.vertices.get(index)?;
((v.location.x, v.location.y, p.elevation), &p.normal)
}
EntityType::Polyline2D(p) => {
let v = p.vertices.get(index)?;
((v.location.x, v.location.y, p.elevation), &p.normal)
}
_ => return None,
};
let (x, y, z) = crate::scene::view::transform::ocs_point_to_wcs(ocs, (n.x, n.y, n.z));
Some(DVec3::new(x, y, z))
}
fn vertex_count(&self) -> usize {
self.target.and_then(|handle| self.entities.get(&handle.value())).map_or(0, |entity| match entity {
EntityType::LwPolyline(polyline) => polyline.vertices.len(),
@@ -237,6 +256,40 @@ impl CadCommand for PeditCommand {
"PEDIT"
}
/// Edit-vertex steps mark the current vertex with a screen-sized X (#1628).
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
let index = match self.mode {
Mode::PolyVertex(i)
| Mode::PolyTangent(i)
| Mode::PolyMove(i)
| Mode::PolyInsert(i)
| Mode::PolyWidthStart(i, _)
| Mode::PolyWidthEnd(i, _) => i,
Mode::PolyRange { end, .. } => end,
_ => return None,
};
let origin = self.vertex_world(index)?;
let at = |dx: f64, dy: f64| {
let p = origin + DVec3::new(dx, dy, 0.0);
[p.x, p.y, p.z]
};
let h = 0.5;
let mut wire = WireModel::solid_f64(
"pedit_vertex_marker".into(),
vec![at(-h, -h), at(h, h), [f64::NAN; 3], at(-h, h), at(h, -h)],
WireModel::CYAN,
false,
);
wire.point_marker = Some(crate::scene::model::wire_model::PointMarker {
origin,
normal: DVec3::Z,
axis_x: DVec3::X,
axis_y: DVec3::Y,
viewport_percent: 3.0,
});
Some(wire)
}
fn prompt(&self) -> String {
match &self.mode {
Mode::PickTarget => {
@@ -622,7 +675,7 @@ impl CadCommand for PeditCommand {
handle,
op: PeditOp::SetMeshSmooth {
smooth:
acadrust::entities::polygon_mesh::SurfaceSmoothType::NoSmooth,
codec::entities::polygon_mesh::SurfaceSmoothType::NoSmooth,
m_density: self.mesh_smooth_density.0,
n_density: self.mesh_smooth_density.1,
},
@@ -741,7 +794,7 @@ impl CadCommand for PeditCommand {
_ => return CmdResult::NeedPoint,
};
let Some(local) = crate::entities::curve::ocs_plane(normal, elevation).project([point.x, point.y, point.z]) else { return CmdResult::NeedPoint; };
let delta = cadkernel::geom2d::Vec2::new(local[0] - location[0], local[1] - location[1]);
let delta = kernel::geom2d::Vec2::new(local[0] - location[0], local[1] - location[1]);
if !delta.length_squared().is_finite() || delta.length_squared() <= 1e-24 { return CmdResult::NeedPoint; }
self.vertex_tangents.insert(index, delta.angle().rem_euclid(TAU));
self.mode = Mode::PolyVertex(index);
@@ -838,7 +891,7 @@ pub enum PeditOp {
SetMeshClosedM(bool),
SetMeshClosedN(bool),
SetMeshSmooth {
smooth: acadrust::entities::polygon_mesh::SurfaceSmoothType,
smooth: codec::entities::polygon_mesh::SurfaceSmoothType,
m_density: i16,
n_density: i16,
},
@@ -878,9 +931,9 @@ pub fn edit_vertex_range(entity: &EntityType, first: usize, last: usize, split:
}
match entity {
EntityType::LwPolyline(polyline) => edit!(polyline, polyline.is_closed, LwPolyline,
|value: &mut acadrust::entities::LwPolyline| { value.is_closed = false; }),
|value: &mut codec::entities::LwPolyline| { value.is_closed = false; }),
EntityType::Polyline2D(polyline) => edit!(polyline, polyline.flags.is_closed(), Polyline2D,
|value: &mut acadrust::entities::Polyline2D| { value.flags.set_closed(false); }),
|value: &mut codec::entities::Polyline2D| { value.flags.set_closed(false); }),
_ => None,
}
}
@@ -929,7 +982,7 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
if *index >= polyline.vertices.len() { return false; }
let location = Vector3::new(local[0], local[1], polyline.elevation);
if *insert {
polyline.vertices.insert(index + 1, acadrust::entities::polyline::Vertex2D::new(location));
polyline.vertices.insert(index + 1, codec::entities::polyline::Vertex2D::new(location));
} else { polyline.vertices[*index].location = location; }
true
}
@@ -948,10 +1001,10 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
true
}
EntityType::Polyline2D(polyline) => {
let flag = acadrust::entities::polyline::PolylineFlags::LINETYPE_CONTINUOUS;
let flag = codec::entities::polyline::PolylineFlags::LINETYPE_CONTINUOUS;
let bits = polyline.flags.bits();
if (bits & flag.bits() != 0) == *enabled { return false; }
polyline.flags = acadrust::entities::polyline::PolylineFlags::from_bits(
polyline.flags = codec::entities::polyline::PolylineFlags::from_bits(
if *enabled { bits | flag.bits() } else { bits & !flag.bits() }
);
true
@@ -1014,7 +1067,7 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
return false;
}
mesh.flags.set(
acadrust::entities::polygon_mesh::PolygonMeshFlags::CLOSED_M,
codec::entities::polygon_mesh::PolygonMeshFlags::CLOSED_M,
*closed,
);
true
@@ -1027,7 +1080,7 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
return false;
}
mesh.flags.set(
acadrust::entities::polygon_mesh::PolygonMeshFlags::CLOSED_N,
codec::entities::polygon_mesh::PolygonMeshFlags::CLOSED_N,
*closed,
);
true
@@ -1043,7 +1096,7 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
let mut changed = mesh.smooth_type != *smooth;
mesh.smooth_type = *smooth;
if mesh.smooth_type
!= acadrust::entities::polygon_mesh::SurfaceSmoothType::NoSmooth
!= codec::entities::polygon_mesh::SurfaceSmoothType::NoSmooth
{
let m_density = (*m_density).clamp(2, 200);
let n_density = (*n_density).clamp(2, 200);
@@ -1074,7 +1127,7 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
{
return false;
}
vertex.location = acadrust::types::Vector3::new(point.x, point.y, point.z);
vertex.location = codec::types::Vector3::new(point.x, point.y, point.z);
true
}
_ => false,
@@ -1087,7 +1140,7 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
/// A Line or Arc as an equivalent 2-vertex LwPolyline (common carried over,
/// handle NULL for the replace flow). `None` for anything else.
pub fn convert_to_polyline(entity: &EntityType) -> Option<EntityType> {
let mut pl = acadrust::LwPolyline::new();
let mut pl = codec::LwPolyline::new();
match entity {
EntityType::Line(l) => {
let normal = DVec3::new(l.normal.x, l.normal.y, l.normal.z).try_normalize()?;
@@ -1098,7 +1151,7 @@ pub fn convert_to_polyline(entity: &EntityType) -> Option<EntityType> {
normal.x, normal.y, normal.z,
));
let plane = crate::entities::curve::ocs_plane(normal.clone(), elevation);
let tolerance = cadkernel::space::coplanarity_tolerance(&[start, end]);
let tolerance = kernel::space::coplanarity_tolerance(&[start, end]);
if !plane.contains(end, tolerance) {
return None;
}
@@ -1154,10 +1207,10 @@ mod convert_tests {
// onto the new entity instead of resetting to 0 (#916).
#[test]
fn convert_keeps_line_thickness() {
let mut l = acadrust::entities::Line::new();
l.start = acadrust::types::Vector3::new(2.0, 3.0, 4.0);
l.end = acadrust::types::Vector3::new(2.0, 5.0, 6.0);
l.normal = acadrust::types::Vector3::new(1.0, 0.0, 0.0);
let mut l = codec::entities::Line::new();
l.start = codec::types::Vector3::new(2.0, 3.0, 4.0);
l.end = codec::types::Vector3::new(2.0, 5.0, 6.0);
l.normal = codec::types::Vector3::new(1.0, 0.0, 0.0);
l.thickness = 3.5;
let Some(EntityType::LwPolyline(pl)) = convert_to_polyline(&EntityType::Line(l)) else {
panic!("line must convert");
@@ -1167,15 +1220,15 @@ mod convert_tests {
"converted polyline must keep source thickness, got {}",
pl.thickness
);
assert_eq!(pl.normal, acadrust::types::Vector3::new(1.0, 0.0, 0.0));
assert_eq!(pl.normal, codec::types::Vector3::new(1.0, 0.0, 0.0));
assert!((pl.elevation - 2.0).abs() < 1e-12);
}
#[test]
fn convert_keeps_arc_thickness() {
let mut a = acadrust::entities::Arc::new();
a.center = acadrust::types::Vector3::new(0.0, 0.0, 4.0);
a.normal = acadrust::types::Vector3::new(0.0, 1.0, 0.0);
let mut a = codec::entities::Arc::new();
a.center = codec::types::Vector3::new(0.0, 0.0, 4.0);
a.normal = codec::types::Vector3::new(0.0, 1.0, 0.0);
a.radius = 5.0;
a.start_angle = 0.0;
a.end_angle = std::f64::consts::FRAC_PI_2;
@@ -1188,7 +1241,7 @@ mod convert_tests {
"converted polyline must keep negative thickness, got {}",
pl.thickness
);
assert_eq!(pl.normal, acadrust::types::Vector3::new(0.0, 1.0, 0.0));
assert_eq!(pl.normal, codec::types::Vector3::new(0.0, 1.0, 0.0));
assert!((pl.elevation - 4.0).abs() < 1e-12);
}
}
@@ -1196,7 +1249,7 @@ mod convert_tests {
// ── Curve fitting ─────────────────────────────────────────────────────────
fn fit_entity(entity: &EntityType, overrides: &[(usize, f64)]) -> Option<EntityType> {
use acadrust::entities::{Polyline2D, PolylineFlags, Vertex2D, VertexFlags};
use codec::entities::{Polyline2D, PolylineFlags, Vertex2D, VertexFlags};
let mut polyline = match entity {
EntityType::Polyline2D(p) => p.clone(),
EntityType::LwPolyline(p) => {
@@ -1231,7 +1284,7 @@ fn fit_entity(entity: &EntityType, overrides: &[(usize, f64)]) -> Option<EntityT
}
let points: Vec<_> = originals.iter().map(|v| [v.location.x, v.location.y]).collect();
let directions: Vec<_> = originals.iter().map(|v| (v.flags.bits() & 2 != 0).then(|| [v.curve_tangent.cos(), v.curve_tangent.sin()])).collect();
let fitted = cadkernel::geom2d::fit_arc_chain(&points, polyline.flags.is_closed(), &directions)?;
let fitted = kernel::geom2d::fit_arc_chain(&points, polyline.flags.is_closed(), &directions)?;
let mut vertices = Vec::with_capacity(fitted.len());
for (i, fitted_vertex) in fitted.iter().enumerate() {
let original = &originals[fitted_vertex.source];
@@ -1251,7 +1304,7 @@ fn fit_entity(entity: &EntityType, overrides: &[(usize, f64)]) -> Option<EntityT
Some(EntityType::Polyline2D(polyline))
}
fn defining_vertices(polyline: &acadrust::entities::Polyline2D) -> Vec<acadrust::entities::Vertex2D> {
fn defining_vertices(polyline: &codec::entities::Polyline2D) -> Vec<codec::entities::Vertex2D> {
polyline.vertices.iter().enumerate().filter(|(_, vertex)| vertex.flags.bits() & 1 == 0).map(|(index, vertex)| {
let mut original = vertex.clone();
if let Some(knee) = polyline.vertices[index + 1..].iter().take_while(|next| next.flags.bits() & 1 != 0).last() {
@@ -1265,7 +1318,7 @@ fn decurve_entity(entity: &EntityType) -> Option<EntityType> {
let mut result = match entity {
EntityType::LwPolyline(p) => p.clone(),
EntityType::Polyline2D(p) => {
let mut result = acadrust::LwPolyline::new();
let mut result = codec::LwPolyline::new();
result.common = p.common.clone(); result.normal = p.normal;
result.elevation = p.elevation; result.thickness = p.thickness;
result.is_closed = p.flags.is_closed(); result.plinegen = p.flags.bits() & 128 != 0;
@@ -1299,7 +1352,7 @@ fn decurve_entity(entity: &EntityType) -> Option<EntityType> {
/// out, with the open case clamped by repeating end points and then the two
/// ends pinned back afterwards to undo the drift that leaves — all of which
/// a clamped knot vector expresses directly and exactly.
fn spline_smooth(p: &mut acadrust::LwPolyline) -> bool {
fn spline_smooth(p: &mut codec::LwPolyline) -> bool {
const DEGREE: usize = 3;
const PER_SPAN: usize = 8;
@@ -1350,10 +1403,10 @@ fn spline_smooth(p: &mut acadrust::LwPolyline) -> bool {
#[cfg(test)]
mod editing_tests {
use super::*;
use acadrust::entities::{Polyline2D, Vertex2D};
use codec::entities::{Polyline2D, Vertex2D};
fn lightweight(handle: u64, points: &[[f64; 2]]) -> EntityType {
let mut polyline = acadrust::LwPolyline::new();
let mut polyline = codec::LwPolyline::new();
polyline.common.handle = Handle::new(handle);
polyline.vertices = points
.iter()
@@ -1488,9 +1541,9 @@ mod editing_tests {
#[test]
fn multiple_decline_and_fuzz_join_keep_the_command_active() {
*JOIN_TYPE.lock().unwrap_or_else(|error| error.into_inner()) = JoinType::Extend;
let mut line = acadrust::entities::Line::new();
let mut line = codec::entities::Line::new();
line.common.handle = Handle::new(11);
let mut arc = acadrust::entities::Arc::new();
let mut arc = codec::entities::Arc::new();
arc.common.handle = Handle::new(12);
let mut declined = command(
vec![EntityType::Line(line), EntityType::Arc(arc)],
@@ -1663,14 +1716,14 @@ pub fn join_selection_extend(source: &EntityType, candidates: &[(Handle, &Entity
let b_ends = [true,false].into_iter().filter_map(|start| endpoint(candidate,start).map(|points|(start,points))).collect::<Vec<_>>();
let a_points = a_ends.iter().map(|(_,points)|*points).collect::<Vec<_>>();
let b_points = b_ends.iter().map(|(_,points)|*points).collect::<Vec<_>>();
if let Some((a_index,b_index,joint)) = cadkernel::space::endpoint_join::closest_line_end_join(&a_points,&b_points,fuzz,connector_distance.unwrap_or(0.0),kind) {
if let Some((a_index,b_index,joint)) = kernel::space::endpoint_join::closest_line_end_join(&a_points,&b_points,fuzz,connector_distance.unwrap_or(0.0),kind) {
let (a_start,a_points) = a_ends[a_index]; let (b_start,b_points) = b_ends[b_index];
let mut a = result.clone(); let mut b = (*candidate).clone();
let joined_pair = if let Some(point) = joint {
if move_endpoint(&mut a,a_start,point).is_none() || move_endpoint(&mut b,b_start,point).is_none() {continue;}
super::join::join_to_source(&a,&[(*handle,&b)])
} else {
let mut line = acadrust::entities::Line::new();
let mut line = codec::entities::Line::new();
line.start = Vector3::new(a_points[1][0],a_points[1][1],a_points[1][2]);
line.end = Vector3::new(b_points[1][0],b_points[1][1],b_points[1][2]);
match &a {
@@ -1710,7 +1763,7 @@ pub fn selection_connector_distance(entities: &[(Handle, EntityType)], fuzz: f64
};
if !z.is_finite() { return None; }
let plane = crate::entities::curve::ocs_plane(*normal, z);
let normal = cadkernel::space::Vec3::from(plane.normal()?);
let normal = kernel::space::Vec3::from(plane.normal()?);
let points = vertices.into_iter().map(|point| plane.point_at(point)).collect::<Vec<_>>();
curves.push((normal, points));
}
@@ -1719,10 +1772,10 @@ pub fn selection_connector_distance(entities: &[(Handle, EntityType)], fuzz: f64
return None;
}
let world = curves.iter().flat_map(|(_, points)| points.iter().copied()).collect::<Vec<_>>();
let tolerance = cadkernel::space::coplanarity_tolerance(&world);
let tolerance = kernel::space::coplanarity_tolerance(&world);
let shared = crate::entities::curve::ocs_plane(Vector3::new(normal.x, normal.y, normal.z),
cadkernel::space::Vec3::from(world[0]).dot(*normal));
kernel::space::Vec3::from(world[0]).dot(*normal));
if world.iter().any(|point| !shared.contains(*point, tolerance)) { return None; }
let points = world.into_iter().map(|point| shared.project(point)).collect::<Option<Vec<_>>>()?;
cadkernel::space::endpoint_join::planar_connector_distance(&points, fuzz)
kernel::space::endpoint_join::planar_connector_distance(&points, fuzz)
}
+3 -3
View File
@@ -13,7 +13,7 @@
// with an error message — full matrix inversion for those cases would
// require per-entity matrix transforms not yet in EntityTransform.
use acadrust::{EntityType, Handle};
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::t;
@@ -53,9 +53,9 @@ pub struct RefEditSession {
// ── INSERT placement, as full affine transforms ───────────────────
/// Block-local → world (the INSERT's `get_transform`). Handles OCS,
/// rotation and non-uniform / mirrored scale in one matrix.
pub forward: acadrust::types::Transform,
pub forward: codec::types::Transform,
/// World → block-local, applied on SAVE to bring edits back.
pub inverse: acadrust::types::Transform,
pub inverse: codec::types::Transform,
/// Handle watermark captured before the temp copies were created: every
/// entity allocated at or above it during the session (drawn, pasted,
/// offset, …) belongs to the working set, so Save folds it into the block
+10 -9
View File
@@ -21,8 +21,8 @@
// `CmdResult::NeedPoint` and keeps prompting so nothing is corrupted.
use crate::t;
use acadrust::entities::Spline;
use acadrust::{EntityType, Handle};
use codec::entities::Spline;
use codec::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
@@ -75,7 +75,8 @@ impl ReverseCommand {
}
EntityType::Spline(sp) => Some(EntityType::Spline(reverse_spline(sp))),
EntityType::Helix(helix) => {
crate::entities::helix::reversed(helix).map(EntityType::Helix)
crate::entities::helix::reversed(helix)
.map(|helix| EntityType::Helix(Box::new(helix)))
}
_ => None,
}
@@ -104,7 +105,7 @@ impl ReverseCommand {
/// starting at original vertex `(n-1-i-1).rem_euclid(n)`. The widths follow the
/// Widths are segment-relative. Reversing a segment swaps its start and end
/// widths in addition to moving them to the corresponding reversed segment.
fn reverse_lwpolyline(pl: &acadrust::LwPolyline) -> acadrust::LwPolyline {
fn reverse_lwpolyline(pl: &codec::LwPolyline) -> codec::LwPolyline {
let mut out = pl.clone();
let n = pl.vertices.len();
if n < 2 {
@@ -141,7 +142,7 @@ fn reverse_lwpolyline(pl: &acadrust::LwPolyline) -> acadrust::LwPolyline {
}
/// Reverse a heavy 2D polyline with the same segment semantics as LWPolyline.
fn reverse_polyline2d(pl: &acadrust::entities::Polyline2D) -> acadrust::entities::Polyline2D {
fn reverse_polyline2d(pl: &codec::entities::Polyline2D) -> codec::entities::Polyline2D {
let mut out = pl.clone();
let n = pl.vertices.len();
if n < 2 {
@@ -152,7 +153,7 @@ fn reverse_polyline2d(pl: &acadrust::entities::Polyline2D) -> acadrust::entities
out.start_width = pl.end_width;
out.end_width = pl.start_width;
for vertex in &mut out.vertices {
let tangent_flag = acadrust::entities::VertexFlags::CURVE_FIT_TANGENT.bits();
let tangent_flag = codec::entities::VertexFlags::CURVE_FIT_TANGENT.bits();
if vertex.flags.bits() & tangent_flag != 0 {
vertex.curve_tangent =
(vertex.curve_tangent + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU);
@@ -190,8 +191,8 @@ pub(super) fn reverse_spline(sp: &Spline) -> Spline {
out.knots = curve.reversed().knots().to_vec();
}
out.begin_tangent =
acadrust::types::Vector3::new(-sp.end_tangent.x, -sp.end_tangent.y, -sp.end_tangent.z);
out.end_tangent = acadrust::types::Vector3::new(
codec::types::Vector3::new(-sp.end_tangent.x, -sp.end_tangent.y, -sp.end_tangent.z);
out.end_tangent = codec::types::Vector3::new(
-sp.begin_tangent.x,
-sp.begin_tangent.y,
-sp.begin_tangent.z,
@@ -252,7 +253,7 @@ inventory::submit!(crate::command::CommandRegistration {
#[cfg(test)]
mod tests {
use super::*;
use acadrust::types::Vector3;
use codec::types::Vector3;
#[test]
fn spline_reversal_preserves_knot_spacing_weights_and_tangents() {
+87 -4
View File
@@ -9,8 +9,8 @@
// two points. The new absolute angle is then typed or picked from the center;
// the applied rotation is new-angle - reference-angle.
use acadrust::Handle;
use cadkernel::geom2d::{self, Curve as KernelCurve};
use codec::Handle;
use kernel::geom2d::{self, Curve as KernelCurve};
use glam::DVec3;
use crate::t;
@@ -36,6 +36,8 @@ pub fn tool() -> ToolDef {
enum Step {
Center,
/// 3DROTATE: the axis through the base point.
Axis { center: DVec3 },
Angle { center: DVec3 },
RefFirst { center: DVec3 },
RefSecond { center: DVec3, first: DVec3 },
@@ -48,6 +50,11 @@ pub struct RotateCommand {
step: Step,
default_angle: f64, // degrees
plane: WorkingPlane,
/// 3DROTATE: ask for the axis, then measure the angle in the plane square
/// to it.
spatial: bool,
/// The plane is the one square to the chosen axis, not the UCS.
axis_chosen: bool,
}
impl RotateCommand {
@@ -58,9 +65,35 @@ impl RotateCommand {
step: Step::Center,
default_angle: defaults::get_rotate_angle(),
plane: WorkingPlane::default(),
spatial: false,
axis_chosen: false,
}
}
/// 3DROTATE: any objects, about any axis through a base point.
pub fn new_3d(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
Self { spatial: true, ..Self::new(handles, wire_models) }
}
/// Turn about `axis` through `center`: the angle is then read in the plane
/// square to it, so every later step works as in a plain rotation.
fn choose_axis(&mut self, center: DVec3, axis: DVec3) -> CmdResult {
let Some(axis) = axis.try_normalize() else {
return CmdResult::NeedPoint;
};
// The UCS X laid into that plane, or Y when X runs along the axis.
let reference = [self.plane.x, self.plane.y]
.into_iter()
.map(|direction| direction - axis * direction.dot(axis))
.find(|direction| direction.length() > 1e-6)
.unwrap_or(DVec3::X);
let x = reference.normalize();
self.plane = WorkingPlane::new(center, x, axis.cross(x));
self.axis_chosen = true;
self.step = Step::Angle { center };
CmdResult::NeedPoint
}
fn commit(&self, center: DVec3, angle_rad: f64) -> CmdResult {
defaults::set_rotate_angle(angle_rad.to_degrees());
CmdResult::TransformSelected(
@@ -138,20 +171,38 @@ impl RotateCommand {
impl CadCommand for RotateCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
if !self.axis_chosen {
self.plane = plane;
}
}
fn name(&self) -> &'static str {
"ROTATE"
if self.spatial {
"3DROTATE"
} else {
"ROTATE"
}
}
fn prompt(&self) -> String {
match &self.step {
Step::Center if self.spatial => t!(
"3DROTATE Specify base point [%{count} objects]:",
count = self.handles.len()
)
.into_owned(),
Step::Center => t!(
"ROTATE Specify rotation center [%{count} objects]:",
count = self.handles.len()
)
.into_owned(),
Step::Axis { .. } => {
t!("3DROTATE Specify rotation axis [X/Y/Z] or a second point on it <Z>:").into_owned()
}
Step::Angle { .. } if self.spatial => {
let a = format!("{:.4}", self.default_angle);
t!("3DROTATE Specify rotation angle <%{a}>:", a = a).into_owned()
}
Step::Angle { .. } => {
let a = format!("{:.4}", self.default_angle);
t!("ROTATE Specify rotation angle <%{a}>:", a = a).into_owned()
@@ -172,6 +223,7 @@ impl CadCommand for RotateCommand {
fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption;
match self.step {
Step::Axis { .. } => ["X", "Y", "Z"].map(|axis| CmdOption::new(axis, axis)).to_vec(),
Step::Angle { .. } => vec![CmdOption::new(t!("Reference").as_ref(), "R")],
_ => vec![],
}
@@ -179,10 +231,18 @@ impl CadCommand for RotateCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step {
Step::Center if self.spatial => {
self.step = Step::Axis { center: pt };
CmdResult::NeedPoint
}
Step::Center => {
self.step = Step::Angle { center: pt };
CmdResult::NeedPoint
}
Step::Axis { center } => {
let center = *center;
self.choose_axis(center, pt - center)
}
Step::Angle { center } => {
let center = *center;
let Some(angle_rad) = self.plane.angle(center, pt) else {
@@ -214,6 +274,11 @@ impl CadCommand for RotateCommand {
}
fn on_enter(&mut self) -> CmdResult {
// The axis defaults to the UCS Z.
if let Step::Axis { center } = &self.step {
let center = *center;
return self.choose_axis(center, self.plane.z);
}
// At the normal angle step, Enter uses the stored default angle.
if let Step::Angle { center } = &self.step {
let center = *center;
@@ -228,6 +293,16 @@ impl CadCommand for RotateCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
match &self.step {
Step::Axis { center } => {
let center = *center;
let axis = match t.to_ascii_uppercase().as_str() {
"X" => self.plane.x,
"Y" => self.plane.y,
"Z" => self.plane.z,
_ => return None,
};
Some(self.choose_axis(center, axis))
}
Step::Angle { center } => {
let center = *center;
let low = t.to_ascii_lowercase();
@@ -260,6 +335,14 @@ impl CadCommand for RotateCommand {
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let (center, angle_rad, guide_angle) = match &self.step {
Step::Axis { center } => {
return vec![WireModel::solid_f64(
"rotate_axis".into(),
vec![center.to_array(), pt.to_array()],
WireModel::CYAN,
false,
)];
}
Step::Angle { center } => {
let Some(angle) = self.plane.angle(*center, pt) else {
return vec![];
+1 -1
View File
@@ -12,7 +12,7 @@
// measured between two points; the new length may be typed or measured
// from the scale base.
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
+10 -10
View File
@@ -1,4 +1,4 @@
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult};
@@ -6,10 +6,10 @@ static MODE: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(255)
pub fn mode() -> u8 { MODE.load(std::sync::atomic::Ordering::Relaxed) }
pub fn set_mode(value: u8) { MODE.store(value, std::sync::atomic::Ordering::Relaxed); }
pub fn apply_mask(common: &mut acadrust::entities::EntityCommon, mask: u8, change_byblock: bool) -> bool {
pub fn apply_mask(common: &mut codec::entities::EntityCommon, mask: u8, change_byblock: bool) -> bool {
let before = common.clone();
if mask & 1 != 0 && (change_byblock || common.color != acadrust::types::Color::ByBlock) {
common.color = acadrust::types::Color::ByLayer;
if mask & 1 != 0 && (change_byblock || common.color != codec::types::Color::ByBlock) {
common.color = codec::types::Color::ByLayer;
common.color_name = None;
common.color_book_handle = None;
}
@@ -17,8 +17,8 @@ pub fn apply_mask(common: &mut acadrust::entities::EntityCommon, mask: u8, chang
common.linetype = "ByLayer".to_string();
common.linetype_handle = None;
}
if mask & 4 != 0 && (change_byblock || common.line_weight != acadrust::types::LineWeight::ByBlock) {
common.line_weight = acadrust::types::LineWeight::ByLayer;
if mask & 4 != 0 && (change_byblock || common.line_weight != codec::types::LineWeight::ByBlock) {
common.line_weight = codec::types::LineWeight::ByLayer;
}
if mask & 8 != 0 && (change_byblock || common.material_flags != 1) {
common.material_flags = 0;
@@ -28,8 +28,8 @@ pub fn apply_mask(common: &mut acadrust::entities::EntityCommon, mask: u8, chang
common.plotstyle_flags = 0;
common.plotstyle_handle = None;
}
if mask & 128 != 0 && (change_byblock || common.transparency != acadrust::types::Transparency::BY_BLOCK) {
common.transparency = acadrust::types::Transparency::BY_LAYER;
if mask & 128 != 0 && (change_byblock || common.transparency != codec::types::Transparency::BY_BLOCK) {
common.transparency = codec::types::Transparency::BY_LAYER;
}
*common != before
}
@@ -155,8 +155,8 @@ inventory::submit!(crate::command::CommandRegistration { names: &["SETBYLAYERMOD
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::EntityCommon;
use acadrust::types::{Color, Transparency};
use codec::entities::EntityCommon;
use codec::types::{Color, Transparency};
#[test]
fn mask_changes_only_requested_properties() {
+8 -8
View File
@@ -1,6 +1,6 @@
// Shared B-spline utilities for modify commands (TRIM, BREAK, OFFSET, LENGTHEN).
//
// The evaluation lives in cadkernel; this file converts a drawing's SPLINE
// The evaluation lives in opencadkernel; this file converts a drawing's SPLINE
// entity to and from it and keeps the knot-domain call shapes the commands
// were written against.
//
@@ -9,11 +9,11 @@
// piece of a NURBS circle into a piece of a parabola — a change that looks
// fine until it is measured.
use acadrust::entities::Spline;
use acadrust::types::Vector3;
use acadrust::Handle;
use cadkernel::geom2d::{NurbsCurve, Parameterization};
use cadkernel::space::Plane;
use codec::entities::Spline;
use codec::types::Vector3;
use codec::Handle;
use kernel::geom2d::{NurbsCurve, Parameterization};
use kernel::space::Plane;
/// The drawing plane a spline sits on.
///
@@ -30,7 +30,7 @@ fn elevation(spl: &Spline) -> f64 {
// ── Conversion ─────────────────────────────────────────────────────────────
/// Convert an acadrust `Spline` to a kernel `NurbsCurve`, weights included.
/// Convert an opencadcodec `Spline` to a kernel `NurbsCurve`, weights included.
///
/// A SPLINE is stored one of two ways and both turn up: a control polygon the
/// curve approaches, or fit points it must pass through with optional end
@@ -114,7 +114,7 @@ fn spline_to_nurbs_with(
)
}
/// Rebuild an acadrust `Spline` from a kernel curve.
/// Rebuild an opencadcodec `Spline` from a kernel curve.
///
/// The `template` supplies entity common data and the Z elevation; splines
/// here are planar, so the kernel's 2D points are lifted back onto it.
+34 -34
View File
@@ -9,8 +9,8 @@
//
// Control-point dragging is already supported via the grip editing system.
use acadrust::types::Vector3;
use acadrust::EntityType;
use codec::types::Vector3;
use codec::EntityType;
use glam::DVec3;
@@ -53,16 +53,16 @@ enum Step {
pub struct SplineditCommand {
step: Step,
handle: acadrust::Handle,
spline: Option<acadrust::entities::Spline>,
pending: Option<acadrust::entities::Spline>,
history: Vec<(acadrust::Handle, acadrust::entities::Spline)>,
handle: codec::Handle,
spline: Option<codec::entities::Spline>,
pending: Option<codec::entities::Spline>,
history: Vec<(codec::Handle, codec::entities::Spline)>,
pick_context: Option<crate::command::PointPickContext>,
join_candidates: Vec<crate::command::SelectionEntity>,
delete_source: bool,
}
fn clear_fit_method(spline: &mut acadrust::entities::Spline) {
fn clear_fit_method(spline: &mut codec::entities::Spline) {
spline.fit_points.clear();
spline.begin_tangent = Vector3::ZERO;
spline.end_tangent = Vector3::ZERO;
@@ -80,7 +80,7 @@ fn weights_are_rational(weights: &[f64]) -> bool {
impl SplineditCommand {
pub fn new() -> Self {
Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new(), pick_context: None, join_candidates: Vec::new(), delete_source: true }
Self { step: Step::SelectSpline, handle: codec::Handle::NULL, spline: None, pending: None, history: Vec::new(), pick_context: None, join_candidates: Vec::new(), delete_source: true }
}
pub fn with_delete_source(mut self, delete: bool) -> Self { self.delete_source = delete; self }
@@ -97,19 +97,19 @@ impl SplineditCommand {
if points.len() < 2 { return None; }
let entity = if let Some(planar) = crate::entities::curve::entity_curve(&EntityType::Spline(source.clone())) {
let normal = planar.plane.normal()?;
let elevation = cadkernel::space::Vec3::from(points[0]).dot(cadkernel::space::Vec3::from(normal));
let elevation = kernel::space::Vec3::from(points[0]).dot(kernel::space::Vec3::from(normal));
let normal = Vector3::new(normal[0], normal[1], normal[2]);
let plane = crate::entities::curve::ocs_plane(normal.clone(), elevation);
let mut polyline = acadrust::LwPolyline::new();
let mut polyline = codec::LwPolyline::new();
polyline.common = source.common.clone();
polyline.elevation = elevation; polyline.normal = normal; polyline.is_closed = self.closed();
polyline.vertices = points.iter().map(|point| {
let uv = plane.project(*point)?;
Some(acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(uv[0], uv[1])))
Some(codec::entities::LwVertex::new(codec::types::Vector2::new(uv[0], uv[1])))
}).collect::<Option<Vec<_>>>()?;
EntityType::LwPolyline(polyline)
} else {
let mut polyline = acadrust::entities::Polyline3D::from_points(points.iter().map(|p| Vector3::new(p[0],p[1],p[2])).collect());
let mut polyline = codec::entities::Polyline3D::from_points(points.iter().map(|p| Vector3::new(p[0],p[1],p[2])).collect());
polyline.common = source.common.clone(); polyline.flags.closed = self.closed();
EntityType::Polyline3D(polyline)
};
@@ -117,7 +117,7 @@ impl SplineditCommand {
});
let Some(mut entity) = result else { return CmdResult::ReportError(crate::t!("Spline cannot be converted within the requested precision.").into_owned()); };
if self.delete_source { CmdResult::ReplaceMany(vec![(self.handle,vec![entity])], Vec::new()) }
else { entity.common_mut().handle = acadrust::Handle::NULL; CmdResult::ReplaceMany(Vec::new(), vec![entity]) }
else { entity.common_mut().handle = codec::Handle::NULL; CmdResult::ReplaceMany(Vec::new(), vec![entity]) }
}
fn picked_vertex(&self, point: DVec3) -> Option<usize> { self.picked_from_points(point, &self.spline.as_ref()?.control_points) }
@@ -132,7 +132,7 @@ impl SplineditCommand {
self.rebuild_fit(result)
}
fn rebuild_fit(&mut self, mut result: acadrust::entities::Spline) -> CmdResult {
fn rebuild_fit(&mut self, mut result: codec::entities::Spline) -> CmdResult {
let Some(source) = self.spline.as_ref() else { return CmdResult::NeedPoint; };
if source.fit_tolerance != 0.0 || source.weights.windows(2).any(|w| w[0] != w[1]) {
return CmdResult::ReportError(crate::t!("Editing weighted or tolerance-fitted interpolation data is not supported.").into_owned());
@@ -201,7 +201,7 @@ impl SplineditCommand {
self.spline.as_ref().is_some_and(|spline| spline.flags.closed || spline.flags.periodic)
}
fn replace(&mut self, spline: acadrust::entities::Spline) -> CmdResult {
fn replace(&mut self, spline: codec::entities::Spline) -> CmdResult {
self.pending = Some(spline.clone());
CmdResult::ReplaceManyContinue(vec![(self.handle, vec![EntityType::Spline(spline)])])
}
@@ -224,7 +224,7 @@ impl SplineditCommand {
CmdResult::ReplaceManyContinue(replacements)
}
fn refined(&self, point: Option<DVec3>, degree: Option<usize>) -> Option<acadrust::entities::Spline> {
fn refined(&self, point: Option<DVec3>, degree: Option<usize>) -> Option<codec::entities::Spline> {
let source = self.spline.as_ref()?;
if let Some(degree) = degree {
let current = usize::try_from(source.degree).ok()?;
@@ -244,10 +244,10 @@ impl SplineditCommand {
return Some(result);
}
let planar = crate::entities::curve::entity_curve(&EntityType::Spline(source.clone()))?;
let cadkernel::geom2d::Curve::Nurbs(mut curve) = planar.curve else { return None; };
let kernel::geom2d::Curve::Nurbs(mut curve) = planar.curve else { return None; };
if let Some(point) = point {
let projected = planar.plane.project([point.x, point.y, point.z])?;
let nearest = cadkernel::geom2d::closest_point(&cadkernel::geom2d::Curve::Nurbs(curve.clone()), projected);
let nearest = kernel::geom2d::closest_point(&kernel::geom2d::Curve::Nurbs(curve.clone()), projected);
let (start, end) = curve.domain();
let parameter = start + nearest.t * (end - start);
if parameter <= start || parameter >= end { return None; }
@@ -321,7 +321,7 @@ impl CadCommand for SplineditCommand {
fn inject_selection_entities(&mut self, entities: Vec<crate::command::SelectionEntity>) {
if matches!(self.step, Step::Join) { self.join_candidates = entities; }
}
fn on_selection_complete(&mut self, handles: Vec<acadrust::Handle>) -> CmdResult {
fn on_selection_complete(&mut self, handles: Vec<codec::Handle>) -> CmdResult {
self.join_candidates.retain(|item| handles.contains(&item.handle) && item.handle != self.handle);
CmdResult::NeedPoint
}
@@ -330,13 +330,13 @@ impl CadCommand for SplineditCommand {
fn inject_picked_entity(&mut self, entity: EntityType) {
self.spline = match entity { EntityType::Spline(spline) => Some(spline), _ => None };
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
fn on_entity_pick(&mut self, handle: codec::Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() || self.spline.is_none() { return CmdResult::NeedPoint; }
self.handle = handle;
self.step = Step::Options;
CmdResult::NeedPoint
}
fn on_entity_replaced(&mut self, old: acadrust::Handle, new: &[acadrust::Handle]) {
fn on_entity_replaced(&mut self, old: codec::Handle, new: &[codec::Handle]) {
if old == self.handle {
if let (Some(&handle), Some(replacement)) = (new.first(), self.pending.take()) {
if let Some(previous) = self.spline.replace(replacement) { self.history.push((old, previous)); }
@@ -514,7 +514,7 @@ impl CadCommand for SplineditCommand {
let controls = source.control_points.iter().map(|point| [point.x, point.y, point.z]).collect();
let weights = if source.weights.is_empty() { vec![1.0; source.control_points.len()] }
else { source.weights.clone() };
let curve = cadkernel::space::NurbsCurve3::new_strict(source.degree as usize, controls,
let curve = kernel::space::NurbsCurve3::new_strict(source.degree as usize, controls,
source.knots.clone(), weights).map(|curve| curve.with_periodicity(source.flags.periodic || source.flags.closed));
let Some(curve) = curve.and_then(|curve| curve.without_control_vertex(index)) else { return CmdResult::NeedPoint; };
let mut spline = source.clone();
@@ -576,14 +576,14 @@ impl CadCommand for SplineditCommand {
}
/// Apply a spline operation (CLOSE/OPEN/REVERSE) to a spline entity.
/// Called from `cmd_result.rs` when the ReplaceEntity sentinel is detected.
pub fn apply_spline_op(doc: &mut acadrust::CadDocument, handle: acadrust::Handle, op: &str) {
pub fn apply_spline_op(doc: &mut codec::CadDocument, handle: codec::Handle, op: &str) {
let Some(EntityType::Spline(spline)) = doc.get_entity_mut(handle) else {
return;
};
apply_to_spline(spline, op);
}
fn apply_to_spline(spline: &mut acadrust::entities::Spline, op: &str) {
fn apply_to_spline(spline: &mut codec::entities::Spline, op: &str) {
let result = match op {
"__SPLINEDIT_CLOSE__" => change_closure(spline, true),
"__SPLINEDIT_OPEN__" => change_closure(spline, false),
@@ -593,8 +593,8 @@ fn apply_to_spline(spline: &mut acadrust::entities::Spline, op: &str) {
if let Some(result) = result { *spline = result; }
}
fn change_closure(source: &acadrust::entities::Spline, closed: bool) -> Option<acadrust::entities::Spline> {
use cadkernel::space::{NurbsCurve3, Parameterization};
fn change_closure(source: &codec::entities::Spline, closed: bool) -> Option<codec::entities::Spline> {
use kernel::space::{NurbsCurve3, Parameterization};
if (source.flags.closed || source.flags.periodic) == closed { return None; }
let fit_method = !source.fit_points.is_empty() || source.dwg_flags1 & 1 != 0 || source.dxf_flags & 32 != 0;
let parameterization = match source.knot_parameterization {
@@ -654,8 +654,8 @@ fn change_closure(source: &acadrust::entities::Spline, closed: bool) -> Option<a
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::Spline;
use acadrust::Handle;
use codec::entities::Spline;
use codec::Handle;
use iced::Rectangle;
fn control_spline(points: &[[f64; 3]], degree: usize) -> Spline {
@@ -665,7 +665,7 @@ mod tests {
.iter()
.map(|point| Vector3::new(point[0], point[1], point[2]))
.collect();
spline.knots = cadkernel::space::clamped_uniform_knots(degree, points.len());
spline.knots = kernel::space::clamped_uniform_knots(degree, points.len());
spline
}
@@ -785,7 +785,7 @@ mod tests {
#[test]
fn a_closed_weighted_control_curve_can_be_opened() {
let curve = cadkernel::space::NurbsCurve3::from_weighted_control_polygon(
let curve = kernel::space::NurbsCurve3::from_weighted_control_polygon(
2,
&[
[0.0, 0.0, 0.0],
@@ -806,7 +806,7 @@ mod tests {
let opened = change_closure(&spline, false).expect("closed control curve opens");
assert!(!opened.flags.closed);
assert!(!opened.flags.periodic);
assert!(cadkernel::space::NurbsCurve3::new_strict(
assert!(kernel::space::NurbsCurve3::new_strict(
opened.degree as usize,
opened
.control_points
@@ -888,7 +888,7 @@ mod tests {
#[test]
fn closed_conversion_closes_once_and_invalid_precision_retries() {
let curve = cadkernel::space::NurbsCurve3::from_control_polygon(
let curve = kernel::space::NurbsCurve3::from_control_polygon(
2,
&[
[0.0, 0.0, 0.0],
@@ -964,13 +964,13 @@ mod tests {
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["SPLINEDIT"] }); // SplineditCommand
fn spatial_spline(source: &acadrust::entities::Spline) -> Option<cadkernel::space::NurbsCurve3> {
fn spatial_spline(source: &codec::entities::Spline) -> Option<kernel::space::NurbsCurve3> {
let curve = if crate::entities::spline::uses_fit_method(source) {
crate::entities::spline::fit_nurbs3(source)?
} else {
let current = usize::try_from(source.degree).ok()?;
let weights = if source.weights.is_empty() { vec![1.0; source.control_points.len()] } else { source.weights.clone() };
cadkernel::space::NurbsCurve3::new_strict(current,
kernel::space::NurbsCurve3::new_strict(current,
source.control_points.iter().map(|point| [point.x, point.y, point.z]).collect(),
source.knots.clone(), weights)?
};
+22 -1
View File
@@ -15,7 +15,7 @@
// Insert : move the whole entity if its insertion point is inside.
// All others : move the whole entity if any point is inside.
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
@@ -67,6 +67,21 @@ impl StretchCommand {
}
}
/// A selection made by a crossing window before the command: that window
/// marks what moves, so the command goes straight to the base point.
pub fn preselected(
handles: Vec<Handle>,
wire_models: Vec<WireModel>,
window: (DVec3, DVec3),
) -> Self {
Self {
handles,
wire_models,
windows: vec![window],
step: Step::Base,
}
}
/// Continue gathering crossing windows after the host has resolved the
/// entities touched by the latest window.
pub fn with_windows(
@@ -223,6 +238,12 @@ impl CadCommand for StretchCommand {
matches!(self.step, Step::WindowCorner1 | Step::WindowCorner2(_))
}
fn selects_by_window(&self) -> bool {
// Crossing windows select what stretches; dynamic input has nothing
// to enter there (#657).
self.window_corner_pick()
}
fn window_first_corner(&self) -> Option<DVec3> {
// Expose the first corner so the host draws a filled crossing marquee to
// the cursor, matching a normal box selection instead of a bare outline.
+2 -2
View File
@@ -5,13 +5,13 @@
// Step 1: Wait for an angle (numerical input), Enter (Most Readable), or pick first point.
// Step 2: If first point picked, pick second point to define angle vector.
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult, DynField};
use crate::scene::model::wire_model::WireModel;
use acadrust::EntityType;
use codec::EntityType;
// ── Command implementation ─────────────────────────────────────────────────
+1 -1
View File
@@ -6,7 +6,7 @@
// Step 2: pick destination → translates all selected entities by (dest - base);
// Enter uses the base point as the displacement (commercial solutions).
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, EntityTransform};
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -13,7 +13,7 @@ pub fn tool() -> ToolDef {
// ── CadCommand implementation ─────────────────────────────────────────────
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
+15 -1
View File
@@ -8,7 +8,7 @@
// Single-object commands (e.g. LAYMCUR) use `instant()` instead: the first
// completed selection action fires straight away, no Enter required.
use acadrust::Handle;
use codec::Handle;
use glam::DVec3;
use crate::t;
@@ -124,6 +124,13 @@ impl SelectObjectsCommand {
command
}
/// The standard gather with the command's own wording.
pub fn with_prompt(prompt_cmd: &str, pending_cmd: &str, prompt_text: &'static str) -> Self {
let mut command = Self::plain(prompt_cmd, pending_cmd);
command.prompt_text = Some(prompt_text);
command
}
pub fn auto_constrain(pending_cmd: &str) -> Self {
let mut command = Self::new(pending_cmd);
command.auto_constrain_settings = true;
@@ -196,6 +203,13 @@ impl CadCommand for SelectObjectsCommand {
self.associative_dimensions_only
}
fn selection_drops_locked_point_clouds(&self) -> bool {
matches!(
self.pending_cmd.as_str(),
"MOVE" | "3DMOVE" | "ROTATE" | "3DROTATE" | "SCALE" | "MIRROR" | "MIRROR3D" | "STRETCH"
)
}
// Clickable selection keywords (#426, #596). Window and Crossing fix the
// sense of the next box, which dragging would otherwise take from the
// direction the corner travels; All takes the whole space; Add and Remove
+1 -1
View File
@@ -145,7 +145,7 @@ pub fn conversion_factor(from: i16, to: i16) -> Option<f64> {
/// Paper space is deliberately left out. Its geometry is measured on the sheet,
/// in sheet units, and means the same thing however the model is labelled —
/// scaling it would resize the drawing frame along with the building.
pub fn model_space_handles(scene: &crate::scene::Scene) -> Vec<acadrust::Handle> {
pub fn model_space_handles(scene: &crate::scene::Scene) -> Vec<codec::Handle> {
let Some(model) = scene.document.block_records.get("*Model_Space") else {
return Vec::new();
};