forked from 1b-agent/opencadstudio-upstream
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:
625 files changed
+104397
-28053
No files matched your search
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+1521
-67
File diff suppressed because it is too large.
Load diff
+1090
-183
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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(_))));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
pub mod apollonius;
|
||||
pub mod arc;
|
||||
pub mod attdef;
|
||||
pub mod centerline;
|
||||
|
||||
@@ -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"]
|
||||
});
|
||||
@@ -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
@@ -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,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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) =
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn tool() -> ToolDef {
|
||||
|
||||
// ── CadCommand implementation ─────────────────────────────────────────────
|
||||
|
||||
use acadrust::Handle;
|
||||
use codec::Handle;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn tool() -> ToolDef {
|
||||
|
||||
// ── CadCommand implementation ─────────────────────────────────────────────
|
||||
|
||||
use acadrust::Handle;
|
||||
use codec::Handle;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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") };
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn tool() -> ToolDef {
|
||||
|
||||
// ── CadCommand implementation ─────────────────────────────────────────────
|
||||
|
||||
use acadrust::Handle;
|
||||
use codec::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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![];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)?
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
+983
-111
File diff suppressed because it is too large.
Load diff
@@ -13,7 +13,7 @@ pub fn tool() -> ToolDef {
|
||||
|
||||
// ── CadCommand implementation ─────────────────────────────────────────────
|
||||
|
||||
use acadrust::Handle;
|
||||
use codec::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user