forked from 1b-agent/opencadstudio-upstream
first commit
This commit is contained in:
1 parent
d52c6440ff
commit
d2fdac7f1e
274 files changed
+89330
No files matched your search
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "COPYCLIP",
|
||||
label: "Copy",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/copy_clip.svg")),
|
||||
event: ModuleEvent::Command("COPYCLIP".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "CUTCLIP",
|
||||
label: "Cut",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/cut.svg")),
|
||||
event: ModuleEvent::Command("CUTCLIP".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod copy_clip;
|
||||
pub mod cut;
|
||||
pub mod paste;
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "PASTECLIP",
|
||||
label: "Paste",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/paste.svg")),
|
||||
event: ModuleEvent::Command("PASTECLIP".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Persistent command defaults — last-used values survive across command runs
|
||||
// within the same session. Uses thread_local! so no synchronisation needed.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
static CIRCLE_RADIUS: Cell<f32> = Cell::new(1.0);
|
||||
static CIRCLE_DIAM: Cell<f32> = Cell::new(2.0);
|
||||
static ROTATE_ANGLE: Cell<f32> = Cell::new(0.0); // degrees
|
||||
static SCALE_FACTOR: Cell<f32> = Cell::new(1.0);
|
||||
static OFFSET_DIST: Cell<f32> = Cell::new(1.0);
|
||||
static FILLET_RADIUS: Cell<f32> = Cell::new(1.0);
|
||||
static CHAMFER_DIST1: Cell<f32> = Cell::new(10.0);
|
||||
static CHAMFER_DIST2: Cell<f32> = Cell::new(10.0);
|
||||
static ARRAY_ROWS: Cell<f32> = Cell::new(2.0);
|
||||
static ARRAY_COLS: Cell<f32> = Cell::new(2.0);
|
||||
static ARRAY_ROW_SP: Cell<f32> = Cell::new(100.0);
|
||||
static ARRAY_COL_SP: Cell<f32> = Cell::new(100.0);
|
||||
static ARRAY_P_COUNT: Cell<f32> = Cell::new(6.0);
|
||||
static ARRAY_P_ANGLE: Cell<f32> = Cell::new(360.0); // degrees
|
||||
static ARRAY_PATH_COUNT: Cell<f32> = Cell::new(6.0);
|
||||
}
|
||||
|
||||
macro_rules! accessors {
|
||||
($get:ident, $set:ident, $var:ident) => {
|
||||
pub fn $get() -> f32 {
|
||||
$var.with(|c| c.get())
|
||||
}
|
||||
pub fn $set(v: f32) {
|
||||
$var.with(|c| c.set(v));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
accessors!(get_circle_radius, set_circle_radius, CIRCLE_RADIUS);
|
||||
accessors!(get_circle_diam, set_circle_diam, CIRCLE_DIAM);
|
||||
accessors!(get_rotate_angle, set_rotate_angle, ROTATE_ANGLE);
|
||||
accessors!(get_scale_factor, set_scale_factor, SCALE_FACTOR);
|
||||
accessors!(get_offset_dist, set_offset_dist, OFFSET_DIST);
|
||||
accessors!(get_fillet_radius, set_fillet_radius, FILLET_RADIUS);
|
||||
accessors!(get_chamfer_dist1, set_chamfer_dist1, CHAMFER_DIST1);
|
||||
accessors!(get_chamfer_dist2, set_chamfer_dist2, CHAMFER_DIST2);
|
||||
accessors!(get_array_rows, set_array_rows, ARRAY_ROWS);
|
||||
accessors!(get_array_cols, set_array_cols, ARRAY_COLS);
|
||||
accessors!(get_array_row_sp, set_array_row_sp, ARRAY_ROW_SP);
|
||||
accessors!(get_array_col_sp, set_array_col_sp, ARRAY_COL_SP);
|
||||
accessors!(get_array_p_count, set_array_p_count, ARRAY_P_COUNT);
|
||||
accessors!(get_array_p_angle, set_array_p_angle, ARRAY_P_ANGLE);
|
||||
accessors!(get_array_path_count, set_array_path_count, ARRAY_PATH_COUNT);
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "DONATE",
|
||||
label: "Donate",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/donate.svg")),
|
||||
event: ModuleEvent::Command("DONATE".to_string()),
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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![],
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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![],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Color tool — ribbon definition.
|
||||
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "COLOR",
|
||||
label: "Color",
|
||||
icon: IconKind::Glyph("🎨"),
|
||||
event: ModuleEvent::Command("COLOR".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYFRZ",
|
||||
label: "Layer Freeze",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/layfrz.svg")),
|
||||
event: ModuleEvent::Command("LAYFRZ".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYLCK",
|
||||
label: "Layer Lock",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/laylck.svg")),
|
||||
event: ModuleEvent::Command("LAYLCK".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYOFF",
|
||||
label: "Layer Off",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/layoff.svg")),
|
||||
event: ModuleEvent::Command("LAYOFF".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYON",
|
||||
label: "Turn All Layers On",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/layon.svg")),
|
||||
event: ModuleEvent::Command("LAYON".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYTHW",
|
||||
label: "Thaw All Layers",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/laythw.svg")),
|
||||
event: ModuleEvent::Command("LAYTHW".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYULK",
|
||||
label: "Layer Unlock",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/layulk.svg")),
|
||||
event: ModuleEvent::Command("LAYULK".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYMCUR",
|
||||
label: "Make Current",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/laymcur.svg")),
|
||||
event: ModuleEvent::Command("LAYMCUR".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYMATCH",
|
||||
label: "Match Layer",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/laymatch.svg")),
|
||||
event: ModuleEvent::Command("LAYMATCH".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pub(super) mod color;
|
||||
pub(super) mod layfrz;
|
||||
pub(super) mod laylck;
|
||||
pub(super) mod layoff;
|
||||
pub(super) mod layon;
|
||||
pub(super) mod laythw;
|
||||
pub(super) mod layulk;
|
||||
pub(super) mod make_current;
|
||||
pub(super) mod match_layer;
|
||||
pub(super) mod panel;
|
||||
@@ -0,0 +1,12 @@
|
||||
// Layers panel toggle — ribbon definition.
|
||||
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LAYERS",
|
||||
label: "Layers",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/panel.svg")),
|
||||
event: ModuleEvent::ToggleLayers,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Home module — Draw, Modify, Annotation and Layer tools.
|
||||
|
||||
mod clipboard;
|
||||
pub mod defaults;
|
||||
mod donate;
|
||||
pub mod draw;
|
||||
mod layers;
|
||||
pub mod modify;
|
||||
mod properties;
|
||||
|
||||
use crate::modules::{CadModule, RibbonGroup, RibbonItem};
|
||||
|
||||
pub struct HomeModule;
|
||||
|
||||
impl CadModule for HomeModule {
|
||||
fn id(&self) -> &'static str {
|
||||
"home"
|
||||
}
|
||||
fn title(&self) -> &'static str {
|
||||
"Home"
|
||||
}
|
||||
|
||||
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||
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 clipboard::{copy_clip, cut, paste};
|
||||
use donate;
|
||||
use draw::{arc, circle, ellipse, hatch, line, polyline, shapes};
|
||||
use layers::{layfrz, laylck, layoff, layon, laythw, layulk, make_current, match_layer, panel};
|
||||
use modify::{
|
||||
array, copy, delete, explode, fillet, group, mirror, offset, rotate, scale, stretch,
|
||||
translate, trim, ungroup,
|
||||
};
|
||||
use properties::match_prop;
|
||||
|
||||
vec![
|
||||
RibbonGroup {
|
||||
title: "Draw",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(line::tool()),
|
||||
RibbonItem::LargeTool(polyline::tool()),
|
||||
RibbonItem::LargeDropdown {
|
||||
id: circle::DROPDOWN_ID,
|
||||
label: "Circle",
|
||||
icon: circle::ICON,
|
||||
items: circle::DROPDOWN_ITEMS.to_vec(),
|
||||
default: "CIRCLE",
|
||||
},
|
||||
RibbonItem::LargeDropdown {
|
||||
id: arc::DROPDOWN_ID,
|
||||
label: "Arc",
|
||||
icon: arc::ICON,
|
||||
items: arc::DROPDOWN_ITEMS.to_vec(),
|
||||
default: "ARC",
|
||||
},
|
||||
RibbonItem::Dropdown {
|
||||
id: shapes::DROPDOWN_ID,
|
||||
icon: shapes::ICON,
|
||||
items: shapes::DROPDOWN_ITEMS.to_vec(),
|
||||
default: "RECT",
|
||||
},
|
||||
RibbonItem::Dropdown {
|
||||
id: ellipse::DROPDOWN_ID,
|
||||
icon: ellipse::ICON,
|
||||
items: ellipse::DROPDOWN_ITEMS.to_vec(),
|
||||
default: "ELLIPSE",
|
||||
},
|
||||
RibbonItem::Dropdown {
|
||||
id: hatch::DROPDOWN_ID,
|
||||
icon: hatch::ICON,
|
||||
items: hatch::DROPDOWN_ITEMS.to_vec(),
|
||||
default: "HATCH",
|
||||
},
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Modify",
|
||||
tools: vec![
|
||||
translate::tool().into(),
|
||||
copy::tool().into(),
|
||||
stretch::tool().into(),
|
||||
rotate::tool().into(),
|
||||
mirror::tool().into(),
|
||||
scale::tool().into(),
|
||||
RibbonItem::Dropdown {
|
||||
id: trim::DROPDOWN_ID,
|
||||
icon: trim::ICON,
|
||||
items: trim::DROPDOWN_ITEMS.to_vec(),
|
||||
default: "TRIM",
|
||||
},
|
||||
RibbonItem::Dropdown {
|
||||
id: fillet::DROPDOWN_ID,
|
||||
icon: fillet::ICON,
|
||||
items: fillet::DROPDOWN_ITEMS.to_vec(),
|
||||
default: "FILLET",
|
||||
},
|
||||
RibbonItem::Dropdown {
|
||||
id: array::DROPDOWN_ID,
|
||||
icon: array::ICON,
|
||||
items: array::DROPDOWN_ITEMS.to_vec(),
|
||||
default: "ARRAYRECT",
|
||||
},
|
||||
delete::tool().into(),
|
||||
explode::tool().into(),
|
||||
offset::tool().into(),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Annotation",
|
||||
tools: vec![
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "ANNOTATION_TEXT",
|
||||
label: "Text",
|
||||
icon: text::ICON,
|
||||
items: vec![
|
||||
(text::tool().id, text::tool().label, text::tool().icon),
|
||||
(mtext::tool().id, mtext::tool().label, mtext::tool().icon),
|
||||
],
|
||||
default: "TEXT",
|
||||
},
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "ANNOTATION_DIMENSIONS",
|
||||
label: "Dimensions",
|
||||
icon: linear_dim::ICON,
|
||||
items: vec![
|
||||
(linear_dim::tool().id, linear_dim::tool().label, linear_dim::tool().icon),
|
||||
(radius_dim::tool().id, radius_dim::tool().label, radius_dim::tool().icon),
|
||||
(angular_dim::tool().id, angular_dim::tool().label, angular_dim::tool().icon),
|
||||
],
|
||||
default: "DIMLINEAR",
|
||||
},
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "ANNOTATION_LEADER",
|
||||
label: "Leader",
|
||||
icon: leader_cmd::ICON,
|
||||
items: vec![
|
||||
(mleader_cmd::tool().id, mleader_cmd::tool().label, mleader_cmd::tool().icon),
|
||||
(leader_cmd::tool().id, leader_cmd::tool().label, leader_cmd::tool().icon),
|
||||
],
|
||||
default: "MLEADER",
|
||||
},
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Layers",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(panel::tool()),
|
||||
RibbonItem::LayerComboGroup {
|
||||
row2: vec![layoff::tool(), layfrz::tool(), laylck::tool(), make_current::tool()],
|
||||
row3: vec![layon::tool(), laythw::tool(), layulk::tool(), match_layer::tool()],
|
||||
},
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Block",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(create_block::tool()),
|
||||
RibbonItem::LargeTool(insert_block::tool()),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Properties",
|
||||
tools: vec![
|
||||
RibbonItem::PropertiesGroup { match_prop: match_prop::tool() },
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Groups",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(group::tool()),
|
||||
RibbonItem::LargeTool(ungroup::tool()),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Clipboard",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(paste::tool()),
|
||||
copy_clip::tool().into(),
|
||||
cut::tool().into(),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Support",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(donate::tool()),
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
// Array dropdown — ribbon definition + interactive commands.
|
||||
//
|
||||
// ARRAYRECT (AR):
|
||||
// Rectangular array: row/column counts and spacing collected via text input.
|
||||
// 1. Row count → 2. Column count → 3. Row spacing → 4. Column spacing
|
||||
// → Returns BatchCopy with a grid of Translate transforms.
|
||||
//
|
||||
// ARRAYPATH:
|
||||
// Path array: copies placed at equal intervals along a curve/line.
|
||||
// (pending geometry engine support — stub)
|
||||
//
|
||||
// ARRAYPOLAR:
|
||||
// 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 glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::modules::home::defaults;
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── Dropdown constants ─────────────────────────────────────────────────────
|
||||
|
||||
pub const DROPDOWN_ID: &str = "array_type";
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../../assets/icons/array_rect.svg"));
|
||||
|
||||
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
|
||||
(
|
||||
"ARRAYRECT",
|
||||
"Rectangular Array",
|
||||
IconKind::Svg(include_bytes!("../../../../assets/icons/array_rect.svg")),
|
||||
),
|
||||
(
|
||||
"ARRAYPATH",
|
||||
"Path Array",
|
||||
IconKind::Svg(include_bytes!("../../../../assets/icons/array_path.svg")),
|
||||
),
|
||||
(
|
||||
"ARRAYPOLAR",
|
||||
"Polar Array",
|
||||
IconKind::Svg(include_bytes!("../../../../assets/icons/array_polar.svg")),
|
||||
),
|
||||
];
|
||||
|
||||
// ── Rectangular Array ──────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RectStep {
|
||||
Rows,
|
||||
Cols { rows: u32 },
|
||||
RowSp { rows: u32, cols: u32 },
|
||||
ColSp { rows: u32, cols: u32, row_sp: f32 },
|
||||
}
|
||||
|
||||
pub struct ArrayRectCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
step: RectStep,
|
||||
default_rows: u32,
|
||||
default_cols: u32,
|
||||
default_row_sp: f32,
|
||||
default_col_sp: f32,
|
||||
}
|
||||
|
||||
impl ArrayRectCommand {
|
||||
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
step: RectStep::Rows,
|
||||
default_rows: defaults::get_array_rows() as u32,
|
||||
default_cols: defaults::get_array_cols() as u32,
|
||||
default_row_sp: defaults::get_array_row_sp(),
|
||||
default_col_sp: defaults::get_array_col_sp(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_transforms(rows: u32, cols: u32, row_sp: f32, col_sp: f32) -> Vec<EntityTransform> {
|
||||
let mut t = Vec::new();
|
||||
for r in 0..rows {
|
||||
for c in 0..cols {
|
||||
if r == 0 && c == 0 {
|
||||
continue;
|
||||
}
|
||||
t.push(EntityTransform::Translate(Vec3::new(
|
||||
col_sp * c as f32,
|
||||
row_sp * r as f32,
|
||||
0.0,
|
||||
)));
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArrayRectCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"ARRAYRECT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
RectStep::Rows => format!("ARRAYRECT Enter row count <{}>:", self.default_rows),
|
||||
RectStep::Cols { rows } => format!(
|
||||
"ARRAYRECT Enter column count <{}> [{rows} rows]:",
|
||||
self.default_cols
|
||||
),
|
||||
RectStep::RowSp { rows, cols } => format!(
|
||||
"ARRAYRECT Row spacing <{:.0}> [{rows}×{cols}]:",
|
||||
self.default_row_sp
|
||||
),
|
||||
RectStep::ColSp { rows, cols, row_sp } => format!(
|
||||
"ARRAYRECT Column spacing <{:.0}> [{rows}×{cols}, row={row_sp:.0}]:",
|
||||
self.default_col_sp
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim().replace(',', ".");
|
||||
let t = t.as_str();
|
||||
match self.step {
|
||||
RectStep::Rows => {
|
||||
let rows = if t.is_empty() {
|
||||
self.default_rows
|
||||
} else {
|
||||
let v = t.parse::<u32>().unwrap_or(self.default_rows).max(1);
|
||||
defaults::set_array_rows(v as f32);
|
||||
self.default_rows = v;
|
||||
v
|
||||
};
|
||||
self.step = RectStep::Cols { rows };
|
||||
None
|
||||
}
|
||||
RectStep::Cols { rows } => {
|
||||
let cols = if t.is_empty() {
|
||||
self.default_cols
|
||||
} else {
|
||||
let v = t.parse::<u32>().unwrap_or(self.default_cols).max(1);
|
||||
defaults::set_array_cols(v as f32);
|
||||
self.default_cols = v;
|
||||
v
|
||||
};
|
||||
self.step = RectStep::RowSp { rows, cols };
|
||||
None
|
||||
}
|
||||
RectStep::RowSp { rows, cols } => {
|
||||
let row_sp = if t.is_empty() {
|
||||
self.default_row_sp
|
||||
} else {
|
||||
let v = t.parse::<f32>().unwrap_or(self.default_row_sp);
|
||||
defaults::set_array_row_sp(v);
|
||||
self.default_row_sp = v;
|
||||
v
|
||||
};
|
||||
self.step = RectStep::ColSp { rows, cols, row_sp };
|
||||
None
|
||||
}
|
||||
RectStep::ColSp { rows, cols, row_sp } => {
|
||||
let col_sp = if t.is_empty() {
|
||||
self.default_col_sp
|
||||
} else {
|
||||
let v = t.parse::<f32>().unwrap_or(self.default_col_sp);
|
||||
defaults::set_array_col_sp(v);
|
||||
v
|
||||
};
|
||||
Some(CmdResult::BatchCopy(
|
||||
self.handles.clone(),
|
||||
Self::build_transforms(rows, cols, row_sp, col_sp),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
|
||||
let (rows, cols, row_sp, col_sp) = match self.step {
|
||||
RectStep::Rows => (
|
||||
self.default_rows,
|
||||
self.default_cols,
|
||||
self.default_row_sp,
|
||||
self.default_col_sp,
|
||||
),
|
||||
RectStep::Cols { rows } => (
|
||||
rows,
|
||||
self.default_cols,
|
||||
self.default_row_sp,
|
||||
self.default_col_sp,
|
||||
),
|
||||
RectStep::RowSp { rows, cols } => {
|
||||
(rows, cols, self.default_row_sp, self.default_col_sp)
|
||||
}
|
||||
RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp),
|
||||
};
|
||||
Self::build_transforms(rows, cols, row_sp, col_sp)
|
||||
.iter()
|
||||
.flat_map(|t| {
|
||||
if let EntityTransform::Translate(delta) = t {
|
||||
self.wire_models
|
||||
.iter()
|
||||
.map(|w| w.translated(*delta))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// Enter with empty input = use default for current step
|
||||
self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ── Polar Array ────────────────────────────────────────────────────────────
|
||||
|
||||
enum PolarStep {
|
||||
Center,
|
||||
Count { center: Vec3 },
|
||||
Angle { center: Vec3, count: u32 },
|
||||
}
|
||||
|
||||
pub struct ArrayPolarCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
step: PolarStep,
|
||||
default_count: u32,
|
||||
default_angle: f32,
|
||||
}
|
||||
|
||||
impl ArrayPolarCommand {
|
||||
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
step: PolarStep::Center,
|
||||
default_count: defaults::get_array_p_count() as u32,
|
||||
default_angle: defaults::get_array_p_angle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArrayPolarCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"ARRAYPOLAR"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
PolarStep::Center => format!(
|
||||
"ARRAYPOLAR Specify center point [{} objects]:",
|
||||
self.handles.len()
|
||||
),
|
||||
PolarStep::Count { .. } => {
|
||||
format!("ARRAYPOLAR Enter item count <{}>:", self.default_count)
|
||||
}
|
||||
PolarStep::Angle { count, .. } => format!(
|
||||
"ARRAYPOLAR Enter total angle in degrees <{:.0}> [{count} items]:",
|
||||
self.default_angle
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, PolarStep::Count { .. } | PolarStep::Angle { .. })
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim().replace(',', ".");
|
||||
let t = t.as_str();
|
||||
match &self.step {
|
||||
PolarStep::Count { center } => {
|
||||
let center = *center;
|
||||
let count = if t.is_empty() {
|
||||
self.default_count
|
||||
} else {
|
||||
let v = t.parse::<u32>().unwrap_or(self.default_count).max(2);
|
||||
defaults::set_array_p_count(v as f32);
|
||||
self.default_count = v;
|
||||
v
|
||||
};
|
||||
self.step = PolarStep::Angle { center, count };
|
||||
None
|
||||
}
|
||||
PolarStep::Angle { center, count } => {
|
||||
let center = *center;
|
||||
let count = *count;
|
||||
let total_deg = if t.is_empty() {
|
||||
self.default_angle
|
||||
} else {
|
||||
let v = t.parse::<f32>().unwrap_or(self.default_angle);
|
||||
defaults::set_array_p_angle(v);
|
||||
v
|
||||
};
|
||||
let step_rad = total_deg.to_radians() / count as f32;
|
||||
let transforms = (1..count)
|
||||
.map(|n| EntityTransform::Rotate {
|
||||
center,
|
||||
angle_rad: step_rad * n as f32,
|
||||
})
|
||||
.collect();
|
||||
Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
||||
let (center, count, total_deg) = match &self.step {
|
||||
PolarStep::Center => (pt, self.default_count, self.default_angle),
|
||||
PolarStep::Count { center } => (*center, self.default_count, self.default_angle),
|
||||
PolarStep::Angle { center, count } => (*center, *count, self.default_angle),
|
||||
};
|
||||
let step_rad = total_deg.to_radians() / count as f32;
|
||||
let mut out: Vec<WireModel> = (1..count)
|
||||
.flat_map(|n| {
|
||||
let angle_rad = step_rad * n as f32;
|
||||
self.wire_models
|
||||
.iter()
|
||||
.map(move |w| w.rotated(center, angle_rad))
|
||||
})
|
||||
.collect();
|
||||
// Rubber-band from center to cursor while picking the center point.
|
||||
if matches!(self.step, PolarStep::Center) {
|
||||
out.push(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
vec![[center.x, center.y, center.z], [pt.x, pt.y, pt.z]],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
if let PolarStep::Center = self.step {
|
||||
self.step = PolarStep::Count { center: pt };
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ── Path Array ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// ARRAYPATH:
|
||||
// Copies selected objects at equal arc-length intervals along a path entity.
|
||||
// 1. Select path entity (Line, Arc, Circle, LwPolyline)
|
||||
// 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 std::f32::consts::TAU as FTAU;
|
||||
|
||||
// ── Path geometry helpers ──────────────────────────────────────────────────
|
||||
|
||||
/// Tessellate an LwPolyline.
|
||||
fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<Vec3> {
|
||||
let z = p.elevation as f32;
|
||||
let verts = &p.vertices;
|
||||
if verts.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let n = verts.len();
|
||||
let segs = if p.is_closed { n } else { n.saturating_sub(1) };
|
||||
let mut out: Vec<Vec3> = vec![];
|
||||
|
||||
for i in 0..segs {
|
||||
let v0 = &verts[i];
|
||||
let v1 = &verts[(i + 1) % n];
|
||||
let x0 = v0.location.x as f32;
|
||||
let y0 = v0.location.y as f32;
|
||||
let x1 = v1.location.x as f32;
|
||||
let y1 = v1.location.y as f32;
|
||||
|
||||
if out.is_empty() {
|
||||
out.push(Vec3::new(x0, y0, z));
|
||||
}
|
||||
|
||||
let bulge = v0.bulge as f32;
|
||||
if bulge.abs() < 1e-9 {
|
||||
out.push(Vec3::new(x1, y1, z));
|
||||
} else {
|
||||
let dx = x1 - x0;
|
||||
let dy = y1 - y0;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d < 1e-9 {
|
||||
out.push(Vec3::new(x1, y1, z));
|
||||
continue;
|
||||
}
|
||||
let angle = 4.0 * bulge.atan();
|
||||
let r = (d * 0.5) / (angle * 0.5).sin().abs();
|
||||
let mx = (x0 + x1) * 0.5;
|
||||
let my = (y0 + y1) * 0.5;
|
||||
let inv = 1.0 / d;
|
||||
let (px, py) = (-dy * inv, dx * inv);
|
||||
let sign = if bulge > 0.0 { 1.0f32 } else { -1.0 };
|
||||
let h_val = r - (r * r - d * d * 0.25).max(0.0).sqrt();
|
||||
let (cx, cy) = (mx - sign * px * (r - h_val), my - sign * py * (r - h_val));
|
||||
let a0 = (y0 - cy).atan2(x0 - cx);
|
||||
let a1 = (y1 - cy).atan2(x1 - cx);
|
||||
// Signed span: CCW for bulge > 0, CW (negative) for bulge < 0.
|
||||
let span = if bulge > 0.0 {
|
||||
let s = a1 - a0;
|
||||
if s <= 0.0 {
|
||||
s + FTAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
} else {
|
||||
let s = a1 - a0;
|
||||
if s >= 0.0 {
|
||||
s - FTAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let steps = ((r * span.abs() / 2.0).ceil() as usize).clamp(4, 64);
|
||||
for j in 1..=steps {
|
||||
let ang = a0 + span * (j as f32 / steps as f32);
|
||||
out.push(Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), z));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Walk `pts` (ordered) and return `count` points at equal arc-length spacing.
|
||||
fn subsample_equidistant(pts: &[Vec3], count: usize) -> Vec<Vec3> {
|
||||
if count == 0 {
|
||||
return vec![];
|
||||
}
|
||||
if pts.len() < 2 {
|
||||
return vec![pts.first().copied().unwrap_or(Vec3::ZERO); count];
|
||||
}
|
||||
|
||||
let mut cum = vec![0.0f32; pts.len()];
|
||||
for i in 1..pts.len() {
|
||||
cum[i] = cum[i - 1] + pts[i].distance(pts[i - 1]);
|
||||
}
|
||||
let total = *cum.last().unwrap();
|
||||
if total < 1e-9 {
|
||||
return vec![pts[0]; count];
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(count);
|
||||
let mut seg = 0usize;
|
||||
for i in 0..count {
|
||||
let target = if count > 1 {
|
||||
total * i as f32 / (count - 1) as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
while seg + 2 < pts.len() && cum[seg + 1] < target - 1e-9 {
|
||||
seg += 1;
|
||||
}
|
||||
let seg_len = cum[seg + 1] - cum[seg];
|
||||
let t = if seg_len > 1e-9 {
|
||||
(target - cum[seg]) / seg_len
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
out.push(pts[seg].lerp(pts[seg + 1], t.clamp(0.0, 1.0)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── State machine ──────────────────────────────────────────────────────────
|
||||
|
||||
enum PathStep {
|
||||
SelectPath,
|
||||
Count { path_entity: EntityType },
|
||||
}
|
||||
|
||||
pub struct ArrayPathCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
all_entities: Vec<EntityType>,
|
||||
step: PathStep,
|
||||
default_count: u32,
|
||||
}
|
||||
|
||||
impl ArrayPathCommand {
|
||||
pub fn new(
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
all_entities: Vec<EntityType>,
|
||||
) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
all_entities,
|
||||
step: PathStep::SelectPath,
|
||||
default_count: defaults::get_array_path_count() as u32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample `count` evenly-spaced points along `entity`.
|
||||
fn sample_path(entity: &EntityType, count: usize) -> Vec<Vec3> {
|
||||
if count == 0 {
|
||||
return vec![];
|
||||
}
|
||||
let fn_norm = |x: f32| -> f32 { ((x % FTAU) + FTAU) % FTAU };
|
||||
match entity {
|
||||
EntityType::Line(l) => {
|
||||
let p0 = Vec3::new(l.start.x as f32, l.start.y as f32, 0.0);
|
||||
let p1 = Vec3::new(l.end.x as f32, l.end.y as f32, 0.0);
|
||||
let d = (count - 1).max(1) as f32;
|
||||
(0..count).map(|i| p0.lerp(p1, i as f32 / d)).collect()
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
let (cx, cy, r) = (a.center.x as f32, a.center.y as f32, a.radius as f32);
|
||||
let a0 = a.start_angle.to_radians() as f32;
|
||||
let a1 = a.end_angle.to_radians() as f32;
|
||||
let span = {
|
||||
let s = fn_norm(a1) - fn_norm(a0);
|
||||
if s <= 0.0 {
|
||||
s + FTAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let d = (count - 1).max(1) as f32;
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let ang = fn_norm(a0) + span * (i as f32 / d);
|
||||
Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), 0.0)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityType::Circle(c) => {
|
||||
let (cx, cy, r) = (c.center.x as f32, c.center.y as f32, c.radius as f32);
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let ang = i as f32 / count as f32 * FTAU;
|
||||
Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), 0.0)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityType::LwPolyline(p) => subsample_equidistant(&lw_dense_pts(p), count),
|
||||
_ => vec![Vec3::ZERO; count],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build Translate transforms: delta from pts[0] to each subsequent point.
|
||||
fn build_transforms(pts: &[Vec3]) -> Vec<EntityTransform> {
|
||||
if pts.len() < 2 {
|
||||
return vec![];
|
||||
}
|
||||
let origin = pts[0];
|
||||
pts[1..]
|
||||
.iter()
|
||||
.map(|&p| EntityTransform::Translate(p - origin))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArrayPathCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"ARRAYPATH"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
PathStep::SelectPath => format!(
|
||||
"ARRAYPATH Select path entity [{} objects]:",
|
||||
self.handles.len()
|
||||
),
|
||||
PathStep::Count { .. } => {
|
||||
format!("ARRAYPATH Enter item count <{}>:", self.default_count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, PathStep::SelectPath)
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, PathStep::Count { .. })
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() || self.handles.contains(&handle) {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if let Some(entity) = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.cloned()
|
||||
{
|
||||
self.step = PathStep::Count {
|
||||
path_entity: entity,
|
||||
};
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, _pt: Vec3) -> Vec<WireModel> {
|
||||
if handle.is_null() || self.handles.contains(&handle) {
|
||||
return vec![];
|
||||
}
|
||||
if !matches!(self.step, PathStep::SelectPath) {
|
||||
return vec![];
|
||||
}
|
||||
if let Some(entity) = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
{
|
||||
let pts = Self::sample_path(entity, 64);
|
||||
if pts.len() >= 2 {
|
||||
return vec![WireModel::solid(
|
||||
"arraypath_hover".into(),
|
||||
pts.iter().map(|p| [p.x, p.y, p.z]).collect(),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)];
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let PathStep::Count { path_entity } = &self.step else {
|
||||
return None;
|
||||
};
|
||||
let t = text.trim().replace(',', ".");
|
||||
let count = if t.is_empty() {
|
||||
self.default_count
|
||||
} else {
|
||||
let v = t.parse::<u32>().unwrap_or(self.default_count).max(2);
|
||||
defaults::set_array_path_count(v as f32);
|
||||
self.default_count = v;
|
||||
v
|
||||
};
|
||||
let pts = Self::sample_path(path_entity, count as usize);
|
||||
let transforms = Self::build_transforms(&pts);
|
||||
if transforms.is_empty() {
|
||||
return Some(CmdResult::Cancel);
|
||||
}
|
||||
Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
|
||||
let PathStep::Count { path_entity } = &self.step else {
|
||||
return vec![];
|
||||
};
|
||||
let pts = Self::sample_path(path_entity, self.default_count as usize);
|
||||
let transforms = Self::build_transforms(&pts);
|
||||
transforms
|
||||
.iter()
|
||||
.flat_map(|t| {
|
||||
if let EntityTransform::Translate(delta) = t {
|
||||
self.wire_models
|
||||
.iter()
|
||||
.map(|w| w.translated(*delta))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copy tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: COPY (CO)
|
||||
// Requires at least one entity selected before starting.
|
||||
// Step 1: pick base point
|
||||
// Step 2+: each click makes another copy at (click - base); Enter to finish.
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── Ribbon definition ──────────────────────────────────────────────────────
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "COPY",
|
||||
label: "Copy",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/copy.svg")),
|
||||
event: ModuleEvent::Command("COPY".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command implementation ─────────────────────────────────────────────────
|
||||
|
||||
enum Step {
|
||||
Base,
|
||||
Placing(Vec3),
|
||||
}
|
||||
|
||||
pub struct CopyCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
step: Step,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl CopyCommand {
|
||||
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
step: Step::Base,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CopyCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"COPY"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Base => format!(
|
||||
"COPY Specify base point [{} objects]:",
|
||||
self.handles.len()
|
||||
),
|
||||
Step::Placing(base) => format!(
|
||||
"COPY Specify destination [{} copies so far | Enter=done | base {:.3},{:.3}]:",
|
||||
self.count, base.x, base.y
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Base => {
|
||||
self.step = Step::Placing(pt);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Placing(base) => {
|
||||
let delta = pt - *base;
|
||||
self.count += 1;
|
||||
CmdResult::CopySelected(self.handles.clone(), EntityTransform::Translate(delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
||||
let Step::Placing(base) = &self.step else {
|
||||
return vec![];
|
||||
};
|
||||
let delta = pt - *base;
|
||||
let mut out: Vec<WireModel> = self
|
||||
.wire_models
|
||||
.iter()
|
||||
.map(|w| w.translated(delta))
|
||||
.collect();
|
||||
out.push(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Delete tool — ribbon definition.
|
||||
//
|
||||
// Fires the ERASE command (same as pressing Delete key or typing ERASE/E).
|
||||
// Shown as "Delete" in the ribbon so the label matches familiar terminology.
|
||||
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "ERASE",
|
||||
label: "Delete",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/erase.svg")),
|
||||
event: ModuleEvent::Command("ERASE".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// Explode tool — ribbon definition + command implementation.
|
||||
//
|
||||
// Command: EXPLODE (X)
|
||||
// EXPLODE: Breaks compound objects into their constituent simple entities.
|
||||
//
|
||||
// Supported:
|
||||
// LwPolyline → Lines (straight segments) + Arcs (bulge segments)
|
||||
//
|
||||
// Unsupported entity types are skipped silently.
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use acadrust::entities::EntityCommon;
|
||||
use acadrust::entities::{Arc as ArcEnt, Circle as CircleEnt, Line as LineEnt, LwPolyline};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use glam::Vec3;
|
||||
|
||||
// ── Ribbon definition ──────────────────────────────────────────────────────
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "EXPLODE",
|
||||
label: "Explode",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/explode.svg")),
|
||||
event: ModuleEvent::Command("EXPLODE".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Geometry helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/// Decompose an entity into its constituent simple entities.
|
||||
/// Returns an empty vec if the entity cannot be exploded.
|
||||
pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<EntityType> {
|
||||
match entity {
|
||||
EntityType::LwPolyline(p) => explode_lwpolyline(p),
|
||||
EntityType::Insert(ins) => ins
|
||||
.explode_from_document(document)
|
||||
.into_iter()
|
||||
.map(normalize_insert_entity)
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_insert_entity(mut entity: EntityType) -> EntityType {
|
||||
match &mut entity {
|
||||
EntityType::Arc(arc) => {
|
||||
arc.start_angle = arc.start_angle.to_degrees();
|
||||
arc.end_angle = arc.end_angle.to_degrees();
|
||||
}
|
||||
EntityType::Ellipse(ell) => {
|
||||
let major_len = ell.major_axis_length();
|
||||
let full_span = {
|
||||
let mut span = ell.end_parameter - ell.start_parameter;
|
||||
if span < 0.0 {
|
||||
span += std::f64::consts::TAU;
|
||||
}
|
||||
(span - std::f64::consts::TAU).abs() < 1e-6
|
||||
};
|
||||
if (ell.minor_axis_ratio - 1.0).abs() < 1e-6 && full_span {
|
||||
let mut circle = CircleEnt::new();
|
||||
circle.common = ell.common.clone();
|
||||
circle.center = ell.center;
|
||||
circle.radius = major_len;
|
||||
circle.normal = ell.normal;
|
||||
entity = EntityType::Circle(circle);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
entity.common_mut().owner_handle = Handle::NULL;
|
||||
entity
|
||||
}
|
||||
|
||||
pub fn normalize_entity_for_block(mut entity: EntityType) -> EntityType {
|
||||
if let EntityType::Arc(arc) = &mut entity {
|
||||
arc.start_angle = arc.start_angle.to_radians();
|
||||
arc.end_angle = arc.end_angle.to_radians();
|
||||
}
|
||||
entity
|
||||
}
|
||||
|
||||
fn explode_lwpolyline(p: &LwPolyline) -> Vec<EntityType> {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let elevation = p.elevation;
|
||||
let n_segs = if p.is_closed { n } else { n - 1 };
|
||||
|
||||
let mut result = Vec::new();
|
||||
for i in 0..n_segs {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
|
||||
let p0 = [v0.location.x, v0.location.y];
|
||||
let p1 = [v1.location.x, v1.location.y];
|
||||
|
||||
if v0.bulge.abs() < 1e-10 {
|
||||
// Straight segment → Line
|
||||
let mut common = p.common.clone();
|
||||
common.handle = Handle::NULL;
|
||||
let line = LineEnt {
|
||||
common,
|
||||
start: Vector3::new(p0[0], p0[1], elevation),
|
||||
end: Vector3::new(p1[0], p1[1], elevation),
|
||||
..LineEnt::new()
|
||||
};
|
||||
result.push(EntityType::Line(line));
|
||||
} else {
|
||||
// Arc segment from bulge
|
||||
if let Some(arc) = bulge_to_arc(p0, p1, v0.bulge, elevation, &p.common) {
|
||||
result.push(arc);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Convert a polyline bulge segment to an Arc entity.
|
||||
/// Arc angles are measured from the +X axis.
|
||||
fn bulge_to_arc(
|
||||
p0: [f64; 2],
|
||||
p1: [f64; 2],
|
||||
bulge: f64,
|
||||
elevation: f64,
|
||||
common_src: &EntityCommon,
|
||||
) -> Option<EntityType> {
|
||||
let chord_x = p1[0] - p0[0];
|
||||
let chord_y = p1[1] - p0[1];
|
||||
let chord_len = (chord_x * chord_x + chord_y * chord_y).sqrt();
|
||||
if chord_len < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Included angle = 4 * atan(bulge)
|
||||
let b = bulge;
|
||||
let b2 = b * b;
|
||||
|
||||
// Radius: r = chord * (1 + b²) / (4 * |b|)
|
||||
let r = chord_len * (1.0 + b2) / (4.0 * b.abs());
|
||||
|
||||
// Perpendicular distance from midpoint to center:
|
||||
// d = r * cos(theta/2), where cos(theta/2) = (1 - b²) / (1 + b²)
|
||||
let d = r * (1.0 - b2) / (1.0 + b2);
|
||||
|
||||
// Midpoint
|
||||
let mx = (p0[0] + p1[0]) * 0.5;
|
||||
let my = (p0[1] + p1[1]) * 0.5;
|
||||
|
||||
// Left-perpendicular direction (rotate chord 90° CCW)
|
||||
let perp_x = -chord_y / chord_len;
|
||||
let perp_y = chord_x / chord_len;
|
||||
|
||||
// Positive bulge → center is to the left of chord direction (CCW arc)
|
||||
let sign = b.signum();
|
||||
let cx = mx + sign * d * perp_x;
|
||||
let cy = my + sign * d * perp_y;
|
||||
|
||||
// Angles from center to p0 and p1
|
||||
let a0_rad = (p0[1] - cy).atan2(p0[0] - cx);
|
||||
let a1_rad = (p1[1] - cy).atan2(p1[0] - cx);
|
||||
|
||||
// acadrust arcs are always CCW: positive bulge → CCW from p0 to p1
|
||||
// Negative bulge → CW from p0 to p1 = CCW from p1 to p0
|
||||
let (start_deg, end_deg) = if b > 0.0 {
|
||||
(norm_deg(a0_rad.to_degrees()), norm_deg(a1_rad.to_degrees()))
|
||||
} else {
|
||||
(norm_deg(a1_rad.to_degrees()), norm_deg(a0_rad.to_degrees()))
|
||||
};
|
||||
|
||||
let mut common = common_src.clone();
|
||||
common.handle = Handle::NULL;
|
||||
|
||||
let arc = ArcEnt {
|
||||
common,
|
||||
center: Vector3::new(cx, cy, elevation),
|
||||
radius: r,
|
||||
start_angle: start_deg,
|
||||
end_angle: end_deg,
|
||||
..ArcEnt::new()
|
||||
};
|
||||
Some(EntityType::Arc(arc))
|
||||
}
|
||||
|
||||
fn norm_deg(a: f64) -> f64 {
|
||||
let t = TAU.to_degrees();
|
||||
((a % t) + t) % t
|
||||
}
|
||||
|
||||
// ── Command stub (kept for future interactive selection mode) ───────────────
|
||||
|
||||
pub struct ExplodeCommand;
|
||||
|
||||
impl ExplodeCommand {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ExplodeCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"EXPLODE"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
"EXPLODE Select objects to explode:".into()
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// Extend — dispatch handled by trim.rs dropdown. This file exists for module resolution.
|
||||
@@ -0,0 +1,830 @@
|
||||
// Fillet / Chamfer — ribbon definitions + full command implementations.
|
||||
//
|
||||
// FILLET (F):
|
||||
// Pick two lines. Command finds their intersection, computes a tangent arc
|
||||
// of radius R, trims both lines to the tangent points, and inserts the arc.
|
||||
// R=0 just extends/trims both lines to the exact intersection (sharp corner).
|
||||
//
|
||||
// CHAMFER (CHA):
|
||||
// Pick two lines. Command finds their intersection, backs off dist1 along
|
||||
// line 1 and dist2 along line 2, trims, and inserts a straight chamfer line.
|
||||
|
||||
use acadrust::entities::{Arc as ArcEnt, Line as LineEnt};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::home::defaults;
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── Dropdown constants ─────────────────────────────────────────────────────
|
||||
|
||||
pub const DROPDOWN_ID: &str = "fillet_chamfer";
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../../assets/icons/fillet.svg"));
|
||||
|
||||
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
|
||||
(
|
||||
"FILLET",
|
||||
"Fillet",
|
||||
IconKind::Svg(include_bytes!("../../../../assets/icons/fillet.svg")),
|
||||
),
|
||||
(
|
||||
"CHAMFER",
|
||||
"Chamfer",
|
||||
IconKind::Svg(include_bytes!("../../../../assets/icons/chamfer.svg")),
|
||||
),
|
||||
];
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// Geometry
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Intersect two infinite lines. Returns (t on L1, u on L2).
|
||||
fn ll(
|
||||
ax: f64,
|
||||
ay: f64,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
ex: f64,
|
||||
ey: f64,
|
||||
) -> Option<(f64, f64)> {
|
||||
let det = dx * ey - dy * ex;
|
||||
if det.abs() < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let t = ((cx - ax) * ey - (cy - ay) * ex) / det;
|
||||
let u = ((cx - ax) * dy - (cy - ay) * dx) / det;
|
||||
Some((t, u))
|
||||
}
|
||||
|
||||
/// Extract coords and unit direction for a Line entity.
|
||||
fn line_geom(l: &LineEnt) -> ([f64; 2], [f64; 2], [f64; 2], f64) {
|
||||
let p1 = [l.start.x, l.start.y];
|
||||
let p2 = [l.end.x, l.end.y];
|
||||
let dx = p2[0] - p1[0];
|
||||
let dy = p2[1] - p1[1];
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||
(p1, p2, [dx / len, dy / len], len)
|
||||
}
|
||||
|
||||
/// Project click onto line, returning t ∈ ℝ.
|
||||
fn project_click(click: [f64; 2], p1: [f64; 2], unit: [f64; 2]) -> f64 {
|
||||
(click[0] - p1[0]) * unit[0] + (click[1] - p1[1]) * unit[1]
|
||||
}
|
||||
|
||||
// ── Fillet ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compute fillet: trim l1/l2 and insert a tangent arc of `radius`.
|
||||
/// Returns (trimmed_l1, trimmed_l2, fillet_arc).
|
||||
fn compute_fillet(
|
||||
l1: &LineEnt,
|
||||
click1: [f64; 2],
|
||||
l2: &LineEnt,
|
||||
click2: [f64; 2],
|
||||
radius: f64,
|
||||
) -> Option<(EntityType, EntityType, Option<EntityType>)> {
|
||||
let (p1, _p2, u1, _len1) = line_geom(l1);
|
||||
let (p3, _p4, u2, _len2) = line_geom(l2);
|
||||
|
||||
// Intersection of infinite lines
|
||||
let (t_p, u_p) = ll(p1[0], p1[1], u1[0], u1[1], p3[0], p3[1], u2[0], u2[1])?;
|
||||
|
||||
// Intersection point
|
||||
let px = p1[0] + t_p * u1[0];
|
||||
let py = p1[1] + t_p * u1[1];
|
||||
|
||||
// Direction from P toward each click (the "keep" side)
|
||||
let s1 = project_click(click1, [px, py], u1); // positive = along u1
|
||||
let s2 = project_click(click2, [px, py], u2);
|
||||
let dir1 = if s1 >= 0.0 {
|
||||
[u1[0], u1[1]]
|
||||
} else {
|
||||
[-u1[0], -u1[1]]
|
||||
};
|
||||
let dir2 = if s2 >= 0.0 {
|
||||
[u2[0], u2[1]]
|
||||
} else {
|
||||
[-u2[0], -u2[1]]
|
||||
};
|
||||
|
||||
// Angle between the two keep-directions
|
||||
let cos_a = (dir1[0] * dir2[0] + dir1[1] * dir2[1]).clamp(-1.0, 1.0);
|
||||
let angle = cos_a.acos();
|
||||
|
||||
// Lines are parallel / anti-parallel
|
||||
if angle < 1e-6 || (angle - std::f64::consts::PI).abs() < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let half = angle / 2.0;
|
||||
let z = l1.start.z;
|
||||
|
||||
if radius < 1e-9 {
|
||||
// r = 0: just extend/trim both lines to the intersection
|
||||
let (new_l1, new_l2) = trim_to_point(l1, t_p, p1, u1, l2, u_p, p3, u2)?;
|
||||
return Some((EntityType::Line(new_l1), EntityType::Line(new_l2), None));
|
||||
}
|
||||
|
||||
// Distance from P to tangent points
|
||||
let d = radius / half.tan();
|
||||
|
||||
// Tangent points
|
||||
let t1 = [px + d * dir1[0], py + d * dir1[1]];
|
||||
let t2 = [px + d * dir2[0], py + d * dir2[1]];
|
||||
|
||||
// Arc center: along bisector of dir1+dir2, distance = r / sin(half)
|
||||
let bx = dir1[0] + dir2[0];
|
||||
let by = dir1[1] + dir2[1];
|
||||
let blen = (bx * bx + by * by).sqrt();
|
||||
if blen < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let arc_dist = radius / half.sin();
|
||||
let arc_cx = px + arc_dist * bx / blen;
|
||||
let arc_cy = py + arc_dist * by / blen;
|
||||
|
||||
// Arc angles in degrees
|
||||
let a_start_deg = (t1[1] - arc_cy).atan2(t1[0] - arc_cx).to_degrees();
|
||||
let a_end_deg = (t2[1] - arc_cy).atan2(t2[0] - arc_cx).to_degrees();
|
||||
|
||||
// Pick CCW direction that fills the concave corner
|
||||
let cross = dir1[0] * dir2[1] - dir1[1] * dir2[0];
|
||||
let (start_deg, end_deg) = if cross <= 0.0 {
|
||||
(a_start_deg, a_end_deg)
|
||||
} else {
|
||||
(a_end_deg, a_start_deg)
|
||||
};
|
||||
|
||||
// Trim l1 to T1 and l2 to T2
|
||||
let new_l1 = trim_to_xy(l1, t_p, t1, dir1, p1, u1)?;
|
||||
let new_l2 = trim_to_xy(l2, u_p, t2, dir2, p3, u2)?;
|
||||
|
||||
// Build arc entity
|
||||
let mut arc = ArcEnt::new();
|
||||
arc.common = l1.common.clone();
|
||||
arc.common.handle = Handle::NULL;
|
||||
arc.center = Vector3::new(arc_cx, arc_cy, z);
|
||||
arc.radius = radius;
|
||||
arc.start_angle = start_deg;
|
||||
arc.end_angle = end_deg;
|
||||
|
||||
Some((
|
||||
EntityType::Line(new_l1),
|
||||
EntityType::Line(new_l2),
|
||||
Some(EntityType::Arc(arc)),
|
||||
))
|
||||
}
|
||||
|
||||
/// Trim a line's parameter to an intersection t on the same side as dir (keep side).
|
||||
fn trim_to_xy(
|
||||
orig: &LineEnt,
|
||||
t_isect: f64,
|
||||
tangent: [f64; 2],
|
||||
dir: [f64; 2],
|
||||
p1: [f64; 2],
|
||||
unit: [f64; 2],
|
||||
) -> Option<LineEnt> {
|
||||
let z = orig.start.z;
|
||||
let mut l = orig.clone();
|
||||
l.common.handle = Handle::NULL;
|
||||
|
||||
// t_tangent: parameter of the tangent point along the line from start
|
||||
let t_tan = (tangent[0] - p1[0]) * unit[0] + (tangent[1] - p1[1]) * unit[1];
|
||||
|
||||
// dir is positive along unit → we keep the portion BEYOND t_tan in that direction
|
||||
// dir positive: keep from t_tan to +∞ (i.e. set start to tangent point)
|
||||
// dir negative: keep from -∞ to t_tan (i.e. set end to tangent point)
|
||||
let len = {
|
||||
let dx = orig.end.x - orig.start.x;
|
||||
let dy = orig.end.y - orig.start.y;
|
||||
(dx * dx + dy * dy).sqrt().max(1e-12)
|
||||
};
|
||||
let dot = dir[0] * unit[0] + dir[1] * unit[1]; // +1 or -1
|
||||
|
||||
if dot > 0.0 {
|
||||
// keep from tangent to end → move start to tangent point
|
||||
l.start = Vector3::new(tangent[0], tangent[1], z);
|
||||
} else {
|
||||
// keep from start to tangent → move end to tangent point
|
||||
l.end = Vector3::new(tangent[0], tangent[1], z);
|
||||
}
|
||||
let _ = (t_isect, len, t_tan); // used implicitly via `dot`
|
||||
Some(l)
|
||||
}
|
||||
|
||||
/// Trim both lines exactly to their intersection point (r=0 case).
|
||||
fn trim_to_point(
|
||||
l1: &LineEnt,
|
||||
t_p: f64,
|
||||
p1: [f64; 2],
|
||||
u1: [f64; 2],
|
||||
l2: &LineEnt,
|
||||
u_p: f64,
|
||||
_p3: [f64; 2],
|
||||
_u2: [f64; 2],
|
||||
) -> Option<(LineEnt, LineEnt)> {
|
||||
let px = p1[0] + t_p * u1[0];
|
||||
let py = p1[1] + t_p * u1[1];
|
||||
let z1 = l1.start.z;
|
||||
let z2 = l2.start.z;
|
||||
|
||||
// For l1: if t_p is past the midpoint, keep start…P; else keep P…end
|
||||
// We use the same "which end is P closer to" logic
|
||||
let mut ll1 = l1.clone();
|
||||
ll1.common.handle = Handle::NULL;
|
||||
let mut ll2 = l2.clone();
|
||||
ll2.common.handle = Handle::NULL;
|
||||
|
||||
if t_p >= 0.0 {
|
||||
ll1.end = Vector3::new(px, py, z1);
|
||||
} else {
|
||||
ll1.start = Vector3::new(px, py, z1);
|
||||
}
|
||||
|
||||
if u_p >= 0.0 {
|
||||
ll2.end = Vector3::new(px, py, z2);
|
||||
} else {
|
||||
ll2.start = Vector3::new(px, py, z2);
|
||||
}
|
||||
|
||||
Some((ll1, ll2))
|
||||
}
|
||||
|
||||
// ── Point-generation helpers ──────────────────────────────────────────────
|
||||
|
||||
fn line_pts(l: &LineEnt) -> Vec<[f32; 3]> {
|
||||
vec![
|
||||
[l.start.x as f32, l.start.y as f32, l.start.z as f32],
|
||||
[l.end.x as f32, l.end.y as f32, l.end.z as f32],
|
||||
]
|
||||
}
|
||||
|
||||
fn arc_pts(cx: f64, cy: f64, r: f64, a0_deg: f64, a1_deg: f64, y: f64) -> Vec<[f32; 3]> {
|
||||
use std::f64::consts::TAU;
|
||||
let fn_norm = |a: f64| -> f64 { ((a % TAU) + TAU) % TAU };
|
||||
let a0 = a0_deg.to_radians();
|
||||
let a1 = a1_deg.to_radians();
|
||||
let span = {
|
||||
let s = fn_norm(a1) - fn_norm(a0);
|
||||
if s <= 0.0 {
|
||||
s + TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let steps = (span.abs() * 20.0).ceil().max(4.0) as usize;
|
||||
(0..=steps)
|
||||
.map(|i| {
|
||||
let ang = fn_norm(a0) + span * (i as f64 / steps as f64);
|
||||
[
|
||||
(cx + r * ang.cos()) as f32,
|
||||
y as f32,
|
||||
(cy + r * ang.sin()) as f32,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> {
|
||||
match e {
|
||||
EntityType::Line(l) => line_pts(l),
|
||||
EntityType::Arc(a) => arc_pts(
|
||||
a.center.x,
|
||||
a.center.y,
|
||||
a.radius,
|
||||
a.start_angle,
|
||||
a.end_angle,
|
||||
a.center.y,
|
||||
),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// ── Chamfer ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compute chamfer: trim l1 by dist1 from intersection, l2 by dist2, add chamfer line.
|
||||
fn compute_chamfer(
|
||||
l1: &LineEnt,
|
||||
click1: [f64; 2],
|
||||
dist1: f64,
|
||||
l2: &LineEnt,
|
||||
click2: [f64; 2],
|
||||
dist2: f64,
|
||||
) -> Option<(EntityType, EntityType, EntityType)> {
|
||||
let (p1, _, u1, _) = line_geom(l1);
|
||||
let (p3, _, u2, _) = line_geom(l2);
|
||||
|
||||
let (t_p, u_p) = ll(p1[0], p1[1], u1[0], u1[1], p3[0], p3[1], u2[0], u2[1])?;
|
||||
|
||||
let px = p1[0] + t_p * u1[0];
|
||||
let py = p1[1] + t_p * u1[1];
|
||||
let z = l1.start.z;
|
||||
|
||||
let s1 = project_click(click1, [px, py], u1);
|
||||
let s2 = project_click(click2, [px, py], u2);
|
||||
let dir1 = if s1 >= 0.0 {
|
||||
[u1[0], u1[1]]
|
||||
} else {
|
||||
[-u1[0], -u1[1]]
|
||||
};
|
||||
let dir2 = if s2 >= 0.0 {
|
||||
[u2[0], u2[1]]
|
||||
} else {
|
||||
[-u2[0], -u2[1]]
|
||||
};
|
||||
|
||||
// Chamfer points: back off dist from P along keep-direction
|
||||
let c1 = [px + dist1 * dir1[0], py + dist1 * dir1[1]];
|
||||
let c2 = [px + dist2 * dir2[0], py + dist2 * dir2[1]];
|
||||
|
||||
// Trim l1 to c1 and l2 to c2
|
||||
let new_l1 = trim_to_xy(l1, t_p, c1, dir1, p1, u1)?;
|
||||
let new_l2 = trim_to_xy(l2, u_p, c2, dir2, p3, u2)?;
|
||||
|
||||
// Chamfer line
|
||||
let mut cline = l1.clone();
|
||||
cline.common.handle = Handle::NULL;
|
||||
cline.start = Vector3::new(c1[0], c1[1], z);
|
||||
cline.end = Vector3::new(c2[0], c2[1], z);
|
||||
|
||||
Some((
|
||||
EntityType::Line(new_l1),
|
||||
EntityType::Line(new_l2),
|
||||
EntityType::Line(cline),
|
||||
))
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// FilletCommand
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
enum FilletStep {
|
||||
First,
|
||||
Second {
|
||||
h1: Handle,
|
||||
l1: LineEnt,
|
||||
click1: [f64; 2],
|
||||
},
|
||||
}
|
||||
|
||||
pub struct FilletCommand {
|
||||
radius: f64,
|
||||
step: FilletStep,
|
||||
all_entities: Vec<EntityType>,
|
||||
}
|
||||
|
||||
impl FilletCommand {
|
||||
pub fn new(radius: f32, all_entities: Vec<EntityType>) -> Self {
|
||||
Self {
|
||||
radius: radius as f64,
|
||||
step: FilletStep::First,
|
||||
all_entities,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for FilletCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"FILLET"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
FilletStep::First => format!(
|
||||
"FILLET Select first line [R={:.4} | type R <val> to change]:",
|
||||
self.radius
|
||||
),
|
||||
FilletStep::Second { .. } => {
|
||||
format!("FILLET Select second line [R={:.4}]:", self.radius)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, FilletStep::First)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim();
|
||||
let body = if t.to_uppercase().starts_with('R') {
|
||||
t[1..].trim()
|
||||
} else {
|
||||
t
|
||||
};
|
||||
if let Ok(v) = body.replace(',', ".").parse::<f64>() {
|
||||
if v >= 0.0 {
|
||||
self.radius = v;
|
||||
defaults::set_fillet_radius(v as f32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let click = [pt.x as f64, pt.y as f64];
|
||||
|
||||
match &self.step {
|
||||
FilletStep::First => {
|
||||
// Must be a line
|
||||
let l1 = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.and_then(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(l.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(l) = l1 {
|
||||
self.step = FilletStep::Second {
|
||||
h1: handle,
|
||||
l1: l,
|
||||
click1: click,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::NeedPoint // not a line — ignore
|
||||
}
|
||||
}
|
||||
FilletStep::Second { h1, l1, click1 } => {
|
||||
let h1 = *h1;
|
||||
let l1 = l1.clone();
|
||||
let click1 = *click1;
|
||||
if handle == h1 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
let l2 = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.and_then(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(l.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(l2) = l2 {
|
||||
match compute_fillet(&l1, click1, &l2, click, self.radius) {
|
||||
Some((new_l1, new_l2, maybe_arc)) => {
|
||||
let mut additions = vec![];
|
||||
if let Some(arc) = maybe_arc {
|
||||
additions.push(arc);
|
||||
}
|
||||
CmdResult::ReplaceMany(
|
||||
vec![(h1, vec![new_l1]), (handle, vec![new_l2])],
|
||||
additions,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
// Could not compute fillet (parallel lines, etc.)
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, pt: Vec3) -> Vec<WireModel> {
|
||||
if handle.is_null() {
|
||||
return vec![];
|
||||
}
|
||||
let click = [pt.x as f64, pt.y as f64];
|
||||
|
||||
match &self.step {
|
||||
FilletStep::First => {
|
||||
// Highlight hovered line in cyan
|
||||
let pts = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.and_then(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(line_pts(l))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(pts) = pts {
|
||||
vec![WireModel::solid(
|
||||
"fillet_hover".into(),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
FilletStep::Second { l1, click1, .. } => {
|
||||
let l1 = l1.clone();
|
||||
let click1 = *click1;
|
||||
if handle.is_null() {
|
||||
return vec![];
|
||||
}
|
||||
let l2 = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.and_then(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(l.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(l2) = l2 {
|
||||
if let Some((new_l1, new_l2, maybe_arc)) =
|
||||
compute_fillet(&l1, click1, &l2, click, self.radius)
|
||||
{
|
||||
let mut out = vec![
|
||||
WireModel::solid(
|
||||
"fillet_l1".into(),
|
||||
entity_pts(&new_l1),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
),
|
||||
WireModel::solid(
|
||||
"fillet_l2".into(),
|
||||
entity_pts(&new_l2),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
),
|
||||
];
|
||||
if let Some(arc) = maybe_arc {
|
||||
out.push(WireModel::solid(
|
||||
"fillet_arc".into(),
|
||||
entity_pts(&arc),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ChamferCommand
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
enum ChamferStep {
|
||||
First,
|
||||
Second {
|
||||
h1: Handle,
|
||||
l1: LineEnt,
|
||||
click1: [f64; 2],
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ChamferCommand {
|
||||
dist1: f64,
|
||||
dist2: f64,
|
||||
step: ChamferStep,
|
||||
all_entities: Vec<EntityType>,
|
||||
}
|
||||
|
||||
impl ChamferCommand {
|
||||
pub fn new(dist: f32, all_entities: Vec<EntityType>) -> Self {
|
||||
Self {
|
||||
dist1: dist as f64,
|
||||
dist2: defaults::get_chamfer_dist2() as f64,
|
||||
step: ChamferStep::First,
|
||||
all_entities,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ChamferCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"CHAMFER"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
ChamferStep::First => format!(
|
||||
"CHAMFER Select first line [D1={:.4} D2={:.4} | type D <d1> <d2>]:",
|
||||
self.dist1, self.dist2
|
||||
),
|
||||
ChamferStep::Second { .. } => format!(
|
||||
"CHAMFER Select second line [D1={:.4} D2={:.4}]:",
|
||||
self.dist1, self.dist2
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, ChamferStep::First)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim();
|
||||
let body = if t.to_uppercase().starts_with('D') {
|
||||
t[1..].trim()
|
||||
} else {
|
||||
t
|
||||
};
|
||||
let parts: Vec<f64> = body
|
||||
.split_whitespace()
|
||||
.filter_map(|s| s.replace(',', ".").parse::<f64>().ok())
|
||||
.collect();
|
||||
if let Some(&v) = parts.first() {
|
||||
self.dist1 = v.max(0.0);
|
||||
defaults::set_chamfer_dist1(self.dist1 as f32);
|
||||
}
|
||||
if let Some(&v) = parts.get(1) {
|
||||
self.dist2 = v.max(0.0);
|
||||
defaults::set_chamfer_dist2(self.dist2 as f32);
|
||||
} else if parts.len() == 1 {
|
||||
self.dist2 = self.dist1;
|
||||
defaults::set_chamfer_dist2(self.dist2 as f32);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let click = [pt.x as f64, pt.y as f64];
|
||||
|
||||
match &self.step {
|
||||
ChamferStep::First => {
|
||||
let l1 = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.and_then(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(l.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(l) = l1 {
|
||||
self.step = ChamferStep::Second {
|
||||
h1: handle,
|
||||
l1: l,
|
||||
click1: click,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
ChamferStep::Second { h1, l1, click1 } => {
|
||||
let h1 = *h1;
|
||||
let l1 = l1.clone();
|
||||
let click1 = *click1;
|
||||
if handle == h1 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
let l2 = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.and_then(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(l.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(l2) = l2 {
|
||||
match compute_chamfer(&l1, click1, self.dist1, &l2, click, self.dist2) {
|
||||
Some((new_l1, new_l2, chamfer_line)) => CmdResult::ReplaceMany(
|
||||
vec![(h1, vec![new_l1]), (handle, vec![new_l2])],
|
||||
vec![chamfer_line],
|
||||
),
|
||||
None => CmdResult::NeedPoint,
|
||||
}
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, pt: Vec3) -> Vec<WireModel> {
|
||||
if handle.is_null() {
|
||||
return vec![];
|
||||
}
|
||||
let click = [pt.x as f64, pt.y as f64];
|
||||
|
||||
match &self.step {
|
||||
ChamferStep::First => {
|
||||
let pts = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.and_then(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(line_pts(l))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(pts) = pts {
|
||||
vec![WireModel::solid(
|
||||
"chamfer_hover".into(),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
ChamferStep::Second { l1, click1, .. } => {
|
||||
let l1 = l1.clone();
|
||||
let click1 = *click1;
|
||||
let l2 = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.and_then(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(l.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(l2) = l2 {
|
||||
if let Some((new_l1, new_l2, cline)) =
|
||||
compute_chamfer(&l1, click1, self.dist1, &l2, click, self.dist2)
|
||||
{
|
||||
return vec![
|
||||
WireModel::solid(
|
||||
"chamfer_l1".into(),
|
||||
entity_pts(&new_l1),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
),
|
||||
WireModel::solid(
|
||||
"chamfer_l2".into(),
|
||||
entity_pts(&new_l2),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
),
|
||||
WireModel::solid(
|
||||
"chamfer_line".into(),
|
||||
entity_pts(&cline),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Group tool — ribbon definition.
|
||||
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "GROUP",
|
||||
label: "Group",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/group.svg")),
|
||||
event: ModuleEvent::Command("GROUP".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Mirror tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: MIRROR (MI)
|
||||
// Requires at least one entity selected.
|
||||
// Step 1: pick first mirror-line point
|
||||
// Step 2: pick second mirror-line point → mirrors
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "MIRROR",
|
||||
label: "Mirror",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mirror.svg")),
|
||||
event: ModuleEvent::Command("MIRROR".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
enum Step {
|
||||
P1,
|
||||
P2(Vec3),
|
||||
}
|
||||
|
||||
pub struct MirrorCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
step: Step,
|
||||
}
|
||||
|
||||
impl MirrorCommand {
|
||||
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
step: Step::P1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MirrorCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"MIRROR"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::P1 => format!(
|
||||
"MIRROR Specify first mirror-line point [{} objects]:",
|
||||
self.handles.len()
|
||||
),
|
||||
Step::P2(p1) => format!(
|
||||
"MIRROR Specify second point [p1={:.2},{:.2}]:",
|
||||
p1.x, p1.y
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::P1 => {
|
||||
self.step = Step::P2(pt);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::P2(p1) => {
|
||||
let p1 = *p1;
|
||||
CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Mirror { p1, p2: pt },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
||||
let Step::P2(p1) = &self.step else {
|
||||
return vec![];
|
||||
};
|
||||
let p1 = *p1;
|
||||
// Mirrored ghosts of all selected objects.
|
||||
let mut out: Vec<WireModel> = self
|
||||
.wire_models
|
||||
.iter()
|
||||
.map(|w| w.mirrored(p1, pt))
|
||||
.collect();
|
||||
// Mirror-axis line (rubber-band).
|
||||
out.push(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
vec![[p1.x, p1.y, p1.z], [pt.x, pt.y, pt.z]],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
pub mod array;
|
||||
pub mod copy;
|
||||
pub mod delete;
|
||||
pub mod explode;
|
||||
mod extend;
|
||||
pub mod fillet;
|
||||
pub mod group;
|
||||
pub mod mirror;
|
||||
pub mod offset;
|
||||
pub mod rotate;
|
||||
pub mod scale;
|
||||
pub mod stretch;
|
||||
pub mod translate;
|
||||
pub mod trim;
|
||||
pub mod ungroup;
|
||||
@@ -0,0 +1,573 @@
|
||||
// Offset tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: OFFSET (O)
|
||||
// OFFSET: Creates a parallel copy of an object (line, arc, circle,
|
||||
// or lwpolyline) at a specified distance on the chosen side.
|
||||
//
|
||||
// Steps:
|
||||
// 1. Text input: "Specify offset distance <last>:" → enter float or Enter for default
|
||||
// 2. Pick object to offset (Line, Arc, Circle, LwPolyline)
|
||||
// 3. Pick a point on the side to offset toward
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use acadrust::entities::LwVertex;
|
||||
use acadrust::entities::{Arc as ArcEnt, Circle as CircleEnt, Line as LineEnt, LwPolyline};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::home::defaults;
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── Ribbon definition ──────────────────────────────────────────────────────
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "OFFSET",
|
||||
label: "Offset",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/offset.svg")),
|
||||
event: ModuleEvent::Command("OFFSET".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Geometry helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/// Infinite-line intersection in 2D. Returns the point or None if parallel.
|
||||
fn isect_lines(p0: [f64; 2], p1: [f64; 2], q0: [f64; 2], q1: [f64; 2]) -> Option<[f64; 2]> {
|
||||
let dx = p1[0] - p0[0];
|
||||
let dy = p1[1] - p0[1];
|
||||
let ex = q1[0] - q0[0];
|
||||
let ey = q1[1] - q0[1];
|
||||
let det = dx * ey - dy * ex;
|
||||
if det.abs() < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let t = ((q0[0] - p0[0]) * ey - (q0[1] - p0[1]) * ex) / det;
|
||||
Some([p0[0] + t * dx, p0[1] + t * dy])
|
||||
}
|
||||
|
||||
fn norm_rad(a: f64) -> f64 {
|
||||
((a % TAU) + TAU) % TAU
|
||||
}
|
||||
|
||||
// ── Line offset ────────────────────────────────────────────────────────────
|
||||
|
||||
fn offset_line(l: &LineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
let dx = l.end.x - l.start.x;
|
||||
let dy = l.end.y - l.start.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let nx = -dy / len; // left-perpendicular
|
||||
let ny = dx / len;
|
||||
|
||||
let vx = side_pt.x as f64 - l.start.x;
|
||||
let vy = side_pt.y as f64 - l.start.y;
|
||||
let cross = dx * vy - dy * vx;
|
||||
let sign = if cross >= 0.0 { 1.0 } else { -1.0 };
|
||||
|
||||
let ox = sign * nx * dist;
|
||||
let oy = sign * ny * dist;
|
||||
|
||||
let mut new_l = l.clone();
|
||||
new_l.common.handle = Handle::NULL;
|
||||
new_l.start.x += ox;
|
||||
new_l.start.y += oy;
|
||||
new_l.end.x += ox;
|
||||
new_l.end.y += oy;
|
||||
Some(EntityType::Line(new_l))
|
||||
}
|
||||
|
||||
// ── Circle offset ──────────────────────────────────────────────────────────
|
||||
|
||||
fn offset_circle(c: &CircleEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
let px = side_pt.x as f64;
|
||||
let py = side_pt.y as f64;
|
||||
let dc = ((px - c.center.x).powi(2) + (py - c.center.y).powi(2)).sqrt();
|
||||
|
||||
let new_r = if dc < c.radius {
|
||||
c.radius - dist
|
||||
} else {
|
||||
c.radius + dist
|
||||
};
|
||||
if new_r <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut new_c = c.clone();
|
||||
new_c.common.handle = Handle::NULL;
|
||||
new_c.radius = new_r;
|
||||
Some(EntityType::Circle(new_c))
|
||||
}
|
||||
|
||||
// ── Arc offset ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn offset_arc(a: &ArcEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
let px = side_pt.x as f64;
|
||||
let py = side_pt.y as f64;
|
||||
let dc = ((px - a.center.x).powi(2) + (py - a.center.y).powi(2)).sqrt();
|
||||
|
||||
let new_r = if dc < a.radius {
|
||||
a.radius - dist
|
||||
} else {
|
||||
a.radius + dist
|
||||
};
|
||||
if new_r <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut new_a = a.clone();
|
||||
new_a.common.handle = Handle::NULL;
|
||||
new_a.radius = new_r;
|
||||
Some(EntityType::Arc(new_a))
|
||||
}
|
||||
|
||||
// ── LwPolyline offset ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Algorithm:
|
||||
// 1. Offset every segment by `dist` in the direction perpendicular to it
|
||||
// (sign is determined once from the first non-degenerate segment + side_pt).
|
||||
// 2. Reconnect adjacent offset segments:
|
||||
// - Open: first / last vertex use the raw offset endpoints;
|
||||
// interior vertices are the intersection of adjacent offset segments.
|
||||
// - Closed: every vertex is the intersection of the previous and next
|
||||
// offset segments.
|
||||
// 3. Bulge values are preserved from the original vertices (arc segments
|
||||
// keep the same angle; the radius changes implicitly via the new chord
|
||||
// length — a minor approximation acceptable for modest offsets).
|
||||
|
||||
fn offset_lwpolyline(p: &LwPolyline, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let n_segs = if p.is_closed { n } else { n - 1 };
|
||||
|
||||
// Determine offset sign from the first non-degenerate segment.
|
||||
let sign: f64 = (0..n_segs).find_map(|i| {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let vx = side_pt.x as f64 - v0.location.x;
|
||||
let vy = side_pt.y as f64 - v0.location.y;
|
||||
let cross = dx * vy - dy * vx;
|
||||
Some(if cross >= 0.0 { 1.0 } else { -1.0 })
|
||||
})?;
|
||||
|
||||
// Offset each segment. A segment may be degenerate (zero length) → None.
|
||||
struct OffSeg {
|
||||
p0: [f64; 2],
|
||||
p1: [f64; 2],
|
||||
}
|
||||
|
||||
let segs: Vec<Option<OffSeg>> = (0..n_segs)
|
||||
.map(|i| {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let ox = sign * (-dy / len) * dist;
|
||||
let oy = sign * (dx / len) * dist;
|
||||
Some(OffSeg {
|
||||
p0: [v0.location.x + ox, v0.location.y + oy],
|
||||
p1: [v1.location.x + ox, v1.location.y + oy],
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let m = segs.len();
|
||||
|
||||
// Helper: corner vertex from the intersection of two consecutive offset segments.
|
||||
let corner = |prev: &OffSeg, curr: &OffSeg| -> [f64; 2] {
|
||||
isect_lines(prev.p0, prev.p1, curr.p0, curr.p1).unwrap_or([
|
||||
(prev.p1[0] + curr.p0[0]) * 0.5,
|
||||
(prev.p1[1] + curr.p0[1]) * 0.5,
|
||||
])
|
||||
};
|
||||
|
||||
let mut new_verts: Vec<LwVertex> = Vec::new();
|
||||
|
||||
if p.is_closed {
|
||||
for i in 0..m {
|
||||
let prev_idx = (i + m - 1) % m;
|
||||
let prev = match &segs[prev_idx] {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
let curr = match &segs[i] {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
let pt = corner(prev, curr);
|
||||
let mut v = LwVertex::from_coords(pt[0], pt[1]);
|
||||
v.bulge = p.vertices[i].bulge;
|
||||
new_verts.push(v);
|
||||
}
|
||||
} else {
|
||||
// First vertex
|
||||
if let Some(s) = &segs[0] {
|
||||
let mut v = LwVertex::from_coords(s.p0[0], s.p0[1]);
|
||||
v.bulge = p.vertices[0].bulge;
|
||||
new_verts.push(v);
|
||||
}
|
||||
// Interior vertices
|
||||
for i in 1..m {
|
||||
let prev = match &segs[i - 1] {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
let curr = match &segs[i] {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
let pt = corner(prev, curr);
|
||||
let mut v = LwVertex::from_coords(pt[0], pt[1]);
|
||||
v.bulge = p.vertices[i].bulge;
|
||||
new_verts.push(v);
|
||||
}
|
||||
// Last vertex
|
||||
if let Some(s) = &segs[m - 1] {
|
||||
new_verts.push(LwVertex::from_coords(s.p1[0], s.p1[1]));
|
||||
}
|
||||
}
|
||||
|
||||
if new_verts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut new_p = p.clone();
|
||||
new_p.common.handle = Handle::NULL;
|
||||
new_p.vertices = new_verts;
|
||||
Some(EntityType::LwPolyline(new_p))
|
||||
}
|
||||
|
||||
// ── Dispatch ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn compute_offset(entity: &EntityType, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
match entity {
|
||||
EntityType::Line(l) => offset_line(l, dist, side_pt),
|
||||
EntityType::Circle(c) => offset_circle(c, dist, side_pt),
|
||||
EntityType::Arc(a) => offset_arc(a, dist, side_pt),
|
||||
EntityType::LwPolyline(p) => offset_lwpolyline(p, dist, side_pt),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wire preview points ─────────────────────────────────────────────────────
|
||||
|
||||
fn entity_wire_pts(e: &EntityType) -> Vec<[f32; 3]> {
|
||||
match e {
|
||||
EntityType::Line(l) => vec![
|
||||
[l.start.x as f32, l.start.y as f32, l.start.z as f32],
|
||||
[l.end.x as f32, l.end.y as f32, l.end.z as f32],
|
||||
],
|
||||
EntityType::Circle(c) => {
|
||||
let steps = 64usize;
|
||||
(0..=steps)
|
||||
.map(|i| {
|
||||
let a = TAU * i as f64 / steps as f64;
|
||||
[
|
||||
(c.center.x + c.radius * a.cos()) as f32,
|
||||
(c.center.y + c.radius * a.sin()) as f32,
|
||||
c.center.z as f32,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
let a0 = norm_rad(a.start_angle.to_radians());
|
||||
let a1 = norm_rad(a.end_angle.to_radians());
|
||||
let span = {
|
||||
let s = a1 - a0;
|
||||
if s <= 0.0 {
|
||||
s + TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let steps = ((span.abs() * 20.0).ceil() as usize).max(4);
|
||||
(0..=steps)
|
||||
.map(|i| {
|
||||
let ang = a0 + span * (i as f64 / steps as f64);
|
||||
[
|
||||
(a.center.x + a.radius * ang.cos()) as f32,
|
||||
(a.center.y + a.radius * ang.sin()) as f32,
|
||||
a.center.z as f32,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityType::LwPolyline(p) => lwpolyline_pts(p),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Tessellate a LwPolyline into wire points (straight segments + arc bulges).
|
||||
fn lwpolyline_pts(p: &LwPolyline) -> Vec<[f32; 3]> {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 {
|
||||
return vec![];
|
||||
}
|
||||
let z = p.elevation as f32;
|
||||
let n_segs = if p.is_closed { n } else { n - 1 };
|
||||
let mut pts: Vec<[f32; 3]> = Vec::new();
|
||||
|
||||
for i in 0..n_segs {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let x0 = v0.location.x;
|
||||
let y0 = v0.location.y;
|
||||
let x1 = v1.location.x;
|
||||
let y1 = v1.location.y;
|
||||
|
||||
if pts.is_empty() {
|
||||
pts.push([x0 as f32, y0 as f32, z]);
|
||||
}
|
||||
|
||||
if v0.bulge.abs() < 1e-10 {
|
||||
pts.push([x1 as f32, y1 as f32, z]);
|
||||
} else {
|
||||
// Arc from bulge
|
||||
let b = v0.bulge;
|
||||
let chord_x = x1 - x0;
|
||||
let chord_y = y1 - y0;
|
||||
let chord_len = (chord_x * chord_x + chord_y * chord_y).sqrt();
|
||||
if chord_len < 1e-12 {
|
||||
pts.push([x1 as f32, y1 as f32, z]);
|
||||
continue;
|
||||
}
|
||||
|
||||
let b2 = b * b;
|
||||
let r = chord_len * (1.0 + b2) / (4.0 * b.abs());
|
||||
let d = r * (1.0 - b2) / (1.0 + b2);
|
||||
let mx = (x0 + x1) * 0.5;
|
||||
let my = (y0 + y1) * 0.5;
|
||||
let perp_x = -chord_y / chord_len;
|
||||
let perp_y = chord_x / chord_len;
|
||||
let sign = b.signum();
|
||||
let cx = mx + sign * d * perp_x;
|
||||
let cy = my + sign * d * perp_y;
|
||||
|
||||
let a0 = norm_rad((y0 - cy).atan2(x0 - cx));
|
||||
let a1 = norm_rad((y1 - cy).atan2(x1 - cx));
|
||||
let span = if b > 0.0 {
|
||||
let s = a1 - a0;
|
||||
if s <= 0.0 {
|
||||
s + TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
} else {
|
||||
let s = a0 - a1;
|
||||
if s <= 0.0 {
|
||||
s + TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let steps = ((span.abs() * 20.0).ceil() as usize).max(4);
|
||||
for j in 1..=steps {
|
||||
let t = j as f64 / steps as f64;
|
||||
let ang = if b > 0.0 {
|
||||
a0 + span * t
|
||||
} else {
|
||||
a0 - span * t
|
||||
};
|
||||
pts.push([(cx + r * ang.cos()) as f32, (cy + r * ang.sin()) as f32, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.is_closed {
|
||||
if let Some(&first) = pts.first() {
|
||||
pts.push(first);
|
||||
}
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
// ── Command implementation ─────────────────────────────────────────────────
|
||||
|
||||
enum Step {
|
||||
Distance,
|
||||
SelectObject {
|
||||
dist: f64,
|
||||
},
|
||||
PickSide {
|
||||
dist: f64,
|
||||
#[allow(dead_code)]
|
||||
handle: Handle,
|
||||
entity: EntityType,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct OffsetCommand {
|
||||
default_dist: f64,
|
||||
step: Step,
|
||||
all_entities: Vec<EntityType>,
|
||||
}
|
||||
|
||||
impl OffsetCommand {
|
||||
pub fn new(last_dist: f32, all_entities: Vec<EntityType>) -> Self {
|
||||
Self {
|
||||
default_dist: last_dist as f64,
|
||||
step: Step::Distance,
|
||||
all_entities,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for OffsetCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"OFFSET"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Distance => format!(
|
||||
"OFFSET Specify offset distance <{:.4}>:",
|
||||
self.default_dist
|
||||
),
|
||||
Step::SelectObject { dist } => {
|
||||
format!("OFFSET Select object to offset [dist={dist:.4}]:")
|
||||
}
|
||||
Step::PickSide { dist, .. } => {
|
||||
format!("OFFSET Specify side to offset [dist={dist:.4}]:")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, Step::Distance)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if let Step::Distance = self.step {
|
||||
let dist = if text.trim().is_empty() {
|
||||
self.default_dist
|
||||
} else {
|
||||
text.trim()
|
||||
.replace(',', ".")
|
||||
.parse::<f64>()
|
||||
.unwrap_or(self.default_dist)
|
||||
.abs()
|
||||
.max(1e-9)
|
||||
};
|
||||
defaults::set_offset_dist(dist as f32);
|
||||
self.default_dist = dist;
|
||||
self.step = Step::SelectObject { dist };
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::SelectObject { .. })
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
let dist = match &self.step {
|
||||
Step::SelectObject { dist } => *dist,
|
||||
_ => return CmdResult::NeedPoint,
|
||||
};
|
||||
|
||||
let entity = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
.cloned();
|
||||
|
||||
match entity {
|
||||
Some(e @ EntityType::Line(_))
|
||||
| Some(e @ EntityType::Circle(_))
|
||||
| Some(e @ EntityType::Arc(_))
|
||||
| Some(e @ EntityType::LwPolyline(_)) => {
|
||||
self.step = Step::PickSide {
|
||||
dist,
|
||||
handle,
|
||||
entity: e,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, _pt: Vec3) -> Vec<WireModel> {
|
||||
if handle.is_null() {
|
||||
return vec![];
|
||||
}
|
||||
if let Some(entity) = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle)
|
||||
{
|
||||
let pts = entity_wire_pts(entity);
|
||||
if !pts.is_empty() {
|
||||
return vec![WireModel::solid(
|
||||
"offset_hover".into(),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)];
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
let (dist, entity) = match &self.step {
|
||||
Step::PickSide { dist, entity, .. } => (*dist, entity.clone()),
|
||||
_ => return CmdResult::NeedPoint,
|
||||
};
|
||||
|
||||
match compute_offset(&entity, dist, pt) {
|
||||
Some(new_entity) => CmdResult::CommitAndExit(new_entity),
|
||||
None => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
||||
let (dist, entity) = match &self.step {
|
||||
Step::PickSide { dist, entity, .. } => (*dist, entity.clone()),
|
||||
_ => return vec![],
|
||||
};
|
||||
|
||||
if let Some(result) = compute_offset(&entity, dist, pt) {
|
||||
let pts = entity_wire_pts(&result);
|
||||
if !pts.is_empty() {
|
||||
return vec![WireModel::solid(
|
||||
"offset_preview".into(),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)];
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Rotate tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: ROTATE (RO)
|
||||
// Requires at least one entity selected before starting.
|
||||
// Step 1: pick rotation center
|
||||
// Step 2: pick reference point (defines the 0° direction)
|
||||
// Step 3: pick destination point → rotates by (dest_angle - ref_angle)
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::modules::home::defaults;
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── Ribbon definition ──────────────────────────────────────────────────────
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "ROTATE",
|
||||
label: "Rotate",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/rotate.svg")),
|
||||
event: ModuleEvent::Command("ROTATE".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command implementation ─────────────────────────────────────────────────
|
||||
|
||||
enum Step {
|
||||
Center,
|
||||
RefPoint { center: Vec3 },
|
||||
Angle { center: Vec3, ref_angle: f32 },
|
||||
}
|
||||
|
||||
pub struct RotateCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
step: Step,
|
||||
default_angle: f32, // degrees
|
||||
}
|
||||
|
||||
impl RotateCommand {
|
||||
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
step: Step::Center,
|
||||
default_angle: defaults::get_rotate_angle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for RotateCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"ROTATE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Center => format!(
|
||||
"ROTATE Specify rotation center [{} objects]:",
|
||||
self.handles.len()
|
||||
),
|
||||
Step::RefPoint { .. } => {
|
||||
"ROTATE Specify reference point (or skip: type angle directly):".into()
|
||||
}
|
||||
Step::Angle { ref_angle, .. } => format!(
|
||||
"ROTATE Specify destination or type angle in degrees <{:.4}> [ref={:.1}°]:",
|
||||
self.default_angle,
|
||||
ref_angle.to_degrees()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Center => {
|
||||
self.step = Step::RefPoint { center: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::RefPoint { center } => {
|
||||
let center = *center;
|
||||
let ref_angle = (pt.y - center.y).atan2(pt.x - center.x);
|
||||
self.step = Step::Angle { center, ref_angle };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Angle { center, ref_angle } => {
|
||||
let center = *center;
|
||||
let ref_angle = *ref_angle;
|
||||
let dest_angle = (pt.y - center.y).atan2(pt.x - center.x);
|
||||
let delta = dest_angle - ref_angle;
|
||||
defaults::set_rotate_angle(delta.to_degrees());
|
||||
self.default_angle = delta.to_degrees();
|
||||
CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Rotate {
|
||||
center,
|
||||
angle_rad: delta,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// At Angle step: Enter uses the stored default angle.
|
||||
if let Step::Angle { center, .. } = &self.step {
|
||||
let center = *center;
|
||||
let angle_rad = self.default_angle.to_radians();
|
||||
return CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Rotate { center, angle_rad },
|
||||
);
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if let Step::Angle { center, .. } = &self.step {
|
||||
let deg: f32 = text.trim().replace(',', ".").parse().ok()?;
|
||||
let center = *center;
|
||||
defaults::set_rotate_angle(deg);
|
||||
return Some(CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Rotate {
|
||||
center,
|
||||
angle_rad: deg.to_radians(),
|
||||
},
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
||||
let (center, ref_angle) = match &self.step {
|
||||
Step::Angle { center, ref_angle } => (*center, *ref_angle),
|
||||
Step::RefPoint { center } => {
|
||||
// Show a reference line from center to cursor only.
|
||||
return vec![WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
vec![[center.x, center.y, center.z], [pt.x, pt.y, pt.z]],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)];
|
||||
}
|
||||
_ => return vec![],
|
||||
};
|
||||
let dest_angle = (pt.y - center.y).atan2(pt.x - center.x);
|
||||
let angle_rad = dest_angle - ref_angle;
|
||||
// Object ghosts rotated to new angle.
|
||||
let mut out: Vec<WireModel> = self
|
||||
.wire_models
|
||||
.iter()
|
||||
.map(|w| w.rotated(center, angle_rad))
|
||||
.collect();
|
||||
// Arc rubber-band showing the rotation sweep.
|
||||
let r = center.distance(pt).max(0.3);
|
||||
let mut end = dest_angle;
|
||||
if end < ref_angle {
|
||||
end += std::f32::consts::TAU;
|
||||
}
|
||||
let span = end - ref_angle;
|
||||
let segs = ((span.abs() / std::f32::consts::TAU) * 48.0)
|
||||
.ceil()
|
||||
.max(4.0) as u32;
|
||||
let mut arc_pts: Vec<[f32; 3]> = (0..=segs)
|
||||
.map(|i| {
|
||||
let a = ref_angle + span * (i as f32 / segs as f32);
|
||||
[center.x + r * a.cos(), center.y + r * a.sin(), center.z]
|
||||
})
|
||||
.collect();
|
||||
arc_pts.push([center.x, center.y, center.z]);
|
||||
arc_pts.push([
|
||||
center.x + r * ref_angle.cos(),
|
||||
center.y + r * ref_angle.sin(),
|
||||
center.z,
|
||||
]);
|
||||
out.push(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
arc_pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Scale tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: SCALE (SC)
|
||||
// Requires at least one entity selected.
|
||||
// Step 1: pick base (scale center)
|
||||
// Step 2: pick reference point (defines reference distance)
|
||||
// Step 3: pick new point (new distance = scale factor * objects)
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::modules::home::defaults;
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "SCALE",
|
||||
label: "Scale",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/scale.svg")),
|
||||
event: ModuleEvent::Command("SCALE".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
enum Step {
|
||||
Base,
|
||||
Ref { base: Vec3 },
|
||||
New { base: Vec3, ref_dist: f32 },
|
||||
}
|
||||
|
||||
pub struct ScaleCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
step: Step,
|
||||
default_factor: f32,
|
||||
}
|
||||
|
||||
impl ScaleCommand {
|
||||
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
step: Step::Base,
|
||||
default_factor: defaults::get_scale_factor(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ScaleCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"SCALE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Base => format!(
|
||||
"SCALE Specify base point [{} objects]:",
|
||||
self.handles.len()
|
||||
),
|
||||
Step::Ref { .. } => {
|
||||
"SCALE Specify reference point (or type scale factor directly):".into()
|
||||
}
|
||||
Step::New { ref_dist, .. } => format!(
|
||||
"SCALE Specify new point or type scale factor <{:.4}> [ref={:.3}]:",
|
||||
self.default_factor, ref_dist
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Base => {
|
||||
self.step = Step::Ref { base: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Ref { base } => {
|
||||
let base = *base;
|
||||
let ref_dist = base.distance(pt).max(1e-6);
|
||||
self.step = Step::New { base, ref_dist };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::New { base, ref_dist } => {
|
||||
let base = *base;
|
||||
let new_dist = base.distance(pt).max(1e-6);
|
||||
let factor = new_dist / *ref_dist;
|
||||
defaults::set_scale_factor(factor);
|
||||
CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Scale {
|
||||
center: base,
|
||||
factor,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// At New-point step: Enter uses the stored default factor applied from base.
|
||||
if let Step::New { base, .. } = &self.step {
|
||||
let base = *base;
|
||||
let factor = self.default_factor;
|
||||
return CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Scale {
|
||||
center: base,
|
||||
factor,
|
||||
},
|
||||
);
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if let Step::New { base, .. } | Step::Ref { base } = &self.step {
|
||||
let factor: f32 = text.trim().replace(',', ".").parse().ok()?;
|
||||
if factor > 0.0 {
|
||||
let base = *base;
|
||||
defaults::set_scale_factor(factor);
|
||||
return Some(CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Scale {
|
||||
center: base,
|
||||
factor,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
||||
let (base, factor) = match &self.step {
|
||||
Step::Ref { base } => {
|
||||
// Reference pick: just show rubber-band line, no scale yet.
|
||||
return vec![WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)];
|
||||
}
|
||||
Step::New { base, ref_dist } => {
|
||||
let f = base.distance(pt).max(1e-6) / ref_dist;
|
||||
(*base, f)
|
||||
}
|
||||
_ => return vec![],
|
||||
};
|
||||
let mut out: Vec<WireModel> = self
|
||||
.wire_models
|
||||
.iter()
|
||||
.map(|w| w.scaled(base, factor))
|
||||
.collect();
|
||||
out.push(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Stretch tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: STRETCH (SS)
|
||||
// STRETCH: Moves endpoints/vertices that lie within a crossing
|
||||
// window while leaving the rest of the object fixed.
|
||||
// Simplified implementation: works like MOVE on the full selected set
|
||||
// because we don't yet have a crossing-window selector in the viewport.
|
||||
// Step 1: pick base point
|
||||
// Step 2: pick new point → translates all selected entities by (new - base)
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── Ribbon definition ──────────────────────────────────────────────────────
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "STRETCH",
|
||||
label: "Stretch",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/stretch.svg")),
|
||||
event: ModuleEvent::Command("STRETCH".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command implementation ─────────────────────────────────────────────────
|
||||
|
||||
enum Step {
|
||||
Base,
|
||||
Target(Vec3),
|
||||
}
|
||||
|
||||
pub struct StretchCommand {
|
||||
handles: Vec<Handle>,
|
||||
step: Step,
|
||||
}
|
||||
|
||||
impl StretchCommand {
|
||||
pub fn new(handles: Vec<Handle>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
step: Step::Base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for StretchCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"STRETCH"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Base => format!(
|
||||
"STRETCH Specify base point [{} objects]:",
|
||||
self.handles.len()
|
||||
),
|
||||
Step::Target(base) => format!(
|
||||
"STRETCH Specify new point [base {:.3},{:.3}]:",
|
||||
base.x, base.y
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Base => {
|
||||
self.step = Step::Target(pt);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Target(base) => {
|
||||
let delta = pt - *base;
|
||||
CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Translate(delta),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
if let Step::Target(base) = &self.step {
|
||||
Some(WireModel {
|
||||
name: "rubber_band".into(),
|
||||
points: vec![[base.x, base.y, base.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![],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Move tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: MOVE (M)
|
||||
// Requires at least one entity selected before starting.
|
||||
// Step 1: pick base point
|
||||
// Step 2: pick destination → translates all selected entities by (dest - base)
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── Ribbon definition ──────────────────────────────────────────────────────
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "MOVE",
|
||||
label: "Move",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/move.svg")),
|
||||
event: ModuleEvent::Command("MOVE".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command implementation ─────────────────────────────────────────────────
|
||||
|
||||
enum Step {
|
||||
Base,
|
||||
Target(Vec3),
|
||||
}
|
||||
|
||||
pub struct MoveCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
step: Step,
|
||||
}
|
||||
|
||||
impl MoveCommand {
|
||||
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
step: Step::Base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MoveCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"MOVE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Base => format!(
|
||||
"MOVE Specify base point [{} objects]:",
|
||||
self.handles.len()
|
||||
),
|
||||
Step::Target(base) => format!(
|
||||
"MOVE Specify destination [base {:.3},{:.3}]:",
|
||||
base.x, base.y
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Base => {
|
||||
self.step = Step::Target(pt);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Target(base) => {
|
||||
let delta = pt - *base;
|
||||
CmdResult::TransformSelected(
|
||||
self.handles.clone(),
|
||||
EntityTransform::Translate(delta),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
|
||||
let Step::Target(base) = &self.step else {
|
||||
return vec![];
|
||||
};
|
||||
let delta = pt - *base;
|
||||
// Translated ghost of each selected object + rubber-band line.
|
||||
let mut out: Vec<WireModel> = self
|
||||
.wire_models
|
||||
.iter()
|
||||
.map(|w| w.translated(delta))
|
||||
.collect();
|
||||
out.push(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
// Trim / Extend — ribbon definitions + full command implementations.
|
||||
//
|
||||
// TRIM (TR): Click the segment you want to remove. The command finds all
|
||||
// intersections of that entity with every other entity and trims
|
||||
// out the clicked interval. Stays active — click more segments,
|
||||
// press Enter to finish.
|
||||
//
|
||||
// EXTEND (EX): Click near one end of an entity. The command extends that
|
||||
// endpoint to the nearest intersecting boundary. Stays active.
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use acadrust::entities::{Arc as ArcEnt, Line as LineEnt};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::IconKind;
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── Dropdown constants ─────────────────────────────────────────────────────
|
||||
|
||||
pub const DROPDOWN_ID: &str = "trim_extend";
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../../assets/icons/trim.svg"));
|
||||
|
||||
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
|
||||
(
|
||||
"TRIM",
|
||||
"Trim",
|
||||
IconKind::Svg(include_bytes!("../../../../assets/icons/trim.svg")),
|
||||
),
|
||||
(
|
||||
"EXTEND",
|
||||
"Extend",
|
||||
IconKind::Svg(include_bytes!("../../../../assets/icons/extend.svg")),
|
||||
),
|
||||
];
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// Geometry helpers
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Normalize angle to [0, 2π).
|
||||
fn norm(a: f64) -> f64 {
|
||||
((a % TAU) + TAU) % TAU
|
||||
}
|
||||
|
||||
/// Is angle `a` within the arc from `s` to `e` (CCW, radians)?
|
||||
fn in_arc(a: f64, s: f64, e: f64) -> bool {
|
||||
let (a, s, e) = (norm(a), norm(s), norm(e));
|
||||
if (e - s).abs() < 1e-9 || (e - s - TAU).abs() < 1e-9 {
|
||||
return true;
|
||||
}
|
||||
if s <= e {
|
||||
a >= s - 1e-9 && a <= e + 1e-9
|
||||
} else {
|
||||
a >= s - 1e-9 || a <= e + 1e-9
|
||||
}
|
||||
}
|
||||
|
||||
/// Parametric t ∈ [0,1] on arc (a0→a1 CCW) for angle `a`.
|
||||
fn arc_t(a: f64, a0: f64, a1: f64) -> f64 {
|
||||
let span = {
|
||||
let s = norm(a1) - norm(a0);
|
||||
if s <= 0.0 {
|
||||
s + TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let da = {
|
||||
let d = norm(a) - norm(a0);
|
||||
if d < 0.0 {
|
||||
d + TAU
|
||||
} else {
|
||||
d
|
||||
}
|
||||
};
|
||||
(da / span).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// Intersect infinite lines (p+t·d) and (q+u·e). Returns (t, u).
|
||||
fn ll(
|
||||
px: f64,
|
||||
py: f64,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
qx: f64,
|
||||
qy: f64,
|
||||
ex: f64,
|
||||
ey: f64,
|
||||
) -> Option<(f64, f64)> {
|
||||
let det = dx * ey - dy * ex;
|
||||
if det.abs() < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let t = ((qx - px) * ey - (qy - py) * ex) / det;
|
||||
let u = ((qx - px) * dy - (qy - py) * dx) / det;
|
||||
Some((t, u))
|
||||
}
|
||||
|
||||
/// Intersect infinite line (p+t·d) with circle (cx,cy,r). Returns t values.
|
||||
fn lc(px: f64, py: f64, dx: f64, dy: f64, cx: f64, cy: f64, r: f64) -> Vec<f64> {
|
||||
let fx = px - cx;
|
||||
let fy = py - cy;
|
||||
let a = dx * dx + dy * dy;
|
||||
let b = 2.0 * (fx * dx + fy * dy);
|
||||
let c = fx * fx + fy * fy - r * r;
|
||||
let disc = b * b - 4.0 * a * c;
|
||||
if disc < 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
let sq = disc.sqrt();
|
||||
if disc < 1e-14 {
|
||||
vec![(-b) / (2.0 * a)]
|
||||
} else {
|
||||
vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Circle-circle intersection: angles on circle 1 where they meet.
|
||||
fn cc_angles(cx1: f64, cy1: f64, r1: f64, cx2: f64, cy2: f64, r2: f64) -> Vec<f64> {
|
||||
let d = ((cx2 - cx1).powi(2) + (cy2 - cy1).powi(2)).sqrt();
|
||||
if d < 1e-9 || d > r1 + r2 + 1e-9 || d < (r1 - r2).abs() - 1e-9 {
|
||||
return vec![];
|
||||
}
|
||||
let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
|
||||
let h2 = r1 * r1 - a * a;
|
||||
if h2 < 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
let h = h2.sqrt();
|
||||
let mx = cx1 + a * (cx2 - cx1) / d;
|
||||
let my = cy1 + a * (cy2 - cy1) / d;
|
||||
let px = h * (cy2 - cy1) / d;
|
||||
let py = -h * (cx2 - cx1) / d;
|
||||
let a1 = ((my + py) - cy1).atan2((mx + px) - cx1);
|
||||
let a2 = ((my - py) - cy1).atan2((mx - px) - cx1);
|
||||
if h < 1e-9 {
|
||||
vec![a1]
|
||||
} else {
|
||||
vec![a1, a2]
|
||||
}
|
||||
}
|
||||
|
||||
// ── Boundary geometry ─────────────────────────────────────────────────────
|
||||
|
||||
enum Geo {
|
||||
Line {
|
||||
handle: Handle,
|
||||
p1: [f64; 2],
|
||||
p2: [f64; 2],
|
||||
},
|
||||
Arc {
|
||||
handle: Handle,
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
r: f64,
|
||||
a0: f64,
|
||||
a1: f64,
|
||||
},
|
||||
Circle {
|
||||
handle: Handle,
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
r: f64,
|
||||
},
|
||||
}
|
||||
|
||||
fn build_geos(entities: &[EntityType]) -> Vec<Geo> {
|
||||
entities
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
let h = e.common().handle;
|
||||
match e {
|
||||
EntityType::Line(l) => Some(Geo::Line {
|
||||
handle: h,
|
||||
p1: [l.start.x, l.start.y],
|
||||
p2: [l.end.x, l.end.y],
|
||||
}),
|
||||
EntityType::Arc(a) => Some(Geo::Arc {
|
||||
handle: h,
|
||||
cx: a.center.x,
|
||||
cy: a.center.y,
|
||||
r: a.radius,
|
||||
a0: a.start_angle.to_radians(),
|
||||
a1: a.end_angle.to_radians(),
|
||||
}),
|
||||
EntityType::Circle(c) => Some(Geo::Circle {
|
||||
handle: h,
|
||||
cx: c.center.x,
|
||||
cy: c.center.y,
|
||||
r: c.radius,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Intersection helpers ──────────────────────────────────────────────────
|
||||
|
||||
/// Sorted, deduped t-params ∈ [0,1] where LINE segment (ax,ay)→(bx,by) intersects boundaries.
|
||||
fn line_seg_ts(ax: f64, ay: f64, bx: f64, by: f64, target: Handle, geos: &[Geo]) -> Vec<f64> {
|
||||
let (dx, dy) = (bx - ax, by - ay);
|
||||
let mut ts = vec![];
|
||||
for geo in geos {
|
||||
match geo {
|
||||
Geo::Line { handle, p1, p2 } => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
let (ex, ey) = (p2[0] - p1[0], p2[1] - p1[1]);
|
||||
if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) {
|
||||
if (-1e-9..=1.0 + 1e-9).contains(&u) && (-1e-9..=1.0 + 1e-9).contains(&t) {
|
||||
ts.push(t.clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
Geo::Arc {
|
||||
handle,
|
||||
cx,
|
||||
cy,
|
||||
r,
|
||||
a0,
|
||||
a1,
|
||||
} => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
|
||||
if !(-1e-9..=1.0 + 1e-9).contains(&t) {
|
||||
continue;
|
||||
}
|
||||
let ix = ax + t * dx;
|
||||
let iy = ay + t * dy;
|
||||
if in_arc((iy - cy).atan2(ix - cx), *a0, *a1) {
|
||||
ts.push(t.clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
Geo::Circle { handle, cx, cy, r } => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
|
||||
if (-1e-9..=1.0 + 1e-9).contains(&t) {
|
||||
ts.push(t.clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
ts.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
||||
ts
|
||||
}
|
||||
|
||||
/// Sorted, deduped t-params ∈ [0,1] where ARC (cx,cy,r,a0→a1) intersects boundaries.
|
||||
fn arc_seg_ts(
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
r: f64,
|
||||
a0: f64,
|
||||
a1: f64,
|
||||
target: Handle,
|
||||
geos: &[Geo],
|
||||
) -> Vec<f64> {
|
||||
let mut ts = vec![];
|
||||
for geo in geos {
|
||||
let angles: Vec<f64> = match geo {
|
||||
Geo::Line { handle, p1, p2 } => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
let (dx, dy) = (p2[0] - p1[0], p2[1] - p1[1]);
|
||||
lc(p1[0], p1[1], dx, dy, cx, cy, r)
|
||||
.into_iter()
|
||||
.filter(|&u| (-1e-9..=1.0 + 1e-9).contains(&u))
|
||||
.map(|u| (p1[1] + u * dy - cy).atan2(p1[0] + u * dx - cx))
|
||||
.collect()
|
||||
}
|
||||
Geo::Arc {
|
||||
handle,
|
||||
cx: cx2,
|
||||
cy: cy2,
|
||||
r: r2,
|
||||
a0: a02,
|
||||
a1: a12,
|
||||
} => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
cc_angles(cx, cy, r, *cx2, *cy2, *r2)
|
||||
.into_iter()
|
||||
.filter(|&a| in_arc(a, *a02, *a12))
|
||||
.collect()
|
||||
}
|
||||
Geo::Circle {
|
||||
handle,
|
||||
cx: cx2,
|
||||
cy: cy2,
|
||||
r: r2,
|
||||
} => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
cc_angles(cx, cy, r, *cx2, *cy2, *r2)
|
||||
}
|
||||
};
|
||||
for a in angles {
|
||||
if in_arc(a, a0, a1) {
|
||||
ts.push(arc_t(a, a0, a1));
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
ts.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
||||
ts
|
||||
}
|
||||
|
||||
// ── Trim helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Remove the t-interval containing `t_click` from sorted ts. Returns surviving pieces.
|
||||
fn trim_intervals(ts: &[f64], t_click: f64) -> Vec<(f64, f64)> {
|
||||
let mut bounds = vec![0.0f64];
|
||||
bounds.extend_from_slice(ts);
|
||||
bounds.push(1.0);
|
||||
bounds.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
||||
|
||||
let remove = bounds
|
||||
.windows(2)
|
||||
.position(|w| t_click >= w[0] - 1e-6 && t_click <= w[1] + 1e-6);
|
||||
|
||||
bounds
|
||||
.windows(2)
|
||||
.enumerate()
|
||||
.filter(|(idx, _)| Some(*idx) != remove)
|
||||
.filter(|(_, w)| (w[1] - w[0]) > 1e-6)
|
||||
.map(|(_, w)| (w[0], w[1]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn lerp2(p1: [f64; 2], p2: [f64; 2], t: f64) -> [f64; 2] {
|
||||
[p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])]
|
||||
}
|
||||
|
||||
/// Trim a Line entity. Returns the surviving line segments.
|
||||
fn trim_line(orig: &LineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
|
||||
let p1 = [orig.start.x, orig.start.y];
|
||||
let p2 = [orig.end.x, orig.end.y];
|
||||
let z = orig.start.z;
|
||||
trim_intervals(ts, t_click)
|
||||
.into_iter()
|
||||
.filter_map(|(ta, tb)| {
|
||||
let a = lerp2(p1, p2, ta);
|
||||
let b = lerp2(p1, p2, tb);
|
||||
if (b[0] - a[0]).hypot(b[1] - a[1]) < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let mut l = orig.clone();
|
||||
l.common.handle = Handle::NULL;
|
||||
l.start = Vector3::new(a[0], a[1], z);
|
||||
l.end = Vector3::new(b[0], b[1], z);
|
||||
Some(EntityType::Line(l))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Trim an Arc entity. Returns the surviving arc segments.
|
||||
fn trim_arc(orig: &ArcEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
|
||||
let a0 = orig.start_angle.to_radians();
|
||||
let a1 = orig.end_angle.to_radians();
|
||||
let span = {
|
||||
let s = norm(a1) - norm(a0);
|
||||
if s <= 0.0 {
|
||||
s + TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let angle_at = |t: f64| norm(a0) + span * t;
|
||||
|
||||
trim_intervals(ts, t_click)
|
||||
.into_iter()
|
||||
.filter_map(|(ta, tb)| {
|
||||
if (tb - ta).abs() < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let mut a = orig.clone();
|
||||
a.common.handle = Handle::NULL;
|
||||
a.start_angle = angle_at(ta).to_degrees();
|
||||
a.end_angle = angle_at(tb).to_degrees();
|
||||
Some(EntityType::Arc(a))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Extend helper ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Extend a Line to the nearest boundary on the extended side.
|
||||
/// t_click < 0.5 → extend start (look for t < 0); t_click ≥ 0.5 → extend end (t > 1).
|
||||
fn extend_line(orig: &LineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityType> {
|
||||
let ax = orig.start.x;
|
||||
let ay = orig.start.y;
|
||||
let bx = orig.end.x;
|
||||
let by = orig.end.y;
|
||||
let (dx, dy) = (bx - ax, by - ay);
|
||||
let target = orig.common.handle;
|
||||
let extend_end = t_click >= 0.5;
|
||||
|
||||
let mut best_t = if extend_end {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
};
|
||||
|
||||
for geo in geos {
|
||||
match geo {
|
||||
Geo::Line { handle, p1, p2 } => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
let (ex, ey) = (p2[0] - p1[0], p2[1] - p1[1]);
|
||||
if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) {
|
||||
if !(-1e-9..=1.0 + 1e-9).contains(&u) {
|
||||
continue;
|
||||
}
|
||||
if extend_end && t > 1.0 + 1e-6 && t < best_t {
|
||||
best_t = t;
|
||||
}
|
||||
if !extend_end && t < -1e-6 && t > best_t {
|
||||
best_t = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
Geo::Arc {
|
||||
handle,
|
||||
cx,
|
||||
cy,
|
||||
r,
|
||||
a0,
|
||||
a1,
|
||||
} => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
|
||||
let ix = ax + t * dx;
|
||||
let iy = ay + t * dy;
|
||||
if !in_arc((iy - cy).atan2(ix - cx), *a0, *a1) {
|
||||
continue;
|
||||
}
|
||||
if extend_end && t > 1.0 + 1e-6 && t < best_t {
|
||||
best_t = t;
|
||||
}
|
||||
if !extend_end && t < -1e-6 && t > best_t {
|
||||
best_t = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
Geo::Circle { handle, cx, cy, r } => {
|
||||
if *handle == target {
|
||||
continue;
|
||||
}
|
||||
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
|
||||
if extend_end && t > 1.0 + 1e-6 && t < best_t {
|
||||
best_t = t;
|
||||
}
|
||||
if !extend_end && t < -1e-6 && t > best_t {
|
||||
best_t = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !best_t.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let mut line = orig.clone();
|
||||
line.common.handle = Handle::NULL;
|
||||
let new_x = ax + best_t * dx;
|
||||
let new_y = ay + best_t * dy;
|
||||
if extend_end {
|
||||
line.end = Vector3::new(new_x, new_y, orig.end.z);
|
||||
} else {
|
||||
line.start = Vector3::new(new_x, new_y, orig.start.z);
|
||||
}
|
||||
Some(EntityType::Line(line))
|
||||
}
|
||||
|
||||
// ── Point-generation helpers ──────────────────────────────────────────────
|
||||
|
||||
const DIM_RED: [f32; 4] = [1.0, 0.3, 0.3, 0.6];
|
||||
|
||||
fn line_pts(l: &LineEnt) -> Vec<[f32; 3]> {
|
||||
vec![
|
||||
[l.start.x as f32, l.start.y as f32, l.start.z as f32],
|
||||
[l.end.x as f32, l.end.y as f32, l.end.z as f32],
|
||||
]
|
||||
}
|
||||
|
||||
fn arc_pts(cx: f64, cy: f64, r: f64, a0: f64, a1: f64, y: f64) -> Vec<[f32; 3]> {
|
||||
let span = {
|
||||
let s = norm(a1) - norm(a0);
|
||||
if s <= 0.0 {
|
||||
s + TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let steps = (span.abs() * 20.0).ceil().max(4.0) as usize;
|
||||
(0..=steps)
|
||||
.map(|i| {
|
||||
let ang = norm(a0) + span * (i as f64 / steps as f64);
|
||||
[
|
||||
(cx + r * ang.cos()) as f32,
|
||||
y as f32,
|
||||
(cy + r * ang.sin()) as f32,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> {
|
||||
match e {
|
||||
EntityType::Line(l) => line_pts(l),
|
||||
EntityType::Arc(a) => arc_pts(
|
||||
a.center.x,
|
||||
a.center.y,
|
||||
a.radius,
|
||||
a.start_angle.to_radians(),
|
||||
a.end_angle.to_radians(),
|
||||
a.center.y,
|
||||
),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// TrimCommand
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
pub struct TrimCommand {
|
||||
all_entities: Vec<EntityType>,
|
||||
geos: Vec<Geo>,
|
||||
}
|
||||
|
||||
impl TrimCommand {
|
||||
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||
let geos = build_geos(&all_entities);
|
||||
Self { all_entities, geos }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for TrimCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"TRIM"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
"TRIM Click segment to remove [Enter=done]:".into()
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
let entity = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle);
|
||||
|
||||
let result: Option<Vec<EntityType>> = match entity {
|
||||
Some(EntityType::Line(l)) => {
|
||||
let ax = l.start.x;
|
||||
let ay = l.start.y;
|
||||
let bx = l.end.x;
|
||||
let by = l.end.y;
|
||||
let ts = line_seg_ts(ax, ay, bx, by, handle, &self.geos);
|
||||
if ts.is_empty() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let dx = bx - ax;
|
||||
let dy = by - ay;
|
||||
let len2 = dx * dx + dy * dy;
|
||||
let t_click = if len2 > 1e-12 {
|
||||
((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
Some(trim_line(l, &ts, t_click))
|
||||
}
|
||||
Some(EntityType::Arc(a)) => {
|
||||
let cx = a.center.x;
|
||||
let cy = a.center.y;
|
||||
let a0 = a.start_angle.to_radians();
|
||||
let a1 = a.end_angle.to_radians();
|
||||
let ts = arc_seg_ts(cx, cy, a.radius, a0, a1, handle, &self.geos);
|
||||
if ts.is_empty() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let click_angle = (pt.y as f64 - cy).atan2(pt.x as f64 - cx);
|
||||
let t_click = arc_t(click_angle, a0, a1);
|
||||
Some(trim_arc(a, &ts, t_click))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(new_entities) = result {
|
||||
// Snapshot is updated in on_entity_replaced once we know the real handles.
|
||||
// Pre-stage: remove old entry now so geos exclude it immediately.
|
||||
if let Some(pos) = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.position(|e| e.common().handle == handle)
|
||||
{
|
||||
self.all_entities.remove(pos);
|
||||
// Add pieces with NULL handles as geometry-only placeholders.
|
||||
self.all_entities.extend(new_entities.clone());
|
||||
self.geos = build_geos(&self.all_entities);
|
||||
}
|
||||
CmdResult::ReplaceEntity(handle, new_entities)
|
||||
} else {
|
||||
self.command_line_hint();
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
fn on_entity_replaced(&mut self, _old: Handle, new_handles: &[acadrust::Handle]) {
|
||||
// The last new_handles.len() entries in all_entities are the trimmed pieces
|
||||
// that were appended with NULL handles. Assign their real document handles.
|
||||
let start = self.all_entities.len().saturating_sub(new_handles.len());
|
||||
for (e, &h) in self.all_entities[start..]
|
||||
.iter_mut()
|
||||
.zip(new_handles.iter())
|
||||
{
|
||||
match e {
|
||||
EntityType::Line(l) => l.common.handle = h,
|
||||
EntityType::Arc(a) => a.common.handle = h,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.geos = build_geos(&self.all_entities);
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, pt: Vec3) -> Vec<WireModel> {
|
||||
if handle.is_null() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let entity = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle);
|
||||
|
||||
match entity {
|
||||
Some(EntityType::Line(l)) => {
|
||||
let ax = l.start.x;
|
||||
let ay = l.start.y;
|
||||
let bx = l.end.x;
|
||||
let by = l.end.y;
|
||||
let ts = line_seg_ts(ax, ay, bx, by, handle, &self.geos);
|
||||
if ts.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let dx = bx - ax;
|
||||
let dy = by - ay;
|
||||
let len2 = dx * dx + dy * dy;
|
||||
let t_click = if len2 > 1e-12 {
|
||||
((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
let survivors = trim_line(l, &ts, t_click);
|
||||
let p1 = [l.start.x as f32, l.start.y as f32, l.start.y as f32];
|
||||
let p2 = [l.end.x as f32, l.end.y as f32, l.end.y as f32];
|
||||
let removed = WireModel::solid("trim_rm".into(), vec![p1, p2], DIM_RED, false);
|
||||
let mut out = vec![removed];
|
||||
for (i, e) in survivors.iter().enumerate() {
|
||||
let pts = entity_pts(e);
|
||||
out.push(WireModel::solid(
|
||||
format!("trim_keep_{i}"),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
Some(EntityType::Arc(a)) => {
|
||||
let cx = a.center.x;
|
||||
let cy = a.center.y;
|
||||
let a0 = a.start_angle.to_radians();
|
||||
let a1 = a.end_angle.to_radians();
|
||||
let ts = arc_seg_ts(cx, cy, a.radius, a0, a1, handle, &self.geos);
|
||||
if ts.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let click_angle = (pt.y as f64 - cy).atan2(pt.x as f64 - cx);
|
||||
let t_click = arc_t(click_angle, a0, a1);
|
||||
let survivors = trim_arc(a, &ts, t_click);
|
||||
let orig_pts = arc_pts(cx, cy, a.radius, a0, a1, a.center.y);
|
||||
let removed = WireModel::solid("trim_rm".into(), orig_pts, DIM_RED, false);
|
||||
let mut out = vec![removed];
|
||||
for (i, e) in survivors.iter().enumerate() {
|
||||
let pts = entity_pts(e);
|
||||
out.push(WireModel::solid(
|
||||
format!("trim_keep_{i}"),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
impl TrimCommand {
|
||||
fn command_line_hint(&self) {}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ExtendCommand
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
pub struct ExtendCommand {
|
||||
all_entities: Vec<EntityType>,
|
||||
geos: Vec<Geo>,
|
||||
/// (old_handle, new_entity_with_updated_geometry) — set in on_entity_pick,
|
||||
/// consumed in on_entity_replaced to patch the snapshot with both new handle + geometry.
|
||||
pending_replace: Option<(Handle, EntityType)>,
|
||||
}
|
||||
|
||||
impl ExtendCommand {
|
||||
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||
let geos = build_geos(&all_entities);
|
||||
Self {
|
||||
all_entities,
|
||||
geos,
|
||||
pending_replace: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ExtendCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"EXTEND"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
"EXTEND Click near end of object to extend [Enter=done]:".into()
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
let entity = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle);
|
||||
|
||||
let result: Option<EntityType> = match entity {
|
||||
Some(EntityType::Line(l)) => {
|
||||
let ax = l.start.x;
|
||||
let ay = l.start.y;
|
||||
let bx = l.end.x;
|
||||
let by = l.end.y;
|
||||
let dx = bx - ax;
|
||||
let dy = by - ay;
|
||||
let len2 = dx * dx + dy * dy;
|
||||
let t_click = if len2 > 1e-12 {
|
||||
((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
extend_line(l, t_click, &self.geos)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(new_entity) = result {
|
||||
// Save the extended entity so on_entity_replaced can patch the snapshot
|
||||
// with both the new geometry and the real document handle.
|
||||
self.pending_replace = Some((handle, new_entity.clone()));
|
||||
CmdResult::ReplaceEntity(handle, vec![new_entity])
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
fn on_entity_replaced(&mut self, old: Handle, new_handles: &[acadrust::Handle]) {
|
||||
if let (Some(&new_handle), Some((pending_old, mut new_entity))) =
|
||||
(new_handles.first(), self.pending_replace.take())
|
||||
{
|
||||
if pending_old == old {
|
||||
// Update the snapshot entry: replace geometry + assign real handle.
|
||||
if let EntityType::Line(l) = &mut new_entity {
|
||||
l.common.handle = new_handle;
|
||||
}
|
||||
if let Some(pos) = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.position(|e| e.common().handle == old)
|
||||
{
|
||||
self.all_entities[pos] = new_entity;
|
||||
}
|
||||
self.geos = build_geos(&self.all_entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, pt: Vec3) -> Vec<WireModel> {
|
||||
if handle.is_null() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let entity = self
|
||||
.all_entities
|
||||
.iter()
|
||||
.find(|e| e.common().handle == handle);
|
||||
if let Some(EntityType::Line(l)) = entity {
|
||||
let ax = l.start.x;
|
||||
let ay = l.start.y;
|
||||
let bx = l.end.x;
|
||||
let by = l.end.y;
|
||||
let dx = bx - ax;
|
||||
let dy = by - ay;
|
||||
let len2 = dx * dx + dy * dy;
|
||||
let t_click = if len2 > 1e-12 {
|
||||
((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
if let Some(extended) = extend_line(l, t_click, &self.geos) {
|
||||
let pts = entity_pts(&extended);
|
||||
return vec![WireModel::solid(
|
||||
"extend_prev".into(),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)];
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Ungroup tool — ribbon definition.
|
||||
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "UNGROUP",
|
||||
label: "Ungroup",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/ungroup.svg")),
|
||||
event: ModuleEvent::Command("UNGROUP".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// ByLayer tool — ribbon definition.
|
||||
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "BYLAYER",
|
||||
label: "ByLayer",
|
||||
icon: IconKind::Glyph("⊟"),
|
||||
event: ModuleEvent::Command("BYLAYER".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Match Properties tool — ribbon definition.
|
||||
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "MATCHPROP",
|
||||
label: "Match",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/match_prop.svg")),
|
||||
event: ModuleEvent::Command("MATCHPROP".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub(super) mod match_prop;
|
||||
Reference in new issue
Block a user