first commit

This commit is contained in:
Hakan Seven committed 2026-03-22 14:56:27 +03:00
1 parent d52c6440ff
commit d2fdac7f1e
274 files changed
+89330

No files matched your search

File diff suppressed because it is too large. Load diff
+912
View File
@@ -0,0 +1,912 @@
// Circle tool — ribbon dropdown + all H7CAD circle creation methods.
//
// Methods:
// CIRCLE — Center, Radius (default)
// CIRCLE_CD — Center, Diameter
// CIRCLE_2P — 2-Point (two endpoints of diameter)
// CIRCLE_3P — 3-Point (circumscribed circle through 3 points)
// CIRCLE_TTR — Tan, Tan, Radius (two tangent objects + radius)
// CIRCLE_TTT — Tan, Tan, Tan (inscribed circle tangent to three objects)
use acadrust::types::Vector3;
use acadrust::{Circle, EntityType};
use crate::command::{CadCommand, CmdResult, TangentObject};
use crate::modules::home::defaults;
use crate::modules::IconKind;
use crate::scene::wire_model::WireModel;
use glam::Vec3;
const TAU: f32 = std::f32::consts::TAU;
// ── Per-method SVG icons ───────────────────────────────────────────────────
const ICON_CR: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/circle/circle_cr.svg"
));
const ICON_CD: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/circle/circle_cd.svg"
));
const ICON_2P: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/circle/circle_2p.svg"
));
const ICON_3P: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/circle/circle_3p.svg"
));
const ICON_TTR: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/circle/circle_ttr.svg"
));
const ICON_TTT: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/circle/circle_ttt.svg"
));
// ── Dropdown metadata (used by ribbon.rs) ─────────────────────────────────
pub const DROPDOWN_ID: &str = "CIRCLE";
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
("CIRCLE", "Center, Radius", ICON_CR),
("CIRCLE_CD", "Center, Diameter", ICON_CD),
("CIRCLE_2P", "2-Point", ICON_2P),
("CIRCLE_3P", "3-Point", ICON_3P),
("CIRCLE_TTR", "Tan, Tan, Radius", ICON_TTR),
("CIRCLE_TTT", "Tan, Tan, Tan", ICON_TTT),
];
/// Default icon — shown until first use (falls back to Center, Radius).
pub const ICON: IconKind = ICON_CR;
// ── Shared geometry ────────────────────────────────────────────────────────
fn circle_wire(center: Vec3, radius: f32) -> WireModel {
let segs = 64u32;
let mut pts: Vec<[f32; 3]> = (0..=segs)
.map(|i| {
let a = (i as f32) * TAU / segs as f32;
[
center.x + radius * a.cos(),
center.y + radius * a.sin(),
center.z,
]
})
.collect();
if let Some(first) = pts.first().cloned() {
pts.push(first);
}
WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false)
}
fn make_circle(center: Vec3, radius: f64) -> EntityType {
EntityType::Circle(Circle {
center: Vector3::new(center.x as f64, center.y as f64, center.z as f64),
radius,
..Default::default()
})
}
/// Circumscribed circle through three points.
/// Returns `None` if the points are collinear.
fn circumcircle(a: Vec3, b: Vec3, c: Vec3) -> Option<(Vec3, f32)> {
let ax = a.x;
let ay = a.y;
let bx = b.x;
let by = b.y;
let cx = c.x;
let cy = c.y;
let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if d.abs() < 1e-9 {
return None;
}
let ux = ((ax * ax + ay * ay) * (by - cy)
+ (bx * bx + by * by) * (cy - ay)
+ (cx * cx + cy * cy) * (ay - by))
/ d;
let uy = ((ax * ax + ay * ay) * (cx - bx)
+ (bx * bx + by * by) * (ax - cx)
+ (cx * cx + cy * cy) * (bx - ax))
/ d;
let center = Vec3::new(ux, uy, a.z);
Some((center, center.distance(a)))
}
// ── Command: Center, Radius ────────────────────────────────────────────────
pub struct CircleCommand {
step: StepCR,
default_r: f32,
}
enum StepCR {
Center,
Radius(Vec3),
}
impl CircleCommand {
pub fn new() -> Self {
Self {
step: StepCR::Center,
default_r: defaults::get_circle_radius(),
}
}
}
impl CadCommand for CircleCommand {
fn name(&self) -> &'static str {
"CIRCLE"
}
fn prompt(&self) -> String {
match &self.step {
StepCR::Center => "CIRCLE Specify center point:".into(),
StepCR::Radius(c) => format!(
"CIRCLE Specify radius or type value <{:.4}> [center ({:.3},{:.3})]:",
self.default_r, c.x, c.y
),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.step {
StepCR::Center => {
self.step = StepCR::Radius(pt);
CmdResult::NeedPoint
}
StepCR::Radius(c) => {
let r = c.distance(pt);
defaults::set_circle_radius(r);
CmdResult::CommitAndExit(make_circle(*c, r as f64))
}
}
}
fn on_enter(&mut self) -> CmdResult {
if let StepCR::Radius(c) = &self.step {
let c = *c;
let r = self.default_r;
return CmdResult::CommitAndExit(make_circle(c, r as f64));
}
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
if let StepCR::Radius(c) = &self.step {
Some(circle_wire(*c, c.distance(pt)))
} else {
None
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let StepCR::Radius(c) = &self.step {
let r: f32 = text.trim().replace(',', ".").parse().ok()?;
if r > 0.0 {
defaults::set_circle_radius(r);
return Some(CmdResult::CommitAndExit(make_circle(*c, r as f64)));
}
}
None
}
}
// ── Command: Center, Diameter ──────────────────────────────────────────────
pub struct CircleCDCommand {
step: StepCR,
default_d: f32,
}
impl CircleCDCommand {
pub fn new() -> Self {
Self {
step: StepCR::Center,
default_d: defaults::get_circle_diam(),
}
}
}
impl CadCommand for CircleCDCommand {
fn name(&self) -> &'static str {
"CIRCLE_CD"
}
fn prompt(&self) -> String {
match &self.step {
StepCR::Center => "CIRCLE CD Specify center point:".into(),
StepCR::Radius(c) => format!(
"CIRCLE CD Specify diameter or type value <{:.4}> [center ({:.3},{:.3})]:",
self.default_d, c.x, c.y
),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.step {
StepCR::Center => {
self.step = StepCR::Radius(pt);
CmdResult::NeedPoint
}
StepCR::Radius(c) => {
let d = c.distance(pt) * 2.0;
defaults::set_circle_diam(d);
CmdResult::CommitAndExit(make_circle(*c, (d / 2.0) as f64))
}
}
}
fn on_enter(&mut self) -> CmdResult {
if let StepCR::Radius(c) = &self.step {
let c = *c;
let d = self.default_d;
return CmdResult::CommitAndExit(make_circle(c, (d / 2.0) as f64));
}
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
// Preview radius = distance to cursor; on commit that distance becomes the radius (diameter = 2x).
if let StepCR::Radius(c) = &self.step {
Some(circle_wire(*c, c.distance(pt)))
} else {
None
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let StepCR::Radius(c) = &self.step {
let d: f32 = text.trim().replace(',', ".").parse().ok()?;
if d > 0.0 {
defaults::set_circle_diam(d);
return Some(CmdResult::CommitAndExit(make_circle(*c, (d / 2.0) as f64)));
}
}
None
}
}
// ── Command: 2-Point ──────────────────────────────────────────────────────
pub struct Circle2PCommand {
p1: Option<Vec3>,
}
impl Circle2PCommand {
pub fn new() -> Self {
Self { p1: None }
}
}
impl CadCommand for Circle2PCommand {
fn name(&self) -> &'static str {
"CIRCLE_2P"
}
fn prompt(&self) -> String {
if self.p1.is_none() {
"CIRCLE 2P Specify first end of diameter:".into()
} else {
"CIRCLE 2P Specify second end of diameter:".into()
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.p1 {
None => {
self.p1 = Some(pt);
CmdResult::NeedPoint
}
Some(p1) => {
let center = (p1 + pt) * 0.5;
let radius = p1.distance(pt) as f64 / 2.0;
CmdResult::CommitAndExit(make_circle(center, radius))
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
let p1 = self.p1?;
let center = (p1 + pt) * 0.5;
let radius = p1.distance(pt) / 2.0;
Some(circle_wire(center, radius))
}
}
// ── Command: 3-Point ──────────────────────────────────────────────────────
pub struct Circle3PCommand {
pts: Vec<Vec3>,
}
impl Circle3PCommand {
pub fn new() -> Self {
Self { pts: Vec::new() }
}
}
impl CadCommand for Circle3PCommand {
fn name(&self) -> &'static str {
"CIRCLE_3P"
}
fn prompt(&self) -> String {
match self.pts.len() {
0 => "CIRCLE 3P Specify first point:".into(),
1 => "CIRCLE 3P Specify second point:".into(),
_ => "CIRCLE 3P Specify third point:".into(),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
self.pts.push(pt);
if self.pts.len() < 3 {
return CmdResult::NeedPoint;
}
let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]);
match circumcircle(a, b, c) {
Some((center, radius)) => CmdResult::CommitAndExit(make_circle(center, radius as f64)),
None => {
self.pts.pop();
CmdResult::NeedPoint
} // collinear — ask again
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
match self.pts.len() {
0 => None,
1 => {
// Show circle preview with p1→cursor as diameter (same as 2P).
let p1 = self.pts[0];
let center = (p1 + pt) * 0.5;
let radius = p1.distance(pt) / 2.0;
Some(circle_wire(center, radius))
}
_ => {
// Show circumcircle preview if non-collinear, else polyline.
let (a, b) = (self.pts[0], self.pts[1]);
if let Some((center, radius)) = circumcircle(a, b, pt) {
Some(circle_wire(center, radius))
} else {
Some(WireModel::solid(
"rubber_band".into(),
vec![[a.x, a.y, a.z], [b.x, b.y, b.z], [pt.x, pt.y, pt.z]],
WireModel::CYAN,
false,
))
}
}
}
}
}
// ── 2-D geometry for TTR/TTT ────────────────────────────────────
#[derive(Clone, Copy)]
struct Line2D {
a: f32,
b: f32,
c: f32,
} // ax + by + c = 0, a²+b²=1
impl Line2D {
fn from_obj(p1: Vec3, p2: Vec3) -> Self {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-9 {
return Self {
a: 1.0,
b: 0.0,
c: -p1.x,
};
}
let a = -dy / len;
let b = dx / len;
Self {
a,
b,
c: -(a * p1.x + b * p1.y),
}
}
}
fn line_line_isect(l1: Line2D, l2: Line2D) -> Option<Vec3> {
let det = l1.a * l2.b - l2.a * l1.b;
if det.abs() < 1e-9 {
return None;
}
let x = (-l1.c * l2.b + l2.c * l1.b) / det;
let y = (-l1.a * l2.c + l2.a * l1.c) / det;
Some(Vec3::new(x, y, 0.0))
}
fn solve_quadratic(a: f32, b: f32, c: f32) -> Vec<f32> {
if a.abs() < 1e-9 {
if b.abs() < 1e-9 {
return vec![];
}
return vec![-c / b];
}
let disc = b * b - 4.0 * a * c;
if disc < 0.0 {
return vec![];
}
let sq = disc.sqrt();
vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
}
fn best_of(candidates: &[Vec3], hint: Vec3) -> Option<Vec3> {
candidates.iter().copied().min_by(|a, b| {
a.distance(hint)
.partial_cmp(&b.distance(hint))
.unwrap_or(std::cmp::Ordering::Equal)
})
}
fn best_circle_of(candidates: &[(Vec3, f32)], hint: Vec3) -> Option<(Vec3, f32)> {
candidates
.iter()
.copied()
.filter(|&(_, r)| r > 1e-4)
.min_by(|(a, _), (b, _)| {
a.distance(hint)
.partial_cmp(&b.distance(hint))
.unwrap_or(std::cmp::Ordering::Equal)
})
}
/// All candidate circle centers tangent to two objects with radius r.
fn ttr_candidates(obj1: TangentObject, obj2: TangentObject, r: f32) -> Vec<Vec3> {
match (obj1, obj2) {
(TangentObject::Line { p1: a, p2: b }, TangentObject::Line { p1: c, p2: d }) => {
let l1 = Line2D::from_obj(a, b);
let l2 = Line2D::from_obj(c, d);
let mut out = Vec::new();
for s1 in [-r, r] {
for s2 in [-r, r] {
let ol1 = Line2D { c: l1.c + s1, ..l1 };
let ol2 = Line2D { c: l2.c + s2, ..l2 };
if let Some(pt) = line_line_isect(ol1, ol2) {
out.push(pt);
}
}
}
out
}
(
TangentObject::Line { p1, p2 },
TangentObject::Circle {
center: cp,
radius: cr,
},
)
| (
TangentObject::Circle {
center: cp,
radius: cr,
},
TangentObject::Line { p1, p2 },
) => {
let l = Line2D::from_obj(p1, p2);
ttr_lc_candidates(l, cp, cr, r)
}
(
TangentObject::Circle {
center: c1,
radius: r1,
},
TangentObject::Circle {
center: c2,
radius: r2,
},
) => ttr_cc_candidates(c1, r1, c2, r2, r),
}
}
fn ttr_lc_candidates(l: Line2D, cp: Vec3, cr: f32, r: f32) -> Vec<Vec3> {
let mut out = Vec::new();
for s1 in [-1.0f32, 1.0] {
for s2 in [-1.0f32, 1.0] {
let rho = cr + s2 * r;
if rho < 0.0 {
continue;
}
let c_off = l.c + s1 * r;
// The candidate center (cx, cy) satisfies:
// l.a*cx + l.b*cy + c_off = 0
// (cx - cp.x)^2 + (cy - cp.y)^2 = rho^2
// Parameterise cx = -l.b*t - l.a*c_off, cy = l.a*t - l.b*c_off
// where t is the free parameter along the offset line.
// Substituting into the circle equation:
// (-l.b*t - l.a*c_off - cp.x)^2 + (l.a*t - l.b*c_off - cp.y)^2 = rho^2
// Let alpha = l.a*c_off + cp.x, beta = l.b*c_off + cp.y (using sign convention)
// Actually: u = -l.a*c_off - cp.x, v = -l.b*c_off - cp.y
let u = -(l.a * c_off) - cp.x;
let v = -(l.b * c_off) - cp.y;
// (-l.b*t + u)^2 + (l.a*t + v)^2 = rho^2
// (l.b^2 + l.a^2)*t^2 + 2*(-l.b*u + l.a*v)*t + (u^2 + v^2 - rho^2) = 0
// Since a^2 + b^2 = 1:
let qa = 1.0f32;
let qb = 2.0 * (-l.b * u + l.a * v);
let qc = u * u + v * v - rho * rho;
for t in solve_quadratic(qa, qb, qc) {
let cx = -l.b * t - l.a * c_off;
let cy = l.a * t - l.b * c_off;
out.push(Vec3::new(cx, cy, 0.0));
}
}
}
out
}
fn ttr_cc_candidates(c1: Vec3, r1: f32, c2: Vec3, r2: f32, r: f32) -> Vec<Vec3> {
let mut out = Vec::new();
for s1 in [-1.0f32, 1.0] {
for s2 in [-1.0f32, 1.0] {
let rho1 = r1 + s1 * r;
let rho2 = r2 + s2 * r;
if rho1 < 0.0 || rho2 < 0.0 {
continue;
}
let ax = c2.x - c1.x;
let ay = c2.y - c1.y;
let k = 0.5
* (rho1 * rho1 - rho2 * rho2 + c2.x * c2.x + c2.y * c2.y
- c1.x * c1.x
- c1.y * c1.y);
let a2 = ax * ax + ay * ay;
if a2 < 1e-12 {
continue;
}
if ay.abs() >= ax.abs() {
let iy = 1.0 / ay;
let qa = 1.0 + ax * ax * iy * iy;
let p = k * iy - c1.y;
let qb = -2.0 * (c1.x + ax * iy * p);
let qc = c1.x * c1.x + p * p - rho1 * rho1;
let disc = qb * qb - 4.0 * qa * qc;
if disc < 0.0 {
continue;
}
for sign in [-1.0f32, 1.0] {
let cx = (-qb + sign * disc.sqrt()) / (2.0 * qa);
let cy = (k - ax * cx) * iy;
out.push(Vec3::new(cx, cy, 0.0));
}
} else {
let ix = 1.0 / ax;
let qa = ay * ay * ix * ix + 1.0;
let p = k * ix - c1.x;
let qb = -2.0 * (ay * ix * p + c1.y);
let qc = p * p + c1.y * c1.y - rho1 * rho1;
let disc = qb * qb - 4.0 * qa * qc;
if disc < 0.0 {
continue;
}
for sign in [-1.0f32, 1.0] {
let cy = (-qb + sign * disc.sqrt()) / (2.0 * qa);
let cx = (k - ay * cy) * ix;
out.push(Vec3::new(cx, cy, 0.0));
}
}
}
}
out
}
/// Unified TTT solver: circle tangent to three objects. Returns all (center, radius) candidates.
fn ttt_candidates(
obj1: TangentObject,
obj2: TangentObject,
obj3: TangentObject,
) -> Vec<(Vec3, f32)> {
let objs = [obj1, obj2, obj3];
let mut results = Vec::new();
let sign_combos: [[f32; 3]; 8] = [
[-1., -1., -1.],
[-1., -1., 1.],
[-1., 1., -1.],
[-1., 1., 1.],
[1., -1., -1.],
[1., -1., 1.],
[1., 1., -1.],
[1., 1., 1.],
];
for eps in &sign_combos {
for (center, r) in ttt_solve_sign(&objs, eps) {
if r > 1e-4 {
results.push((center, r));
}
}
}
results
}
// LinEq: lx*cx + ly*cy + lr*r = k
struct LinEq {
lx: f32,
ly: f32,
lr: f32,
k: f32,
}
fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f32; 3]) -> Vec<(Vec3, f32)> {
let mut lin_eqs: Vec<LinEq> = Vec::new();
let mut circle_idx: Vec<usize> = Vec::new();
for (i, &obj) in objs.iter().enumerate() {
match obj {
TangentObject::Line { p1, p2 } => {
let l = Line2D::from_obj(p1, p2);
lin_eqs.push(LinEq {
lx: l.a,
ly: l.b,
lr: -eps[i],
k: -l.c,
});
}
TangentObject::Circle { .. } => {
circle_idx.push(i);
}
}
}
// Circle-pair differences → additional linear equations
for j in 1..circle_idx.len() {
let i0 = circle_idx[0];
let i1 = circle_idx[j];
if let (
TangentObject::Circle {
center: p0,
radius: r0,
},
TangentObject::Circle {
center: p1,
radius: r1,
},
) = (objs[i0], objs[i1])
{
let lx = 2.0 * (p1.x - p0.x);
let ly = 2.0 * (p1.y - p0.y);
let lr = -2.0 * (r0 * eps[i0] - r1 * eps[i1]);
let k = (r0 * r0 - r1 * r1) + p1.x * p1.x + p1.y * p1.y - p0.x * p0.x - p0.y * p0.y;
lin_eqs.push(LinEq { lx, ly, lr, k });
}
}
if lin_eqs.len() < 2 {
return vec![];
}
let e0 = &lin_eqs[0];
let e1 = &lin_eqs[1];
let det = e0.lx * e1.ly - e1.lx * e0.ly;
if det.abs() < 1e-9 {
return vec![];
}
// cx = a_cx + b_cx*r, cy = a_cy + b_cy*r
let a_cx = (e0.k * e1.ly - e1.k * e0.ly) / det;
let b_cx = -(e0.lr * e1.ly - e1.lr * e0.ly) / det;
let a_cy = (e0.lx * e1.k - e1.lx * e0.k) / det;
let b_cy = -(e0.lx * e1.lr - e1.lx * e0.lr) / det;
let r_vals: Vec<f32> = if lin_eqs.len() >= 3 {
let e2 = &lin_eqs[2];
let r_coeff = b_cx * e2.lx + b_cy * e2.ly + e2.lr;
let r_const = e2.k - a_cx * e2.lx - a_cy * e2.ly;
if r_coeff.abs() < 1e-9 {
vec![]
} else {
vec![r_const / r_coeff]
}
} else if !circle_idx.is_empty() {
let ci = circle_idx[0];
if let TangentObject::Circle {
center: cp,
radius: cr,
} = objs[ci]
{
let p = a_cx - cp.x;
let q = a_cy - cp.y;
let e = eps[ci];
let a_q = b_cx * b_cx + b_cy * b_cy - e * e;
let b_q = 2.0 * (p * b_cx + q * b_cy - cr * e);
let c_q = p * p + q * q - cr * cr;
solve_quadratic(a_q, b_q, c_q)
} else {
vec![]
}
} else {
vec![]
};
r_vals
.into_iter()
.filter_map(|r| {
if r <= 1e-4 {
return None;
}
let cx = a_cx + b_cx * r;
let cy = a_cy + b_cy * r;
Some((Vec3::new(cx, cy, 0.0), r))
})
.collect()
}
// ── Command: Tan, Tan, Radius ──────────────────────────────────────────────
pub struct CircleTTRCommand {
step: StepTTR,
}
enum StepTTR {
First,
Second {
obj1: TangentObject,
hit1: Vec3,
},
Radius {
obj1: TangentObject,
obj2: TangentObject,
hit1: Vec3,
hit2: Vec3,
},
}
impl CircleTTRCommand {
pub fn new() -> Self {
Self {
step: StepTTR::First,
}
}
}
impl CadCommand for CircleTTRCommand {
fn name(&self) -> &'static str {
"CIRCLE_TTR"
}
fn needs_tangent_pick(&self) -> bool {
matches!(self.step, StepTTR::First | StepTTR::Second { .. })
}
fn wants_text_input(&self) -> bool {
matches!(self.step, StepTTR::Radius { .. })
}
fn prompt(&self) -> String {
match &self.step {
StepTTR::First => "CIRCLE TTR Select first tangent object:".into(),
StepTTR::Second { .. } => "CIRCLE TTR Select second tangent object:".into(),
StepTTR::Radius { .. } => "CIRCLE TTR Specify radius:".into(),
}
}
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult {
match &self.step {
StepTTR::First => {
self.step = StepTTR::Second {
obj1: obj,
hit1: hit,
};
CmdResult::NeedPoint
}
StepTTR::Second { obj1, hit1 } => {
let (o1, h1) = (*obj1, *hit1);
self.step = StepTTR::Radius {
obj1: o1,
obj2: obj,
hit1: h1,
hit2: hit,
};
CmdResult::NeedPoint
}
StepTTR::Radius { .. } => CmdResult::NeedPoint,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let r: f32 = text.trim().parse().ok()?;
if r <= 0.0 {
return Some(CmdResult::Cancel);
}
if let StepTTR::Radius {
obj1,
obj2,
hit1,
hit2,
} = &self.step
{
let hint = (*hit1 + *hit2) * 0.5;
let candidates = ttr_candidates(*obj1, *obj2, r);
if let Some(center) = best_of(&candidates, hint) {
Some(CmdResult::CommitAndExit(make_circle(center, r as f64)))
} else {
Some(CmdResult::Cancel)
}
} else {
None
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> {
None
}
}
// ── Command: Tan, Tan, Tan ────────────────────────────────────────────────
pub struct CircleTTTCommand {
objs: Vec<TangentObject>,
hits: Vec<Vec3>,
}
impl CircleTTTCommand {
pub fn new() -> Self {
Self {
objs: Vec::new(),
hits: Vec::new(),
}
}
}
impl CadCommand for CircleTTTCommand {
fn name(&self) -> &'static str {
"CIRCLE_TTT"
}
fn needs_tangent_pick(&self) -> bool {
self.objs.len() < 3
}
fn prompt(&self) -> String {
match self.objs.len() {
0 => "CIRCLE TTT Select first tangent object:".into(),
1 => "CIRCLE TTT Select second tangent object:".into(),
_ => "CIRCLE TTT Select third tangent object:".into(),
}
}
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult {
self.objs.push(obj);
self.hits.push(hit);
if self.objs.len() < 3 {
return CmdResult::NeedPoint;
}
let hint = self.hits.iter().fold(Vec3::ZERO, |a, &b| a + b) / 3.0;
let candidates = ttt_candidates(self.objs[0], self.objs[1], self.objs[2]);
match best_circle_of(&candidates, hint) {
Some((center, r)) => CmdResult::CommitAndExit(make_circle(center, r as f64)),
None => {
self.objs.pop();
self.hits.pop();
CmdResult::NeedPoint
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> {
None
}
}
+529
View File
@@ -0,0 +1,529 @@
// Ellipse tool — ribbon dropdown + all H7CAD ellipse creation methods.
//
// Commands:
// ELLIPSE — Center, Axes (center → major endpoint → minor distance)
// 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 crate::command::{CadCommand, CmdResult};
use crate::modules::IconKind;
use crate::scene::wire_model::WireModel;
use glam::Vec3;
fn parse_f32(text: &str) -> Option<f32> {
text.trim().replace(',', ".").parse().ok()
}
const TAU: f32 = std::f32::consts::TAU;
// ── Icons ─────────────────────────────────────────────────────────────────
const ICON_CTR: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/ellipse/ellipse_ctr.svg"
));
const ICON_AXIS: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/ellipse/ellipse_axis.svg"
));
const ICON_ARC: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/ellipse/ellipse_arc.svg"
));
// ── Dropdown metadata ─────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "ELLIPSE";
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
("ELLIPSE", "Center, Axes", ICON_CTR),
("ELLIPSE_AXIS", "Axis, End", ICON_AXIS),
("ELLIPSE_ARC", "Ellipse Arc", ICON_ARC),
];
pub const ICON: IconKind = ICON_CTR;
// ── Shared helpers ────────────────────────────────────────────────────────
/// Preview wire for a full or partial ellipse.
fn ellipse_wire(
center: Vec3,
major: Vec3, // vector from center to major-axis endpoint
ratio: f32, // minor/major
t_start: f32,
t_end: f32,
) -> WireModel {
let r_major = major.length();
if r_major < 1e-9 {
return WireModel::solid("rubber_band".into(), vec![], WireModel::CYAN, false);
}
let major_dir = major / r_major;
let v = major_dir.cross(Vec3::Z).normalize();
let segs = 64u32;
// Unwrap t_end so the arc goes counter-clockwise.
let t_e = if t_end <= t_start { t_end + TAU } else { t_end };
let pts: Vec<[f32; 3]> = (0..=segs)
.map(|i| {
let t = t_start + (t_e - t_start) * (i as f32 / segs as f32);
let p = center + t.cos() * r_major * major_dir + t.sin() * r_major * ratio * v;
[p.x, p.y, p.z]
})
.collect();
WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false)
}
/// Convert a world point to the parametric angle on the ellipse.
fn param_angle(center: Vec3, major_dir: Vec3, v: Vec3, pt: Vec3, ratio: f32) -> f32 {
let d = pt - center;
let u_proj = d.dot(major_dir);
let v_proj = d.dot(v);
// Inverse-map: on ellipse x=cos(t)*r_major, y=sin(t)*r_major*ratio
// → t = atan2(v_proj / (r_major*ratio), u_proj / r_major) but we normalise
v_proj.atan2(u_proj * ratio).rem_euclid(TAU)
}
/// Build the final Ellipse entity.
fn make_ellipse(center: Vec3, major: Vec3, ratio: f32, t_start: f32, t_end: f32) -> Ellipse {
Ellipse {
center: Vector3::new(center.x as f64, center.y as f64, center.z as f64),
major_axis: Vector3::new(major.x as f64, major.y as f64, major.z as f64),
minor_axis_ratio: ratio as f64,
start_parameter: t_start as f64,
end_parameter: t_end as f64,
..Default::default()
}
}
// ── 1. Center mode ────────────────────────────────────────────────────────
// Step 1: center Step 2: major-axis endpoint Step 3: minor-axis point
enum CtrStep {
Center,
MajorAxis { center: Vec3 },
MinorRatio { center: Vec3, major: Vec3 },
}
pub struct EllipseCommand {
step: CtrStep,
}
impl EllipseCommand {
pub fn new() -> Self {
Self {
step: CtrStep::Center,
}
}
}
impl CadCommand for EllipseCommand {
fn name(&self) -> &'static str {
"ELLIPSE"
}
fn prompt(&self) -> String {
match &self.step {
CtrStep::Center => "ELLIPSE Specify center:".into(),
CtrStep::MajorAxis { .. } => "ELLIPSE Specify major axis endpoint:".into(),
CtrStep::MinorRatio { major, .. } => format!(
"ELLIPSE Specify minor axis point or type half-length [major r={:.3}]:",
major.length()
),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.step {
CtrStep::Center => {
self.step = CtrStep::MajorAxis { center: pt };
CmdResult::NeedPoint
}
CtrStep::MajorAxis { center } => {
let center = *center;
self.step = CtrStep::MinorRatio {
center,
major: pt - center,
};
CmdResult::NeedPoint
}
CtrStep::MinorRatio { center, major } => {
let (center, major) = (*center, *major);
let ratio = minor_ratio(center, major, pt);
CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
center, major, ratio, 0.0, TAU,
)))
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let CtrStep::MinorRatio { center, major } = &self.step {
let r_minor = parse_f32(text)?;
if r_minor > 0.0 {
let ratio = (r_minor / major.length()).clamp(1e-6, 1.0);
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
*center, *major, ratio, 0.0, TAU,
))));
}
}
None
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
match &self.step {
CtrStep::MajorAxis { center } => Some(line_wire(*center, pt)),
CtrStep::MinorRatio { center, major } => {
let ratio = minor_ratio(*center, *major, pt).max(0.001);
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU))
}
_ => None,
}
}
}
// ── 2. Axis, End mode ─────────────────────────────────────────────────────
// Step 1: axis endpoint 1 Step 2: axis endpoint 2 Step 3: minor-axis point
enum AxisStep {
Pt1,
Pt2 { p1: Vec3 },
MinorRatio { center: Vec3, major: Vec3 },
}
pub struct EllipseAxisCommand {
step: AxisStep,
}
impl EllipseAxisCommand {
pub fn new() -> Self {
Self {
step: AxisStep::Pt1,
}
}
}
impl CadCommand for EllipseAxisCommand {
fn name(&self) -> &'static str {
"ELLIPSE_AXIS"
}
fn prompt(&self) -> String {
match &self.step {
AxisStep::Pt1 => "ELLIPSE (Axis) Specify first endpoint of major axis:".into(),
AxisStep::Pt2 { .. } => "ELLIPSE (Axis) Specify second endpoint of major axis:".into(),
AxisStep::MinorRatio { major, .. } => format!(
"ELLIPSE (Axis) Specify minor axis point or type half-length [major r={:.3}]:",
major.length()
),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.step {
AxisStep::Pt1 => {
self.step = AxisStep::Pt2 { p1: pt };
CmdResult::NeedPoint
}
AxisStep::Pt2 { p1 } => {
let center = (*p1 + pt) * 0.5;
let major = pt - center; // half-vector
self.step = AxisStep::MinorRatio { center, major };
CmdResult::NeedPoint
}
AxisStep::MinorRatio { center, major } => {
let (center, major) = (*center, *major);
let ratio = minor_ratio(center, major, pt);
CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
center, major, ratio, 0.0, TAU,
)))
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let AxisStep::MinorRatio { center, major } = &self.step {
let r_minor = parse_f32(text)?;
if r_minor > 0.0 {
let ratio = (r_minor / major.length()).clamp(1e-6, 1.0);
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
*center, *major, ratio, 0.0, TAU,
))));
}
}
None
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
match &self.step {
AxisStep::Pt1 => None,
AxisStep::Pt2 { p1 } => Some(line_wire(*p1, pt)),
AxisStep::MinorRatio { center, major } => {
let ratio = minor_ratio(*center, *major, pt).max(0.001);
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU))
}
}
}
}
// ── 3. Ellipse Arc mode ───────────────────────────────────────────────────
// Same shape steps as Center mode, then: start parameter, end parameter.
enum ArcStep {
Center,
MajorAxis {
center: Vec3,
},
MinorRatio {
center: Vec3,
major: Vec3,
},
StartAngle {
center: Vec3,
major: Vec3,
ratio: f32,
},
EndAngle {
center: Vec3,
major: Vec3,
ratio: f32,
t_start: f32,
},
}
pub struct EllipseArcCommand {
step: ArcStep,
prev_pt: Option<Vec3>,
cw: bool,
}
impl EllipseArcCommand {
pub fn new() -> Self {
Self {
step: ArcStep::Center,
prev_pt: None,
cw: false,
}
}
}
impl CadCommand for EllipseArcCommand {
fn name(&self) -> &'static str {
"ELLIPSE_ARC"
}
fn prompt(&self) -> String {
match &self.step {
ArcStep::Center => "ELLIPSE ARC Specify center:".into(),
ArcStep::MajorAxis { .. } => "ELLIPSE ARC Specify major axis endpoint:".into(),
ArcStep::MinorRatio { major, .. } => format!(
"ELLIPSE ARC Specify minor axis point or type half-length [major r={:.3}]:",
major.length()
),
ArcStep::StartAngle { .. } => {
"ELLIPSE ARC Specify start angle point or type degrees:".into()
}
ArcStep::EndAngle { t_start, .. } => format!(
"ELLIPSE ARC Specify end angle point or type degrees [start={:.1}°]:",
t_start.to_degrees()
),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.step {
ArcStep::Center => {
self.step = ArcStep::MajorAxis { center: pt };
CmdResult::NeedPoint
}
ArcStep::MajorAxis { center } => {
let center = *center;
self.step = ArcStep::MinorRatio {
center,
major: pt - center,
};
CmdResult::NeedPoint
}
ArcStep::MinorRatio { center, major } => {
let (center, major) = (*center, *major);
let ratio = minor_ratio(center, major, pt);
self.step = ArcStep::StartAngle {
center,
major,
ratio,
};
CmdResult::NeedPoint
}
ArcStep::StartAngle {
center,
major,
ratio,
} => {
let (center, major, ratio) = (*center, *major, *ratio);
let t_start = angle_from_point(center, major, ratio, pt);
self.prev_pt = None; // reset direction tracking for end-angle step
self.step = ArcStep::EndAngle {
center,
major,
ratio,
t_start,
};
CmdResult::NeedPoint
}
ArcStep::EndAngle {
center,
major,
ratio,
t_start,
} => {
let (center, major, ratio, t_start) = (*center, *major, *ratio, *t_start);
let t_end = angle_from_point(center, major, ratio, pt);
let entity = if self.cw {
make_ellipse(center, major, ratio, t_end, t_start)
} else {
make_ellipse(center, major, ratio, t_start, t_end)
};
CmdResult::CommitAndExit(EntityType::Ellipse(entity))
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let val = parse_f32(text)?;
match &self.step {
ArcStep::MinorRatio { center, major } => {
if val > 0.0 {
let ratio = (val / major.length()).clamp(1e-6, 1.0);
let (c, m) = (*center, *major);
self.step = ArcStep::StartAngle {
center: c,
major: m,
ratio,
};
return Some(CmdResult::NeedPoint);
}
}
ArcStep::StartAngle {
center,
major,
ratio,
} => {
let t_start = val.to_radians();
let (c, m, r) = (*center, *major, *ratio);
self.prev_pt = None;
self.step = ArcStep::EndAngle {
center: c,
major: m,
ratio: r,
t_start,
};
return Some(CmdResult::NeedPoint);
}
ArcStep::EndAngle {
center,
major,
ratio,
t_start,
} => {
// Typed degrees: positive = CCW, negative = CW.
let t_end = val.to_radians();
return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse(
*center, *major, *ratio, *t_start, t_end,
))));
}
_ => {}
}
None
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
match &self.step {
ArcStep::MajorAxis { center } => Some(line_wire(*center, pt)),
ArcStep::MinorRatio { center, major } => {
let ratio = minor_ratio(*center, *major, pt).max(0.001);
Some(ellipse_wire(*center, *major, ratio, 0.0, TAU))
}
ArcStep::StartAngle {
center,
major,
ratio,
} => {
let t = angle_from_point(*center, *major, *ratio, pt);
Some(ellipse_wire(*center, *major, *ratio, 0.0, t))
}
ArcStep::EndAngle {
center,
major,
ratio,
t_start,
} => {
// Detect rotation direction via cross product.
if let Some(prev) = self.prev_pt {
let p = prev - *center;
let c = pt - *center;
let sign = p.x * c.y - p.y * c.x;
if sign.abs() > 1e-6 {
self.cw = sign < 0.0;
}
}
self.prev_pt = Some(pt);
let t_end = angle_from_point(*center, *major, *ratio, pt);
Some(if self.cw {
ellipse_wire(*center, *major, *ratio, t_end, *t_start)
} else {
ellipse_wire(*center, *major, *ratio, *t_start, t_end)
})
}
_ => None,
}
}
}
// ── Internal helpers ──────────────────────────────────────────────────────
fn minor_ratio(center: Vec3, major: Vec3, pt: Vec3) -> f32 {
let r_major = major.length();
if r_major < 1e-9 {
return 0.5;
}
let major_dir = major / r_major;
let to_pt = pt - center;
let perp = to_pt - major_dir * to_pt.dot(major_dir);
let r_minor = perp.length().max(1e-6);
(r_minor / r_major).clamp(1e-6, 1.0)
}
fn angle_from_point(center: Vec3, major: Vec3, ratio: f32, pt: Vec3) -> f32 {
let r_major = major.length();
if r_major < 1e-9 {
return 0.0;
}
let major_dir = major / r_major;
let v = major_dir.cross(Vec3::Z).normalize();
param_angle(center, major_dir, v, pt, ratio)
}
fn line_wire(from: Vec3, to: Vec3) -> WireModel {
WireModel {
name: "rubber_band".into(),
points: vec![[from.x, from.y, from.z], [to.x, to.y, to.z]],
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
}
}
+419
View File
@@ -0,0 +1,419 @@
// Hatch/Gradient/Boundary commands — H7CAD Home > Draw > Hatch dropdown.
//
// Commands:
// HATCH — ANSI31: 45° hatch lines (pick inside or type S for manual)
// GRADIENT — Linear gradient fill (pick inside or type S for manual)
// BOUNDARY — Traces the enclosing boundary as a closed LwPolyline
//
// Primary workflow (matches H7CAD):
// Click a point INSIDE a closed region → boundary auto-detected.
// Type "S" to switch to manual vertex-picking mode (HATCH/GRADIENT only).
use crate::command::{CadCommand, CmdResult};
use crate::modules::IconKind;
use crate::scene::hatch_model::{HatchModel, HatchPattern, PatFamily};
use crate::scene::wire_model::WireModel;
use glam::Vec3;
// ── Icons ──────────────────────────────────────────────────────────────────
const ICON_HATCH: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/hatch/hatch_lines.svg"
));
const ICON_GRADIENT: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/hatch/hatch_gradient.svg"
));
const ICON_BOUNDARY: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/hatch/hatch_boundary.svg"
));
// ── Dropdown metadata ──────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "HATCH";
pub const ICON: IconKind = ICON_HATCH;
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
("HATCH", "Hatch", ICON_HATCH),
("GRADIENT", "Gradient", ICON_GRADIENT),
("BOUNDARY", "Boundary", ICON_BOUNDARY),
];
// ── Shared mode ────────────────────────────────────────────────────────────
enum Mode {
/// Primary: click inside a closed shape → boundary auto-detected.
PickInside,
/// Fallback: user manually picks polygon vertices (type "S" to enter).
Manual,
}
// ── CPU point-in-polygon (ray casting) ────────────────────────────────────
fn point_in_polygon(p: [f32; 2], poly: &[[f32; 2]]) -> bool {
let n = poly.len();
if n < 3 {
return false;
}
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let vi = poly[i];
let vj = poly[j];
if (vi[1] > p[1]) != (vj[1] > p[1]) {
let x_int = (vj[0] - vi[0]) * (p[1] - vi[1]) / (vj[1] - vi[1]) + vi[0];
if p[0] < x_int {
inside = !inside;
}
}
j = i;
}
inside
}
// ── HATCH command ──────────────────────────────────────────────────────────
pub struct HatchCommand {
outlines: Vec<Vec<[f32; 2]>>,
mode: Mode,
manual_pts: Vec<Vec3>,
missed: bool,
}
impl HatchCommand {
pub fn new(outlines: Vec<Vec<[f32; 2]>>) -> Self {
Self {
outlines,
mode: Mode::PickInside,
manual_pts: vec![],
missed: false,
}
}
fn make_hatch(&self, boundary: Vec<[f32; 2]>) -> HatchModel {
// Default: ANSI31 from catalog; fallback to a single 45° family.
let pat_name = "ANSI31";
let families = crate::scene::hatch_patterns::find(pat_name)
.and_then(|e| {
if let HatchPattern::Pattern(f) = &e.gpu {
Some(f.clone())
} else {
None
}
})
.unwrap_or_else(|| {
// 45° lines, perpendicular spacing ≈ 5 world units.
let dy = 5.0_f32 / (45.0_f32.to_radians().cos());
vec![PatFamily {
angle_deg: 45.0,
x0: 0.0,
y0: 0.0,
dx: 0.0,
dy,
dashes: vec![],
}]
});
HatchModel {
boundary,
pattern: HatchPattern::Pattern(families),
name: pat_name.into(),
color: [0.75, 0.75, 0.75, 0.85],
angle_offset: 0.0,
scale: 1.0,
}
}
}
impl CadCommand for HatchCommand {
fn name(&self) -> &'static str {
"HATCH"
}
fn prompt(&self) -> String {
match &self.mode {
Mode::PickInside => {
let miss = if self.missed {
" ⚠ Kapalı sınır bulunamadı."
} else {
""
};
format!("HATCH İçini tıklayın [S=Elle çiz]:{miss}")
}
Mode::Manual => {
if self.manual_pts.is_empty() {
"HATCH Sınır noktası 1:".into()
} else {
format!(
"HATCH Nokta {} [Enter=onayla, ≥3 nokta]:",
self.manual_pts.len() + 1
)
}
}
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.mode {
Mode::PickInside => {
let xy = [pt.x, pt.y];
for outline in &self.outlines {
if point_in_polygon(xy, outline) {
self.missed = false;
return CmdResult::CommitHatch(self.make_hatch(outline.clone()));
}
}
self.missed = true;
CmdResult::NeedPoint
}
Mode::Manual => {
self.manual_pts.push(pt);
CmdResult::NeedPoint
}
}
}
fn on_enter(&mut self) -> CmdResult {
match &self.mode {
Mode::PickInside => CmdResult::Cancel,
Mode::Manual => {
if self.manual_pts.len() < 3 {
return CmdResult::Cancel;
}
let boundary = self.manual_pts.iter().map(|p| [p.x, p.y]).collect();
CmdResult::CommitHatch(self.make_hatch(boundary))
}
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn wants_text_input(&self) -> bool {
matches!(self.mode, Mode::PickInside)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if text.trim().eq_ignore_ascii_case("s") {
self.mode = Mode::Manual;
self.missed = false;
return Some(CmdResult::NeedPoint);
}
None
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
if let Mode::Manual = &self.mode {
if self.manual_pts.is_empty() {
return None;
}
let mut pts: Vec<[f32; 3]> = self.manual_pts.iter().map(|p| [p.x, p.y, p.z]).collect();
pts.push([pt.x, pt.y, pt.z]);
pts.push([
self.manual_pts[0].x,
self.manual_pts[0].y,
self.manual_pts[0].z,
]);
return Some(WireModel::solid(
"rubber_band".into(),
pts,
WireModel::CYAN,
false,
));
}
None
}
}
// ── GRADIENT command ───────────────────────────────────────────────────────
pub struct GradientCommand {
outlines: Vec<Vec<[f32; 2]>>,
mode: Mode,
manual_pts: Vec<Vec3>,
missed: bool,
}
impl GradientCommand {
pub fn new(outlines: Vec<Vec<[f32; 2]>>) -> Self {
Self {
outlines,
mode: Mode::PickInside,
manual_pts: vec![],
missed: false,
}
}
fn make_hatch(&self, boundary: Vec<[f32; 2]>) -> HatchModel {
HatchModel {
boundary,
pattern: HatchPattern::Gradient {
angle_deg: 0.0,
color2: [0.18, 0.18, 0.18, 0.0],
},
name: "LINEAR".into(),
color: [0.30, 0.60, 0.95, 0.80],
angle_offset: 0.0,
scale: 1.0,
}
}
}
impl CadCommand for GradientCommand {
fn name(&self) -> &'static str {
"GRADIENT"
}
fn prompt(&self) -> String {
match &self.mode {
Mode::PickInside => {
let miss = if self.missed {
" ⚠ Kapalı sınır bulunamadı."
} else {
""
};
format!("GRADIENT İçini tıklayın [S=Elle çiz]:{miss}")
}
Mode::Manual => {
if self.manual_pts.is_empty() {
"GRADIENT Sınır noktası 1:".into()
} else {
format!(
"GRADIENT Nokta {} [Enter=onayla, ≥3 nokta]:",
self.manual_pts.len() + 1
)
}
}
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.mode {
Mode::PickInside => {
let xy = [pt.x, pt.y];
for outline in &self.outlines {
if point_in_polygon(xy, outline) {
self.missed = false;
return CmdResult::CommitHatch(self.make_hatch(outline.clone()));
}
}
self.missed = true;
CmdResult::NeedPoint
}
Mode::Manual => {
self.manual_pts.push(pt);
CmdResult::NeedPoint
}
}
}
fn on_enter(&mut self) -> CmdResult {
match &self.mode {
Mode::PickInside => CmdResult::Cancel,
Mode::Manual => {
if self.manual_pts.len() < 3 {
return CmdResult::Cancel;
}
let boundary = self.manual_pts.iter().map(|p| [p.x, p.y]).collect();
CmdResult::CommitHatch(self.make_hatch(boundary))
}
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn wants_text_input(&self) -> bool {
matches!(self.mode, Mode::PickInside)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if text.trim().eq_ignore_ascii_case("s") {
self.mode = Mode::Manual;
self.missed = false;
return Some(CmdResult::NeedPoint);
}
None
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
if let Mode::Manual = &self.mode {
if self.manual_pts.is_empty() {
return None;
}
let mut pts: Vec<[f32; 3]> = self.manual_pts.iter().map(|p| [p.x, p.y, p.z]).collect();
pts.push([pt.x, pt.y, pt.z]);
pts.push([
self.manual_pts[0].x,
self.manual_pts[0].y,
self.manual_pts[0].z,
]);
return Some(WireModel::solid(
"rubber_band".into(),
pts,
WireModel::CYAN,
false,
));
}
None
}
}
// ── BOUNDARY command ───────────────────────────────────────────────────────
pub struct BoundaryCommand {
outlines: Vec<Vec<[f32; 2]>>,
missed: bool,
}
impl BoundaryCommand {
pub fn new(outlines: Vec<Vec<[f32; 2]>>) -> Self {
Self {
outlines,
missed: false,
}
}
}
impl CadCommand for BoundaryCommand {
fn name(&self) -> &'static str {
"BOUNDARY"
}
fn prompt(&self) -> String {
let miss = if self.missed {
" ⚠ Kapalı sınır bulunamadı."
} else {
""
};
format!("BOUNDARY İçini tıklayın:{miss}")
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
let xy = [pt.x, pt.y];
for outline in &self.outlines {
if point_in_polygon(xy, outline) {
self.missed = false;
// Store as a Hatch entity (solid fill) so it is selectable.
let model = HatchModel {
boundary: outline.clone(),
pattern: HatchPattern::Solid,
name: "SOLID".into(),
color: [0.45, 0.45, 0.45, 0.60],
angle_offset: 0.0,
scale: 1.0,
};
return CmdResult::CommitHatch(model);
}
}
self.missed = true;
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
+91
View File
@@ -0,0 +1,91 @@
// Line tool — ribbon definition + interactive command.
//
// Command: LINE — H7CAD behaviour:
// 1. First click → stores start point, prompts for next point
// 2. Each further click → immediately commits an acadrust::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 crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::wire_model::WireModel;
use glam::Vec3;
// ── Ribbon definition ─────────────────────────────────────────────────────
pub fn tool() -> ToolDef {
ToolDef {
id: "LINE",
label: "Line",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/line.svg")),
event: ModuleEvent::Command("LINE".to_string()),
}
}
// ── Command implementation ────────────────────────────────────────────────
pub struct LineCommand {
/// The last committed point (start of the next segment).
last: Option<Vec3>,
}
impl LineCommand {
pub fn new() -> Self {
Self { last: None }
}
}
impl CadCommand for LineCommand {
fn name(&self) -> &'static str {
"LINE"
}
fn prompt(&self) -> String {
if self.last.is_none() {
"LINE Specify first point:".to_string()
} else {
"LINE Specify next point [Enter/Esc = done]:".to_string()
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
if let Some(last) = self.last {
let line = Line::from_points(
Vector3::new(last.x as f64, last.y as f64, last.z as f64),
Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64),
);
self.last = Some(pt);
CmdResult::CommitEntity(EntityType::Line(line))
} else {
self.last = Some(pt);
CmdResult::NeedPoint
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
let last = self.last?;
Some(WireModel {
name: "rubber_band".to_string(),
points: vec![[last.x, last.y, last.z], [pt.x, pt.y, pt.z]],
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
key_vertices: vec![],
})
}
}
+9
View File
@@ -0,0 +1,9 @@
pub mod arc;
pub mod circle;
pub mod ellipse;
pub mod hatch;
pub mod line;
pub mod point;
pub mod polyline;
pub mod shapes;
pub mod spline;
+57
View File
@@ -0,0 +1,57 @@
// Point tool — ribbon definition + interactive command.
//
// Command: POINT (PO)
// Single click → commits EntityType::Point. Stays active for more points.
use acadrust::types::Vector3;
use acadrust::{EntityType, Point as CadPoint};
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::wire_model::WireModel;
use glam::Vec3;
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef {
id: "POINT",
label: "Point",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/point.svg")),
event: ModuleEvent::Command("POINT".to_string()),
}
}
pub struct PointCommand;
impl PointCommand {
pub fn new() -> Self {
Self
}
}
impl CadCommand for PointCommand {
fn name(&self) -> &'static str {
"POINT"
}
fn prompt(&self) -> String {
"POINT Specify point [Enter=done]:".into()
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
let p = CadPoint {
location: Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64),
..Default::default()
};
CmdResult::CommitEntity(EntityType::Point(p))
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> {
None
}
}
+106
View File
@@ -0,0 +1,106 @@
// Polyline tool — ribbon definition + interactive command.
//
// Command: PLINE (PL)
// Each click adds a vertex. The rubber-band shows the segment being drawn.
// Enter / C = close and commit. Escape = commit as-is (if ≥2 vertices).
use acadrust::entities::LwVertex;
use acadrust::{EntityType, LwPolyline};
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::wire_model::WireModel;
use glam::Vec3;
// ── Ribbon definition ──────────────────────────────────────────────────────
pub fn tool() -> ToolDef {
ToolDef {
id: "PLINE",
label: "Polyline",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/polyline.svg")),
event: ModuleEvent::Command("PLINE".to_string()),
}
}
// ── Command implementation ─────────────────────────────────────────────────
pub struct PlineCommand {
vertices: Vec<Vec3>,
}
impl PlineCommand {
pub fn new() -> Self {
Self {
vertices: Vec::new(),
}
}
fn build_entity(&self, closed: bool) -> Option<EntityType> {
if self.vertices.len() < 2 {
return None;
}
let lw_verts: Vec<LwVertex> = self
.vertices
.iter()
.map(|v| LwVertex::new(acadrust::types::Vector2::new(v.x as f64, v.y as f64)))
.collect();
let pline = LwPolyline {
vertices: lw_verts,
is_closed: closed,
..Default::default()
};
Some(EntityType::LwPolyline(pline))
}
}
impl CadCommand for PlineCommand {
fn name(&self) -> &'static str {
"PLINE"
}
fn prompt(&self) -> String {
if self.vertices.is_empty() {
"PLINE Specify start point:".into()
} else {
format!(
"PLINE Specify next point [{}pts | Enter=done C=close Esc=cancel]:",
self.vertices.len()
)
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
self.vertices.push(pt);
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
match self.build_entity(false) {
Some(e) => CmdResult::CommitAndExit(e),
None => CmdResult::Cancel,
}
}
fn on_escape(&mut self) -> CmdResult {
match self.build_entity(false) {
Some(e) => CmdResult::CommitAndExit(e),
None => CmdResult::Cancel,
}
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
if self.vertices.is_empty() {
return None;
}
// Show all committed vertices + cursor as a continuous preview.
let mut pts: Vec<[f32; 3]> = self.vertices.iter().map(|v| [v.x, v.y, v.z]).collect();
pts.push([pt.x, pt.y, pt.z]);
Some(WireModel::solid(
"rubber_band".into(),
pts,
WireModel::CYAN,
false,
))
}
}
+635
View File
@@ -0,0 +1,635 @@
// Shapes dropdown — Rectangle and Polygon creation methods.
//
// Rectangle:
// RECT — Two Corners (axis-aligned)
// RECT_ROT — Rotated (corner + adjacent corner + height)
// RECT_CEN — Center Point + corner
//
// Polygon (regular N-gon, sides typed in command line):
// POLY — Inscribed in circle (vertices ON the circle)
// 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 crate::command::{CadCommand, CmdResult};
use crate::modules::IconKind;
use crate::scene::wire_model::WireModel;
use glam::Vec3;
const TAU: f32 = std::f32::consts::TAU;
const PI: f32 = std::f32::consts::PI;
// ── Icons ──────────────────────────────────────────────────────────────────
const ICON_RECT: IconKind =
IconKind::Svg(include_bytes!("../../../../assets/icons/shapes/rect.svg"));
const ICON_RECT_ROT: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/shapes/rect_rot.svg"
));
const ICON_RECT_CEN: IconKind = IconKind::Svg(include_bytes!(
"../../../../assets/icons/shapes/rect_cen.svg"
));
const ICON_POLY_I: IconKind =
IconKind::Svg(include_bytes!("../../../../assets/icons/shapes/poly_i.svg"));
const ICON_POLY_C: IconKind =
IconKind::Svg(include_bytes!("../../../../assets/icons/shapes/poly_c.svg"));
const ICON_POLY_E: IconKind =
IconKind::Svg(include_bytes!("../../../../assets/icons/shapes/poly_e.svg"));
// ── Dropdown metadata ──────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "SHAPES";
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
("RECT", "Rectangle - Two Corners", ICON_RECT),
("RECT_ROT", "Rectangle - Rotated", ICON_RECT_ROT),
("RECT_CEN", "Rectangle - Center", ICON_RECT_CEN),
("POLY", "Polygon - Inscribed", ICON_POLY_I),
("POLY_C", "Polygon - Circumscribed", ICON_POLY_C),
("POLY_E", "Polygon - Edge", ICON_POLY_E),
];
pub const ICON: IconKind = ICON_RECT;
// ── Shared geometry helpers ────────────────────────────────────────────────
fn make_pline(xy_pairs: &[[f64; 2]]) -> EntityType {
EntityType::LwPolyline(LwPolyline {
vertices: xy_pairs
.iter()
.map(|&[x, y]| LwVertex::new(Vector2::new(x, y)))
.collect(),
is_closed: true,
..Default::default()
})
}
fn wire_loop(pts: Vec<[f32; 3]>) -> WireModel {
let mut p = pts;
if let Some(&first) = p.first() {
p.push(first);
}
WireModel::solid("rubber_band".into(), p, WireModel::CYAN, false)
}
fn wire_seg(a: Vec3, b: Vec3) -> WireModel {
WireModel::solid(
"rubber_band".into(),
vec![[a.x, a.y, a.z], [b.x, b.y, b.z]],
WireModel::CYAN,
false,
)
}
// ── Polygon geometry ───────────────────────────────────────────────────────
fn poly_verts_xy(center: Vec3, vertex_r: f32, sides: u32, start_angle: f32) -> Vec<[f64; 2]> {
(0..sides)
.map(|i| {
let a = start_angle + (i as f32) * TAU / sides as f32;
[
(center.x + vertex_r * a.cos()) as f64,
(center.y + vertex_r * a.sin()) as f64,
]
})
.collect()
}
fn poly_wire(center: Vec3, vertex_r: f32, sides: u32, start_angle: f32) -> WireModel {
let pts: Vec<[f32; 3]> = (0..sides)
.map(|i| {
let a = start_angle + (i as f32) * TAU / sides as f32;
[
center.x + vertex_r * a.cos(),
center.y + vertex_r * a.sin(),
center.z,
]
})
.collect();
wire_loop(pts)
}
fn angle_xy(from: Vec3, to: Vec3) -> f32 {
(to.y - from.y).atan2(to.x - from.x)
}
// ── Command: Rectangle — Two Corners (RECT) ──────────────────────────────
pub struct RectCommand {
a: Option<Vec3>,
}
impl RectCommand {
pub fn new() -> Self {
Self { a: None }
}
}
impl CadCommand for RectCommand {
fn name(&self) -> &'static str {
"RECT"
}
fn prompt(&self) -> String {
if self.a.is_none() {
"RECT Specify first corner:".into()
} else {
"RECT Specify opposite corner:".into()
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.a {
None => {
self.a = Some(pt);
CmdResult::NeedPoint
}
Some(a) => CmdResult::CommitAndExit(make_pline(&[
[a.x as f64, a.y as f64],
[pt.x as f64, a.y as f64],
[pt.x as f64, pt.y as f64],
[a.x as f64, pt.y as f64],
])),
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
let a = self.a?;
Some(wire_loop(vec![
[a.x, a.y, a.z],
[pt.x, a.y, a.z],
[pt.x, pt.y, a.z],
[a.x, pt.y, a.z],
]))
}
}
// ── Command: Rectangle — Rotated (RECT_ROT) ──────────────────────────────
// Step 0: pick corner A
// Step 1: pick adjacent corner B (defines one edge direction + length)
// Step 2: pick height point (perpendicular offset from the A-B edge)
pub struct RectRotCommand {
step: u8,
a: Vec3,
b: Vec3,
}
impl RectRotCommand {
pub fn new() -> Self {
Self {
step: 0,
a: Vec3::ZERO,
b: Vec3::ZERO,
}
}
}
impl CadCommand for RectRotCommand {
fn name(&self) -> &'static str {
"RECT_ROT"
}
fn prompt(&self) -> String {
match self.step {
0 => "RECT ROT Specify first corner:".into(),
1 => "RECT ROT Specify adjacent corner (defines edge direction):".into(),
_ => "RECT ROT Specify height (perpendicular pick):".into(),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.step {
0 => {
self.a = pt;
self.step = 1;
CmdResult::NeedPoint
}
1 => {
self.b = pt;
self.step = 2;
CmdResult::NeedPoint
}
_ => {
let (a, b) = (self.a, self.b);
let dir = (b - a).normalize_or_zero();
let perp = Vec3::new(-dir.y, dir.x, 0.0);
let h = (pt - b).dot(perp); // signed height
let c = b + perp * h;
let d = a + perp * h;
CmdResult::CommitAndExit(make_pline(&[
[a.x as f64, a.y as f64],
[b.x as f64, b.y as f64],
[c.x as f64, c.y as f64],
[d.x as f64, d.y as f64],
]))
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
match self.step {
1 => Some(wire_seg(self.a, pt)),
2 => {
let (a, b) = (self.a, self.b);
let dir = (b - a).normalize_or_zero();
let perp = Vec3::new(-dir.y, dir.x, 0.0);
let h = (pt - b).dot(perp);
let c = b + perp * h;
let d = a + perp * h;
Some(wire_loop(vec![
[a.x, a.y, a.z],
[b.x, b.y, b.z],
[c.x, c.y, c.z],
[d.x, d.y, d.z],
]))
}
_ => None,
}
}
}
// ── Command: Rectangle — Center (RECT_CEN) ───────────────────────────────
// Step 0: pick center
// Step 1: pick any corner (half-width = |cx|, half-height = |cy|)
pub struct RectCenCommand {
center: Option<Vec3>,
}
impl RectCenCommand {
pub fn new() -> Self {
Self { center: None }
}
}
impl CadCommand for RectCenCommand {
fn name(&self) -> &'static str {
"RECT_CEN"
}
fn prompt(&self) -> String {
if self.center.is_none() {
"RECT CEN Specify center point:".into()
} else {
"RECT CEN Specify corner point:".into()
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.center {
None => {
self.center = Some(pt);
CmdResult::NeedPoint
}
Some(c) => {
let hw = (pt.x - c.x).abs() as f64;
let hh = (pt.y - c.y).abs() as f64;
let cx = c.x as f64;
let cy = c.y as f64;
CmdResult::CommitAndExit(make_pline(&[
[cx - hw, cy - hh],
[cx + hw, cy - hh],
[cx + hw, cy + hh],
[cx - hw, cy + hh],
]))
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
let c = self.center?;
let hw = (pt.x - c.x).abs();
let hh = (pt.y - c.y).abs();
Some(wire_loop(vec![
[c.x - hw, c.y - hh, c.z],
[c.x + hw, c.y - hh, c.z],
[c.x + hw, c.y + hh, c.z],
[c.x - hw, c.y + hh, c.z],
]))
}
}
// ── Command: Polygon — Inscribed (POLY) ──────────────────────────────────
// Type number of sides (default 6) → pick center → pick vertex
// Vertices lie ON the circle of the picked radius.
pub struct PolyCommand {
sides: u32,
step: u8,
center: Vec3,
}
impl PolyCommand {
pub fn new() -> Self {
Self {
sides: 6,
step: 0,
center: Vec3::ZERO,
}
}
}
impl CadCommand for PolyCommand {
fn name(&self) -> &'static str {
"POLY"
}
fn wants_text_input(&self) -> bool {
self.step == 0
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let Ok(n) = text.trim().parse::<u32>() {
if (3..=1024).contains(&n) {
self.sides = n;
}
}
self.step = 1;
Some(CmdResult::NeedPoint)
}
fn prompt(&self) -> String {
match self.step {
0 => format!("POLYGON Enter number of sides <{}>:", self.sides),
1 => format!("POLYGON Specify center [{} sides]:", self.sides),
_ => format!(
"POLYGON Specify vertex on circle [{} sides inscribed]:",
self.sides
),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.step {
0 => {
// User clicked without typing sides: use default, treat click as center.
self.center = pt;
self.step = 2;
CmdResult::NeedPoint
}
1 => {
self.center = pt;
self.step = 2;
CmdResult::NeedPoint
}
_ => {
let r = self.center.distance(pt);
let sa = angle_xy(self.center, pt);
CmdResult::CommitAndExit(make_pline(&poly_verts_xy(self.center, r, self.sides, sa)))
}
}
}
fn on_enter(&mut self) -> CmdResult {
if self.step == 0 {
self.step = 1;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
if self.step < 2 {
return None;
}
let r = self.center.distance(pt);
let sa = angle_xy(self.center, pt);
Some(poly_wire(self.center, r, self.sides, sa))
}
}
// ── Command: Polygon — Circumscribed (POLY_C) ────────────────────────────
// Type sides → pick center → pick edge-midpoint (on the inscribed circle).
// vertex_radius = inradius / cos(π/N).
pub struct PolyCCommand {
sides: u32,
step: u8,
center: Vec3,
}
impl PolyCCommand {
pub fn new() -> Self {
Self {
sides: 6,
step: 0,
center: Vec3::ZERO,
}
}
}
impl CadCommand for PolyCCommand {
fn name(&self) -> &'static str {
"POLY_C"
}
fn wants_text_input(&self) -> bool {
self.step == 0
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let Ok(n) = text.trim().parse::<u32>() {
if (3..=1024).contains(&n) {
self.sides = n;
}
}
self.step = 1;
Some(CmdResult::NeedPoint)
}
fn prompt(&self) -> String {
match self.step {
0 => format!("POLYGON C Enter number of sides <{}>:", self.sides),
1 => format!("POLYGON C Specify center [{} sides]:", self.sides),
_ => format!(
"POLYGON C Specify edge-midpoint radius [{} sides circumscribed]:",
self.sides
),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.step {
0 => {
self.center = pt;
self.step = 2;
CmdResult::NeedPoint
}
1 => {
self.center = pt;
self.step = 2;
CmdResult::NeedPoint
}
_ => {
let inradius = self.center.distance(pt);
let vr = inradius / (PI / self.sides as f32).cos();
// The picked pt is at the midpoint of an edge; the vertex is
// offset by half a sector (π/N) from that direction.
let edge_angle = angle_xy(self.center, pt);
let sa = edge_angle + PI / self.sides as f32;
CmdResult::CommitAndExit(make_pline(&poly_verts_xy(
self.center,
vr,
self.sides,
sa,
)))
}
}
}
fn on_enter(&mut self) -> CmdResult {
if self.step == 0 {
self.step = 1;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
if self.step < 2 {
return None;
}
let inradius = self.center.distance(pt);
let vr = inradius / (PI / self.sides as f32).cos();
let sa = angle_xy(self.center, pt) + PI / self.sides as f32;
Some(poly_wire(self.center, vr, self.sides, sa))
}
}
// ── Command: Polygon — Edge (POLY_E) ─────────────────────────────────────
// Type sides → pick edge start A → pick edge end B.
// Center is computed from the edge and the polygon geometry.
pub struct PolyECommand {
sides: u32,
step: u8,
a: Vec3,
}
impl PolyECommand {
pub fn new() -> Self {
Self {
sides: 6,
step: 0,
a: Vec3::ZERO,
}
}
}
impl CadCommand for PolyECommand {
fn name(&self) -> &'static str {
"POLY_E"
}
fn wants_text_input(&self) -> bool {
self.step == 0
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let Ok(n) = text.trim().parse::<u32>() {
if (3..=1024).contains(&n) {
self.sides = n;
}
}
self.step = 1;
Some(CmdResult::NeedPoint)
}
fn prompt(&self) -> String {
match self.step {
0 => format!("POLYGON E Enter number of sides <{}>:", self.sides),
1 => format!(
"POLYGON E Specify first endpoint of edge [{} sides]:",
self.sides
),
_ => format!(
"POLYGON E Specify second endpoint of edge [{} sides]:",
self.sides
),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.step {
0 => {
self.a = pt;
self.step = 2;
CmdResult::NeedPoint
}
1 => {
self.a = pt;
self.step = 2;
CmdResult::NeedPoint
}
_ => {
if let Some((center, vr, sa)) = edge_poly_params(self.a, pt, self.sides) {
CmdResult::CommitAndExit(make_pline(&poly_verts_xy(center, vr, self.sides, sa)))
} else {
CmdResult::Cancel
}
}
}
}
fn on_enter(&mut self) -> CmdResult {
if self.step == 0 {
self.step = 1;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
if self.step < 2 {
return None;
}
if let Some((center, vr, sa)) = edge_poly_params(self.a, pt, self.sides) {
Some(poly_wire(center, vr, self.sides, sa))
} else {
Some(wire_seg(self.a, pt))
}
}
}
/// Compute polygon center, vertex-radius and start-angle from two edge endpoints.
/// The polygon is placed on the left side of A→B (CCW convention).
fn edge_poly_params(a: Vec3, b: Vec3, sides: u32) -> Option<(Vec3, f32, f32)> {
let edge_len = a.distance(b);
if edge_len < 1e-6 {
return None;
}
// vertex_radius = edge_len / (2 * sin(π/N))
let vr = edge_len / (2.0 * (PI / sides as f32).sin());
// inradius = vr * cos(π/N) = edge_len / (2 * tan(π/N))
let inradius = vr * (PI / sides as f32).cos();
// Center: on the left perpendicular bisector of A→B
let dir = (b - a) / edge_len;
let perp = Vec3::new(-dir.y, dir.x, 0.0); // CCW left
let mid = (a + b) * 0.5;
let center = mid + perp * inradius;
// First vertex = A
let sa = angle_xy(center, a);
Some((center, vr, sa))
}
+120
View File
@@ -0,0 +1,120 @@
// Spline tool — ribbon definition + interactive command.
//
// Command: SPLINE (SPL)
// Click to add control points. Enter (≥2 pts) → commits EntityType::Spline.
use acadrust::types::Vector3;
use acadrust::{EntityType, Spline};
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::wire_model::WireModel;
use glam::Vec3;
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef {
id: "SPLINE",
label: "Spline",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/spline.svg")),
event: ModuleEvent::Command("SPLINE".to_string()),
}
}
pub struct SplineCommand {
pts: Vec<Vec3>,
}
impl SplineCommand {
pub fn new() -> Self {
Self { pts: Vec::new() }
}
fn build(&self) -> Option<EntityType> {
if self.pts.len() < 2 {
return None;
}
let control_points = self
.pts
.iter()
.map(|p| Vector3::new(p.x as f64, p.y as f64, p.z as f64))
.collect();
let n = self.pts.len();
// Uniform open knot vector for degree-3 clamped B-spline
let degree = 3_i32.min((n - 1) as i32);
let knots = uniform_knots(n, degree as usize);
let spline = Spline {
degree,
control_points,
knots,
..Default::default()
};
Some(EntityType::Spline(spline))
}
}
fn uniform_knots(n: usize, d: usize) -> Vec<f64> {
let m = n + d + 1;
(0..m)
.map(|i| {
if i <= d {
0.0
} else if i >= m - d - 1 {
1.0
} else {
(i - d) as f64 / (n - d) as f64
}
})
.collect()
}
impl CadCommand for SplineCommand {
fn name(&self) -> &'static str {
"SPLINE"
}
fn prompt(&self) -> String {
if self.pts.is_empty() {
"SPLINE Specify first control point:".into()
} else {
format!(
"SPLINE Specify next point [{} pts | Enter=done]:",
self.pts.len()
)
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
self.pts.push(pt);
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
match self.build() {
Some(e) => CmdResult::CommitEntity(e),
None => CmdResult::Cancel,
}
}
fn on_escape(&mut self) -> CmdResult {
match self.build() {
Some(e) => CmdResult::CommitEntity(e),
None => CmdResult::Cancel,
}
}
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
if self.pts.is_empty() {
return None;
}
// Show all committed points + cursor as a connected polyline.
let mut pts: Vec<[f32; 3]> = self.pts.iter().map(|p| [p.x, p.y, p.z]).collect();
pts.push([pt.x, pt.y, pt.z]);
Some(WireModel::solid(
"rubber_band".into(),
pts,
WireModel::CYAN,
false,
))
}
}