mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
2301 lines
95 KiB
Rust
2301 lines
95 KiB
Rust
//! Draw menu depth (M1/M2): every ARC and CIRCLE variant, ELLIPSE axis/arc entries, RECTANG
|
|
//! options, DIVIDE/MEASURE, REVCLOUD, WIPEOUT, XLINE options, 3DPOLY, MLINE, HELIX, spline by
|
|
//! control vertices, CENTERMARK and CENTERLINE. Each has a JSON form and a prompt sequence.
|
|
|
|
use cadcraft_doc::{EntityKind, Handle, Helix, HelixConstrain, Point, Polyline3d, RayLine, Wipeout};
|
|
use cadcraft_geom::{Arc, Circle, Ellipse, Line, PolyVertex, Polyline, Spline, TAU, Vec2, Vec3, norm_angle, shoelace};
|
|
use serde_json::{Value, json};
|
|
|
|
use super::curves::{self, Chain, MAX_GEN, TanObj};
|
|
use super::helpers::*;
|
|
use super::machines::number;
|
|
use super::*;
|
|
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
|
|
|
pub(crate) mod mline;
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
let mut v = vec![
|
|
CommandSpec::new("arc.sce", "Start, Center, End", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Start, Center, End"])
|
|
.params("{start, center, end}")
|
|
.interactive(|_| Ok(seq(ArcV::Sce))),
|
|
CommandSpec::new("arc.sca", "Start, Center, Angle", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Start, Center, Angle"])
|
|
.params("{start, center, angle (degrees, + = CCW)}")
|
|
.interactive(|_| Ok(seq(ArcV::Sca))),
|
|
CommandSpec::new("arc.scl", "Start, Center, Length", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Start, Center, Length"])
|
|
.params("{start, center, length (chord; negative = major arc)}")
|
|
.interactive(|_| Ok(seq(ArcV::Scl))),
|
|
CommandSpec::new("arc.sea", "Start, End, Angle", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Start, End, Angle"])
|
|
.params("{start, end, angle (degrees, + = CCW)}")
|
|
.interactive(|_| Ok(seq(ArcV::Sea))),
|
|
CommandSpec::new("arc.sed", "Start, End, Direction", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Start, End, Direction"])
|
|
.params("{start, end, direction (degrees | [dx,dy])}")
|
|
.interactive(|_| Ok(seq(ArcV::Sed))),
|
|
CommandSpec::new("arc.ser", "Start, End, Radius", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Start, End, Radius"])
|
|
.params("{start, end, radius (negative = major arc)}")
|
|
.interactive(|_| Ok(seq(ArcV::Ser))),
|
|
CommandSpec::new("arc.cse", "Center, Start, End", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Center, Start, End"])
|
|
.params("{center, start, end}")
|
|
.interactive(|_| Ok(seq(ArcV::Cse))),
|
|
CommandSpec::new("arc.csa", "Center, Start, Angle", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Center, Start, Angle"])
|
|
.params("{center, start, angle (degrees)}")
|
|
.interactive(|_| Ok(seq(ArcV::Csa))),
|
|
CommandSpec::new("arc.csl", "Center, Start, Length", run_arc_variant)
|
|
.menu(&["Draw", "Arc", "Center, Start, Length"])
|
|
.params("{center, start, length (chord)}")
|
|
.interactive(|_| Ok(seq(ArcV::Csl))),
|
|
CommandSpec::new("arc.continue", "Continue", run_arc_continue)
|
|
.menu(&["Draw", "Arc", "Continue"])
|
|
.params("{end} (tangent from the last line/arc) | {start, direction, end}")
|
|
.interactive(|_| Ok(Box::new(ArcContinueM::default()))),
|
|
CommandSpec::new("circle.cd", "Center, Diameter", run_circle_variant)
|
|
.menu(&["Draw", "Circle", "Center, Diameter"])
|
|
.params("{center, diameter}")
|
|
.interactive(|_| Ok(seq(ArcV::CircleCd))),
|
|
CommandSpec::new("circle.2p", "2 Points", run_circle_variant)
|
|
.menu(&["Draw", "Circle", "2 Points"])
|
|
.params("{p1, p2}")
|
|
.interactive(|_| Ok(seq(ArcV::Circle2p))),
|
|
CommandSpec::new("circle.3p", "3 Points", run_circle_variant)
|
|
.menu(&["Draw", "Circle", "3 Points"])
|
|
.params("{p1, p2, p3}")
|
|
.interactive(|_| Ok(seq(ArcV::Circle3p))),
|
|
CommandSpec::new("circle.ttr", "Tan, Tan, Radius", run_circle_ttr)
|
|
.menu(&["Draw", "Circle", "Tan, Tan, Radius"])
|
|
.params("{h1, p1, h2, p2, radius} (lines, circles, arcs, polyline segments; nearest solution to the picks)")
|
|
.interactive(|_| Ok(Box::new(TanCircleM::new(true)))),
|
|
CommandSpec::new("circle.ttt", "Tan, Tan, Tan", run_circle_ttt)
|
|
.menu(&["Draw", "Circle", "Tan, Tan, Tan"])
|
|
.params("{h1, p1, h2, p2, h3, p3}")
|
|
.interactive(|_| Ok(Box::new(TanCircleM::new(false)))),
|
|
CommandSpec::new("ellipse.axis", "Axis, End", run_ellipse_axis)
|
|
.menu(&["Draw", "Ellipse", "Axis, End"])
|
|
.params("{p1, p2, distance} | {p1, p2, p3} (axis endpoints, then half the other axis)")
|
|
.interactive(|_| Ok(super::draw::ellipse_machine(false, false))),
|
|
CommandSpec::new("ellipse.arc", "Elliptical Arc", run_ellipse_arc)
|
|
.menu(&["Draw", "Ellipse", "Arc"])
|
|
.params("{center, major: [dx,dy], ratio, start, end (degrees)} | {p1, p2, distance, start, end}")
|
|
.interactive(|_| Ok(super::draw::ellipse_machine(false, true))),
|
|
CommandSpec::new("divide", "Divide", run_divide)
|
|
.menu(&["Draw", "Point", "Divide"])
|
|
.alias(&["div"])
|
|
.params("{handle, segments, block?, align?: bool}")
|
|
.interactive(|_| Ok(Box::new(DivideM::new(false)))),
|
|
CommandSpec::new("measure", "Measure", run_measure)
|
|
.menu(&["Draw", "Point", "Measure"])
|
|
.alias(&["me"])
|
|
.params("{handle, length, block?, align?: bool, from?: [x,y] (end to start at)}")
|
|
.interactive(|_| Ok(Box::new(DivideM::new(true)))),
|
|
CommandSpec::new("revcloud", "Rectangular", run_revcloud)
|
|
.menu(&["Draw", "Revision Cloud", "Rectangular"])
|
|
.params("{p1, p2} | {points: [...]} | {freehand: [...]} | {handle}, arcLength?, style?: normal|calligraphy, reverse?")
|
|
.interactive(|s| Ok(Box::new(RevcloudM::new(s, CloudKind::Rect)))),
|
|
CommandSpec::new("revcloud.polygonal", "Polygonal", run_revcloud)
|
|
.menu(&["Draw", "Revision Cloud", "Polygonal"])
|
|
.params("{points: [[x,y],...], arcLength?}")
|
|
.interactive(|s| Ok(Box::new(RevcloudM::new(s, CloudKind::Poly)))),
|
|
CommandSpec::new("revcloud.freehand", "Freehand", run_revcloud)
|
|
.menu(&["Draw", "Revision Cloud", "Freehand"])
|
|
.params("{freehand: [[x,y],...], arcLength?}")
|
|
.interactive(|s| Ok(Box::new(RevcloudM::new(s, CloudKind::Free)))),
|
|
CommandSpec::new("wipeout", "Wipeout", run_wipeout)
|
|
.menu(&["Draw", "Wipeout"])
|
|
.params("{points: [[x,y],...]} | {handle (closed polyline), erase?: bool} | {frames: on|off|display}")
|
|
.interactive(|_| Ok(Box::new(WipeoutM::default()))),
|
|
CommandSpec::new("3dpoly", "3D Polyline", run_3dpoly)
|
|
.menu(&["Draw", "3D Polyline"])
|
|
.alias(&["3p"])
|
|
.params("{points: [[x,y,z?],...], closed?}")
|
|
.interactive(|_| Ok(Box::new(Poly3dM::default()))),
|
|
CommandSpec::new("helix", "Helix", run_helix)
|
|
.menu(&["Draw", "Helix"])
|
|
.params("{center, baseRadius, topRadius?, height?, turns?, ccw?, startAngle? (degrees)} → {handle}")
|
|
.interactive(|_| Ok(Box::new(HelixM::default()))),
|
|
CommandSpec::new("spline.cv", "Control Vertices", run_spline_cv)
|
|
.menu(&["Draw", "Spline", "Control Vertices"])
|
|
.params("{control: [[x,y],...], degree?: 1..10 (default 3), closed?}")
|
|
.interactive(|_| Ok(Box::new(SplineCvM::default()))),
|
|
CommandSpec::new("centermark", "Center Mark", run_centermark)
|
|
.menu(&["Draw", "Center Mark"])
|
|
.params("{handle | handles} (circles/arcs; lines on the CENTER linetype)")
|
|
.interactive(|_| Ok(Box::new(CenterM { first: None, mark: true }))),
|
|
CommandSpec::new("centerline", "Center Line", run_centerline)
|
|
.menu(&["Draw", "Center Line"])
|
|
.params("{h1, h2} (two lines)")
|
|
.interactive(|_| Ok(Box::new(CenterM { first: None, mark: false }))),
|
|
];
|
|
v.extend(mline::specs());
|
|
v
|
|
}
|
|
|
|
fn added(h: Handle) -> Result<Value> {
|
|
Ok(json!({ "handle": h.hex() }))
|
|
}
|
|
|
|
fn other(m: impl Into<String>) -> EngineError {
|
|
EngineError::Other(m.into())
|
|
}
|
|
|
|
// =====================================================================================
|
|
// Generic point/value sequences (arc and circle variants)
|
|
// =====================================================================================
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum ArcV {
|
|
/// The ARC command itself: three points, with options switching to the other variants.
|
|
ThreeP,
|
|
/// Start, end, then center.
|
|
Sec,
|
|
Sce,
|
|
Sca,
|
|
Scl,
|
|
Sea,
|
|
Sed,
|
|
Ser,
|
|
Cse,
|
|
Csa,
|
|
Csl,
|
|
CircleCd,
|
|
Circle2p,
|
|
Circle3p,
|
|
}
|
|
|
|
/// What a step asks for.
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum Ask {
|
|
Point,
|
|
/// Absolute angle from value `base` to the picked point, or a typed angle.
|
|
Angle(usize),
|
|
/// CCW angle at value `center` from the ray to value `from` to the ray to the picked point.
|
|
Sweep(usize, usize),
|
|
/// Distance from value `base`, or a typed distance (may be negative).
|
|
Dist(usize),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum Val {
|
|
P(Vec2),
|
|
N(f64),
|
|
}
|
|
|
|
impl Val {
|
|
fn p(self) -> Option<Vec2> {
|
|
if let Val::P(p) = self { Some(p) } else { None }
|
|
}
|
|
fn n(self) -> Option<f64> {
|
|
if let Val::N(n) = self { Some(n) } else { None }
|
|
}
|
|
}
|
|
|
|
impl ArcV {
|
|
fn name(self) -> &'static str {
|
|
match self {
|
|
ArcV::CircleCd | ArcV::Circle2p | ArcV::Circle3p => "CIRCLE",
|
|
_ => "ARC",
|
|
}
|
|
}
|
|
fn steps(self) -> &'static [(&'static str, Ask)] {
|
|
const S: &str = "Specify start point of arc";
|
|
const C: &str = "Specify center point of arc";
|
|
const E: &str = "Specify end point of arc";
|
|
const A: &str = "Specify included angle";
|
|
const L: &str = "Specify length of chord";
|
|
match self {
|
|
ArcV::ThreeP => &[(S, Ask::Point), ("Specify second point of arc", Ask::Point), (E, Ask::Point)],
|
|
ArcV::Sec => &[(S, Ask::Point), (E, Ask::Point), (C, Ask::Point)],
|
|
ArcV::Sce => &[(S, Ask::Point), (C, Ask::Point), (E, Ask::Point)],
|
|
ArcV::Sca => &[(S, Ask::Point), (C, Ask::Point), (A, Ask::Sweep(1, 0))],
|
|
ArcV::Scl => &[(S, Ask::Point), (C, Ask::Point), (L, Ask::Dist(0))],
|
|
ArcV::Sea => &[(S, Ask::Point), (E, Ask::Point), (A, Ask::Angle(0))],
|
|
ArcV::Sed => &[(S, Ask::Point), (E, Ask::Point), ("Specify tangent direction for the start point of arc", Ask::Angle(0))],
|
|
ArcV::Ser => &[(S, Ask::Point), (E, Ask::Point), ("Specify radius of arc", Ask::Dist(1))],
|
|
ArcV::Cse => &[(C, Ask::Point), (S, Ask::Point), (E, Ask::Point)],
|
|
ArcV::Csa => &[(C, Ask::Point), (S, Ask::Point), (A, Ask::Sweep(0, 1))],
|
|
ArcV::Csl => &[(C, Ask::Point), (S, Ask::Point), (L, Ask::Dist(1))],
|
|
ArcV::CircleCd => &[("Specify center point for circle", Ask::Point), ("Specify diameter of circle", Ask::Dist(0))],
|
|
ArcV::Circle2p => {
|
|
&[("Specify first end point of circle's diameter", Ask::Point), ("Specify second end point of circle's diameter", Ask::Point)]
|
|
}
|
|
ArcV::Circle3p => &[
|
|
("Specify first point on circle", Ask::Point),
|
|
("Specify second point on circle", Ask::Point),
|
|
("Specify third point on circle", Ask::Point),
|
|
],
|
|
}
|
|
}
|
|
fn build(self, v: &[Val]) -> Option<EntityKind> {
|
|
let p = |i: usize| v.get(i).and_then(|x| x.p());
|
|
let n = |i: usize| v.get(i).and_then(|x| x.n());
|
|
let a = match self {
|
|
ArcV::ThreeP => Arc::from_3_points(p(0)?, p(1)?, p(2)?),
|
|
ArcV::Sec => Some(Arc::from_start_center_end(p(0)?, p(2)?, p(1)?)),
|
|
ArcV::Sce => Some(Arc::from_start_center_end(p(0)?, p(1)?, p(2)?)),
|
|
ArcV::Sca => curves::arc_sca(p(0)?, p(1)?, n(2)?),
|
|
ArcV::Scl => curves::arc_scl(p(0)?, p(1)?, n(2)?),
|
|
ArcV::Sea => curves::arc_sea(p(0)?, p(1)?, n(2)?),
|
|
ArcV::Sed => curves::arc_sed(p(0)?, p(1)?, Vec2::from_angle(n(2)?)),
|
|
ArcV::Ser => curves::arc_ser(p(0)?, p(1)?, n(2)?),
|
|
ArcV::Cse => Some(Arc::from_start_center_end(p(1)?, p(0)?, p(2)?)),
|
|
ArcV::Csa => curves::arc_sca(p(1)?, p(0)?, n(2)?),
|
|
ArcV::Csl => curves::arc_scl(p(1)?, p(0)?, n(2)?),
|
|
ArcV::CircleCd => {
|
|
let d = n(1)?.abs();
|
|
let c = p(0)?;
|
|
return (d > 1e-12 && d.is_finite()).then(|| circle(c, d / 2.0));
|
|
}
|
|
ArcV::Circle2p => {
|
|
let c = Circle::from_2_points(p(0)?, p(1)?);
|
|
return (c.radius > 1e-12).then(|| circle(c.center, c.radius));
|
|
}
|
|
ArcV::Circle3p => {
|
|
let c = Circle::from_3_points(p(0)?, p(1)?, p(2)?)?;
|
|
return Some(circle(c.center, c.radius));
|
|
}
|
|
}?;
|
|
(a.radius > 1e-12 && a.radius.is_finite() && a.center.is_finite()).then(|| arc(&a))
|
|
}
|
|
}
|
|
|
|
impl ArcV {
|
|
/// Options at step `i` and the variant each one switches to (the values so far carry over).
|
|
fn alts(self, i: usize) -> &'static [(&'static str, ArcV)] {
|
|
match (self, i) {
|
|
(ArcV::ThreeP, 0) => &[("Center", ArcV::Cse)],
|
|
(ArcV::ThreeP, 1) => &[("Center", ArcV::Sce), ("End", ArcV::Sec)],
|
|
(ArcV::Sce, 2) => &[("Angle", ArcV::Sca), ("chord Length", ArcV::Scl)],
|
|
(ArcV::Cse, 2) => &[("Angle", ArcV::Csa), ("chord Length", ArcV::Csl)],
|
|
(ArcV::Sec, 2) => &[("Angle", ArcV::Sea), ("Direction", ArcV::Sed), ("Radius", ArcV::Ser)],
|
|
_ => &[],
|
|
}
|
|
}
|
|
}
|
|
|
|
fn seq(v: ArcV) -> Box<dyn Interactive> {
|
|
Box::new(SeqM { v, vals: Vec::new(), cont: None })
|
|
}
|
|
|
|
/// The full ARC prompt sequence (all options; Enter at the first prompt continues the last
|
|
/// line or arc).
|
|
pub(crate) fn arc_machine() -> Box<dyn Interactive> {
|
|
seq(ArcV::ThreeP)
|
|
}
|
|
|
|
struct SeqM {
|
|
v: ArcV,
|
|
vals: Vec<Val>,
|
|
/// ARC continuation: start point and tangent.
|
|
cont: Option<(Vec2, Vec2)>,
|
|
}
|
|
|
|
impl SeqM {
|
|
fn ask(&self) -> Option<(&'static str, Ask)> {
|
|
self.v.steps().get(self.vals.len()).copied()
|
|
}
|
|
fn value_from_point(&self, ask: Ask, q: Vec2) -> Option<Val> {
|
|
let pt = |i: usize| self.vals.get(i).and_then(|x| x.p());
|
|
Some(match ask {
|
|
Ask::Point => Val::P(q),
|
|
Ask::Angle(b) => Val::N(pt(b)?.angle_to(q)),
|
|
Ask::Sweep(c, f) => {
|
|
let c = pt(c)?;
|
|
Val::N(norm_angle(c.angle_to(q) - c.angle_to(pt(f)?)))
|
|
}
|
|
Ask::Dist(b) => Val::N(pt(b)?.dist(q)),
|
|
})
|
|
}
|
|
fn base(&self) -> Option<Vec2> {
|
|
match self.ask()?.1 {
|
|
Ask::Point => self.vals.last().and_then(|v| v.p()),
|
|
Ask::Angle(b) | Ask::Dist(b) => self.vals.get(b).and_then(|v| v.p()),
|
|
Ask::Sweep(c, _) => self.vals.get(c).and_then(|v| v.p()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Interactive for SeqM {
|
|
fn name(&self) -> &'static str {
|
|
self.v.name()
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
if let Some((st, _)) = self.cont {
|
|
return Prompt::new("Specify end point of arc (hold Ctrl to switch direction)", Accept::POINT).base(st);
|
|
}
|
|
let kws: Vec<&str> = self.v.alts(self.vals.len()).iter().map(|a| a.0).collect();
|
|
let p = match self.ask() {
|
|
Some((m, Ask::Point)) => Prompt::new(m, Accept::POINT).base_opt(self.base()),
|
|
Some((m, _)) => Prompt::new(m, Accept::POINT_OR_NUMBER).base_opt(self.base()),
|
|
None => Prompt::new("", Accept::default()),
|
|
};
|
|
p.kw(&kws)
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
if let Some((st, dir)) = self.cont {
|
|
return match i {
|
|
Input::Point(e) => {
|
|
let a = curves::arc_sed(st, e, dir).ok_or_else(|| other("Invalid end point."))?;
|
|
s.add_entity(arc(&a))?;
|
|
Ok(Step::Done)
|
|
}
|
|
Input::Enter => Ok(Step::Cancel),
|
|
_ => Err(other("Point required.")),
|
|
};
|
|
}
|
|
if let Input::Keyword(k) = &i
|
|
&& let Some((_, v)) = self.v.alts(self.vals.len()).iter().find(|a| a.0 == k)
|
|
{
|
|
self.v = *v;
|
|
return Ok(Step::Continue);
|
|
}
|
|
if i == Input::Enter && self.v == ArcV::ThreeP && self.vals.is_empty() {
|
|
self.cont = curves::last_curve_end(s);
|
|
if self.cont.is_none() {
|
|
s.echo("No line or arc to continue.");
|
|
return Ok(Step::Cancel);
|
|
}
|
|
return Ok(Step::Continue);
|
|
}
|
|
let Some((_, ask)) = self.ask() else { return Ok(Step::Done) };
|
|
let val = match (&i, ask) {
|
|
(Input::Point(q), _) => self.value_from_point(ask, *q),
|
|
(Input::Text(t), Ask::Angle(_) | Ask::Sweep(..)) => {
|
|
// The tangent direction is a direction; included angles are sizes.
|
|
let au = s.angle_settings();
|
|
let a = if self.v == ArcV::Sed { au.direction(t) } else { au.amount(t) };
|
|
Some(Val::N(a.ok_or_else(|| other("Requires a valid angle."))?))
|
|
}
|
|
(Input::Text(t), Ask::Dist(_)) => Some(Val::N(number(t).ok_or_else(|| other("Requires numeric distance or a point."))?)),
|
|
(Input::Enter, _) => return Ok(Step::Cancel),
|
|
_ => return Err(other("Point or value required.")),
|
|
};
|
|
let Some(val) = val else { return Ok(Step::Continue) };
|
|
self.vals.push(val);
|
|
if self.vals.len() < self.v.steps().len() {
|
|
return Ok(Step::Continue);
|
|
}
|
|
match self.v.build(&self.vals) {
|
|
Some(k) => {
|
|
if let EntityKind::Circle(c) = &k {
|
|
let r = c.radius;
|
|
s.add_entity(k)?;
|
|
s.doc_mut()?.header.set_f64("CIRCLERAD", r);
|
|
} else {
|
|
s.add_entity(k)?;
|
|
}
|
|
Ok(Step::Done)
|
|
}
|
|
None => {
|
|
self.vals.pop();
|
|
Err(other("Invalid arc or circle for these values."))
|
|
}
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
if let Some((st, d)) = self.cont {
|
|
return curves::arc_sed(st, c, d).map(|a| vec![arc(&a)]).unwrap_or_default();
|
|
}
|
|
let Some((_, ask)) = self.ask() else { return Vec::new() };
|
|
let Some(v) = self.value_from_point(ask, c) else { return Vec::new() };
|
|
let mut vals = self.vals.clone();
|
|
vals.push(v);
|
|
let mut out: Vec<EntityKind> = self.base().map(|b| vec![line(b, c)]).unwrap_or_default();
|
|
if vals.len() == self.v.steps().len() {
|
|
out.extend(self.v.build(&vals));
|
|
}
|
|
out
|
|
}
|
|
}
|
|
|
|
fn direction_param(p: &Value) -> Option<f64> {
|
|
match p.get("direction") {
|
|
Some(v) if v.is_array() || v.is_object() => point_value(v).filter(|d| d.len() > 1e-12).map(Vec2::angle),
|
|
Some(v) => v.as_f64().filter(|x| x.is_finite()).map(f64::to_radians),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
fn arc_from_params(p: &Value) -> Result<EntityKind> {
|
|
let st = point_param(p, "start");
|
|
let c = point_param(p, "center");
|
|
let e = point_param(p, "end");
|
|
let ang = p.get("angle").and_then(Value::as_f64).filter(|v| v.is_finite()).map(f64::to_radians);
|
|
let len = p.get("length").and_then(Value::as_f64).filter(|v| v.is_finite());
|
|
let rad = p.get("radius").and_then(Value::as_f64).filter(|v| v.is_finite());
|
|
let dir = direction_param(p);
|
|
let a = match (st, c, e) {
|
|
(Some(st), Some(c), Some(e)) => Some(Arc::from_start_center_end(st, c, e)),
|
|
(Some(st), Some(c), None) => match (ang, len) {
|
|
(Some(a), _) => curves::arc_sca(st, c, a),
|
|
(None, Some(l)) => curves::arc_scl(st, c, l),
|
|
_ => return Err(bad("arc", "start+center needs `end`, `angle` or `length`")),
|
|
},
|
|
(Some(st), None, Some(e)) => match (ang, dir, rad) {
|
|
(Some(a), _, _) => curves::arc_sea(st, e, a),
|
|
(None, Some(d), _) => curves::arc_sed(st, e, Vec2::from_angle(d)),
|
|
(None, None, Some(r)) => curves::arc_ser(st, e, r),
|
|
_ => return Err(bad("arc", "start+end needs `angle`, `direction` or `radius`")),
|
|
},
|
|
_ => return Err(bad("arc", "give start with center and/or end (see params)")),
|
|
};
|
|
let a = a.filter(|a| a.radius > 1e-12 && a.radius.is_finite()).ok_or_else(|| bad("arc", "these values do not define an arc"))?;
|
|
Ok(arc(&a))
|
|
}
|
|
|
|
fn run_arc_variant(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let k = arc_from_params(p)?;
|
|
added(s.add_entity(k)?)
|
|
}
|
|
|
|
fn run_arc_continue(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let e = point_req("arc.continue", p, "end")?;
|
|
let (st, dir) = match (point_param(p, "start"), direction_param(p)) {
|
|
(Some(st), Some(d)) => (st, Vec2::from_angle(d)),
|
|
_ => curves::last_curve_end(s).ok_or_else(|| bad("arc.continue", "no line or arc to continue from"))?,
|
|
};
|
|
let a = curves::arc_sed(st, e, dir).ok_or_else(|| bad("arc.continue", "end point is on the tangent line"))?;
|
|
let h = s.add_entity(arc(&a))?;
|
|
// The next continuation starts where this arc was drawn to, not at its stored CCW end.
|
|
s.last_point = e;
|
|
added(h)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ArcContinueM {
|
|
from: Option<(Vec2, Vec2)>,
|
|
}
|
|
|
|
impl Interactive for ArcContinueM {
|
|
fn name(&self) -> &'static str {
|
|
"ARC"
|
|
}
|
|
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
|
self.from = curves::last_curve_end(s);
|
|
if self.from.is_none() {
|
|
s.echo("No line or arc to continue.");
|
|
return Ok(Step::Cancel);
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
Prompt::new("Specify end point of arc (hold Ctrl to switch direction)", Accept::POINT).base_opt(self.from.map(|f| f.0))
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
let Some((st, dir)) = self.from else { return Ok(Step::Cancel) };
|
|
match i {
|
|
Input::Point(e) => {
|
|
let a = curves::arc_sed(st, e, dir).ok_or_else(|| other("Invalid end point."))?;
|
|
s.add_entity(arc(&a))?;
|
|
Ok(Step::Done)
|
|
}
|
|
Input::Enter => Ok(Step::Cancel),
|
|
_ => Err(other("Point required.")),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
self.from.and_then(|(st, d)| curves::arc_sed(st, c, d)).map(|a| vec![arc(&a)]).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
fn run_circle_variant(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let c = if let (Some(c), Some(d)) = (point_param(p, "center"), p.get("diameter").and_then(Value::as_f64)) {
|
|
Circle::new(c, d / 2.0)
|
|
} else if let (Some(a), Some(b)) = (point_param(p, "p1"), point_param(p, "p2")) {
|
|
match point_param(p, "p3") {
|
|
Some(c3) => Circle::from_3_points(a, b, c3).ok_or_else(|| bad("circle", "points are collinear"))?,
|
|
None => Circle::from_2_points(a, b),
|
|
}
|
|
} else {
|
|
return Err(bad("circle", "give {center, diameter}, {p1, p2} or {p1, p2, p3}"));
|
|
};
|
|
if !(c.radius > 1e-12 && c.radius.is_finite()) {
|
|
return Err(bad("circle", "radius must be positive"));
|
|
}
|
|
let h = s.add_entity(circle(c.center, c.radius))?;
|
|
s.doc_mut()?.header.set_f64("CIRCLERAD", c.radius);
|
|
added(h)
|
|
}
|
|
|
|
// ---------------- tangent circles ----------------
|
|
|
|
fn tan_obj(s: &Session, h: Handle, pick: Vec2) -> Result<TanObj> {
|
|
let e = curves::entity(s, h)?;
|
|
TanObj::from_kind(&e.kind, pick).ok_or_else(|| other("Select a line, circle, arc or polyline segment."))
|
|
}
|
|
|
|
fn pick_pair(cmd: &str, p: &Value, hk: &str, pk: &str) -> Result<(Handle, Vec2)> {
|
|
let h = curves::handle_param(p, hk).ok_or_else(|| bad(cmd, format!("`{hk}` is required")))?;
|
|
let q = point_req(cmd, p, pk)?;
|
|
Ok((h, q))
|
|
}
|
|
|
|
fn run_circle_ttr(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let (h1, p1) = pick_pair("circle.ttr", p, "h1", "p1")?;
|
|
let (h2, p2) = pick_pair("circle.ttr", p, "h2", "p2")?;
|
|
let r = f64_req("circle.ttr", p, "radius")?;
|
|
let c = curves::ttr(tan_obj(s, h1, p1)?, p1, tan_obj(s, h2, p2)?, p2, r).ok_or_else(|| other("Circle does not exist."))?;
|
|
let h = s.add_entity(circle(c.center, c.radius))?;
|
|
s.doc_mut()?.header.set_f64("CIRCLERAD", c.radius);
|
|
added(h)
|
|
}
|
|
|
|
fn run_circle_ttt(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let (h1, p1) = pick_pair("circle.ttt", p, "h1", "p1")?;
|
|
let (h2, p2) = pick_pair("circle.ttt", p, "h2", "p2")?;
|
|
let (h3, p3) = pick_pair("circle.ttt", p, "h3", "p3")?;
|
|
let objs = [(tan_obj(s, h1, p1)?, p1), (tan_obj(s, h2, p2)?, p2), (tan_obj(s, h3, p3)?, p3)];
|
|
let c = curves::ttt(objs).ok_or_else(|| other("Circle does not exist."))?;
|
|
added(s.add_entity(circle(c.center, c.radius))?)
|
|
}
|
|
|
|
/// TTR (two picks + radius) or TTT (three picks). Also used by CIRCLE's `Ttr` option.
|
|
pub(crate) struct TanCircleM {
|
|
radius_mode: bool,
|
|
picks: Vec<(TanObj, Vec2)>,
|
|
}
|
|
|
|
impl TanCircleM {
|
|
pub(crate) fn new(radius_mode: bool) -> Self {
|
|
TanCircleM { radius_mode, picks: Vec::new() }
|
|
}
|
|
}
|
|
|
|
impl Interactive for TanCircleM {
|
|
fn name(&self) -> &'static str {
|
|
"CIRCLE"
|
|
}
|
|
fn prompt(&self, s: &Session) -> Prompt {
|
|
let n = self.picks.len();
|
|
if self.radius_mode {
|
|
match n {
|
|
0 => Prompt::new("Specify point on object for first tangent of circle", Accept::POINT),
|
|
1 => Prompt::new("Specify point on object for second tangent of circle", Accept::POINT),
|
|
_ => {
|
|
let r = s.doc().map(|d| d.header.f64("CIRCLERAD", 0.0)).unwrap_or(0.0);
|
|
let p = Prompt::new("Specify radius of circle", Accept::POINT_OR_NUMBER).base_opt(self.picks.last().map(|x| x.1)).distance();
|
|
if r > 0.0 { p.default(format!("{r:.4}")) } else { p }
|
|
}
|
|
}
|
|
} else {
|
|
let m = ["Specify first point on circle: _tan to", "Specify second point on circle: _tan to", "Specify third point on circle: _tan to"];
|
|
Prompt::new(m.get(n).copied().unwrap_or(""), Accept::POINT)
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
let need = if self.radius_mode { 2 } else { 3 };
|
|
if self.picks.len() < need {
|
|
return match i {
|
|
Input::Point(p) => {
|
|
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
|
|
let o = tan_obj(s, h, p)?;
|
|
self.picks.push((o, p));
|
|
if !self.radius_mode && self.picks.len() == 3 {
|
|
let (Some(a), Some(b), Some(c)) = (self.picks.first(), self.picks.get(1), self.picks.get(2)) else {
|
|
return Ok(Step::Cancel);
|
|
};
|
|
let c = curves::ttt([*a, *b, *c]).ok_or_else(|| {
|
|
self.picks.clear();
|
|
other("Circle does not exist.")
|
|
})?;
|
|
s.add_entity(circle(c.center, c.radius))?;
|
|
return Ok(Step::Done);
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Enter => Ok(Step::Cancel),
|
|
_ => Err(other("Select an object.")),
|
|
};
|
|
}
|
|
let r = match i {
|
|
Input::Point(p) => self.picks.last().map(|x| x.1.dist(p)).unwrap_or(0.0),
|
|
Input::Text(t) => number(&t).ok_or_else(|| other("Requires numeric distance or second point."))?,
|
|
Input::Enter => s.doc()?.header.f64("CIRCLERAD", 0.0),
|
|
_ => return Ok(Step::Continue),
|
|
};
|
|
if r.is_nan() || r <= 0.0 {
|
|
return Err(other("Value must be positive and nonzero."));
|
|
}
|
|
let ((o1, p1), (o2, p2)) = match (self.picks.first(), self.picks.get(1)) {
|
|
(Some(a), Some(b)) => (*a, *b),
|
|
_ => return Ok(Step::Cancel),
|
|
};
|
|
match curves::ttr(o1, p1, o2, p2, r) {
|
|
Some(c) => {
|
|
s.add_entity(circle(c.center, c.radius))?;
|
|
s.doc_mut()?.header.set_f64("CIRCLERAD", r);
|
|
Ok(Step::Done)
|
|
}
|
|
None => {
|
|
s.echo("Circle does not exist.");
|
|
Ok(Step::Done)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------- ellipse entries ----------------
|
|
|
|
fn ellipse_from_axis(p1: Vec2, p2: Vec2, half: f64) -> Option<Ellipse> {
|
|
let c = p1.mid(p2);
|
|
let m = p2 - c;
|
|
let ml = m.len();
|
|
let half = half.abs();
|
|
if ml < 1e-12 || half < 1e-12 || !half.is_finite() {
|
|
return None;
|
|
}
|
|
Some(if half > ml { Ellipse::full(c, m.normalized().perp() * half, ml / half) } else { Ellipse::full(c, m, half / ml) })
|
|
}
|
|
|
|
fn axis_ellipse_params(cmd: &str, p: &Value) -> Result<Ellipse> {
|
|
let p1 = point_req(cmd, p, "p1")?;
|
|
let p2 = point_req(cmd, p, "p2")?;
|
|
let half = match point_param(p, "p3") {
|
|
Some(p3) => {
|
|
let ax = Line::new(p1, p2);
|
|
ax.project(p3).dist(p3)
|
|
}
|
|
None => f64_req(cmd, p, "distance")?,
|
|
};
|
|
ellipse_from_axis(p1, p2, half).ok_or_else(|| bad(cmd, "degenerate ellipse"))
|
|
}
|
|
|
|
fn ellipse_kind(e: &Ellipse) -> EntityKind {
|
|
EntityKind::Ellipse(cadcraft_doc::Ellipse { center: v3(e.center), major: v3(e.major), ratio: e.ratio, start: e.start, end: e.end })
|
|
}
|
|
|
|
fn run_ellipse_axis(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let e = axis_ellipse_params("ellipse.axis", p)?;
|
|
added(s.add_entity(ellipse_kind(&e))?)
|
|
}
|
|
|
|
fn run_ellipse_arc(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let mut e = if let Some(c) = point_param(p, "center") {
|
|
let m = point_req("ellipse.arc", p, "major")?;
|
|
let ratio = f64_req("ellipse.arc", p, "ratio")?;
|
|
if !(ratio > 0.0 && ratio <= 1.0) || m.len() < 1e-12 {
|
|
return Err(bad("ellipse.arc", "ratio must be in (0, 1] and major non-zero"));
|
|
}
|
|
Ellipse::full(c, m, ratio)
|
|
} else {
|
|
axis_ellipse_params("ellipse.arc", p)?
|
|
};
|
|
// Angles are measured from the major axis (like the prompts); convert to parameters.
|
|
let to_param = |deg: f64| {
|
|
let a = deg.to_radians();
|
|
let ge = Ellipse { start: 0.0, end: TAU, ..e };
|
|
ge.param_of(e.center + Vec2::from_angle(a + e.major.angle()))
|
|
};
|
|
let st = to_param(f64_req("ellipse.arc", p, "start")?);
|
|
let en = to_param(f64_req("ellipse.arc", p, "end")?);
|
|
e.start = st;
|
|
e.end = en;
|
|
added(s.add_entity(ellipse_kind(&e))?)
|
|
}
|
|
|
|
// =====================================================================================
|
|
// DIVIDE / MEASURE
|
|
// =====================================================================================
|
|
|
|
fn chain_of(s: &Session, h: Handle) -> Result<Chain> {
|
|
let e = curves::entity(s, h)?;
|
|
Chain::of(&e.kind).ok_or_else(|| other("Cannot divide/measure that object."))
|
|
}
|
|
|
|
fn place_markers(s: &mut Session, at: &[(Vec2, Vec2)], block: Option<&str>, align: bool) -> Result<Vec<String>> {
|
|
let mut out = Vec::new();
|
|
for (p, t) in at {
|
|
let h = match block {
|
|
Some(b) => super::blocks::insert(s, b, *p, 1.0, if align { t.angle() } else { 0.0 }, &serde_json::Map::new())?,
|
|
None => s.add_entity(EntityKind::Point(Point { p: v3(*p), angle: 0.0 }))?,
|
|
};
|
|
out.push(h.hex());
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
fn check_block(s: &Session, b: Option<&str>) -> Result<()> {
|
|
if let Some(b) = b
|
|
&& s.doc()?.block(b).is_none()
|
|
{
|
|
return Err(other(format!("Block \"{b}\" not found.")));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn divide_points(c: &Chain, n: usize) -> Vec<(Vec2, Vec2)> {
|
|
let l = c.len();
|
|
let range = if c.closed { 0..n } else { 1..n };
|
|
range.filter_map(|i| c.at_length(l * i as f64 / n as f64)).collect()
|
|
}
|
|
|
|
pub(crate) fn measure_points(c: &Chain, step: f64) -> Vec<(Vec2, Vec2)> {
|
|
let l = c.len();
|
|
let mut out = Vec::new();
|
|
let mut k = 1usize;
|
|
while (k as f64) * step < l - 1e-9 && out.len() < MAX_GEN {
|
|
let d = k as f64 * step;
|
|
out.extend(c.at_length(d));
|
|
k += 1;
|
|
}
|
|
out
|
|
}
|
|
|
|
fn run_divide(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let h = targets(s, p)?.first().copied().ok_or_else(|| bad("divide", "`handle` is required"))?;
|
|
let n = p.get("segments").and_then(Value::as_u64).ok_or_else(|| bad("divide", "`segments` (2..32767) is required"))?;
|
|
if !(2..=32767).contains(&n) {
|
|
return Err(bad("divide", "segments must be 2..32767"));
|
|
}
|
|
let block = str_param(p, "block").map(str::to_string);
|
|
check_block(s, block.as_deref())?;
|
|
let c = chain_of(s, h)?;
|
|
let pts = divide_points(&c, n as usize);
|
|
let hs = place_markers(s, &pts, block.as_deref(), bool_or(p, "align", true))?;
|
|
Ok(json!({ "handles": hs }))
|
|
}
|
|
|
|
fn run_measure(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let h = targets(s, p)?.first().copied().ok_or_else(|| bad("measure", "`handle` is required"))?;
|
|
let step = f64_req("measure", p, "length")?;
|
|
let mut c = chain_of(s, h)?;
|
|
if step.is_nan() || step <= 0.0 || c.len() / step > MAX_GEN as f64 {
|
|
return Err(bad("measure", "length must be positive (and not tiny)"));
|
|
}
|
|
let block = str_param(p, "block").map(str::to_string);
|
|
check_block(s, block.as_deref())?;
|
|
if let Some(from) = point_param(p, "from")
|
|
&& !c.closed
|
|
&& let (Some(a), Some(b)) = (c.start(), c.end())
|
|
&& from.dist(b) < from.dist(a)
|
|
{
|
|
c = c.reversed();
|
|
}
|
|
let pts = measure_points(&c, step);
|
|
let hs = place_markers(s, &pts, block.as_deref(), bool_or(p, "align", true))?;
|
|
Ok(json!({ "handles": hs }))
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum DivPhase {
|
|
Object,
|
|
Value,
|
|
BlockName,
|
|
Align,
|
|
}
|
|
|
|
struct DivideM {
|
|
measure: bool,
|
|
obj: Option<(Handle, Vec2)>,
|
|
phase: DivPhase,
|
|
block: Option<String>,
|
|
align: bool,
|
|
}
|
|
|
|
impl DivideM {
|
|
fn new(measure: bool) -> Self {
|
|
DivideM { measure, obj: None, phase: DivPhase::Object, block: None, align: true }
|
|
}
|
|
}
|
|
|
|
impl Interactive for DivideM {
|
|
fn name(&self) -> &'static str {
|
|
if self.measure { "MEASURE" } else { "DIVIDE" }
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
match self.phase {
|
|
DivPhase::Object => Prompt::new(if self.measure { "Select object to measure" } else { "Select object to divide" }, Accept::POINT),
|
|
DivPhase::Value => {
|
|
let m = if self.measure { "Specify length of segment" } else { "Enter the number of segments" };
|
|
let p = Prompt::new(m, if self.measure { Accept::POINT_OR_NUMBER } else { Accept::NUMBER });
|
|
let p = if self.measure { p.base_opt(self.obj.map(|o| o.1)) } else { p };
|
|
if self.block.is_some() { p } else { p.kw(&["Block"]) }
|
|
}
|
|
DivPhase::BlockName => Prompt::new("Enter name of block to insert", Accept::TEXT),
|
|
DivPhase::Align => Prompt::new("Align block with object?", curves::KW).kw(&["Yes", "No"]).default("Y"),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
match (self.phase, i) {
|
|
(DivPhase::Object, Input::Point(p)) => {
|
|
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
|
|
chain_of(s, h)?;
|
|
self.obj = Some((h, p));
|
|
self.phase = DivPhase::Value;
|
|
Ok(Step::Continue)
|
|
}
|
|
(DivPhase::Value, Input::Keyword(k)) if k == "Block" => {
|
|
self.phase = DivPhase::BlockName;
|
|
Ok(Step::Continue)
|
|
}
|
|
(DivPhase::BlockName, Input::Text(t) | Input::Keyword(t)) => {
|
|
let name = t.trim().to_string();
|
|
check_block(s, Some(&name))?;
|
|
self.block = Some(name);
|
|
self.phase = DivPhase::Align;
|
|
Ok(Step::Continue)
|
|
}
|
|
(DivPhase::Align, Input::Keyword(k) | Input::Text(k)) => {
|
|
self.align = !k.trim().to_ascii_lowercase().starts_with('n');
|
|
self.phase = DivPhase::Value;
|
|
Ok(Step::Continue)
|
|
}
|
|
(DivPhase::Align, Input::Enter) => {
|
|
self.phase = DivPhase::Value;
|
|
Ok(Step::Continue)
|
|
}
|
|
(DivPhase::Value, inp @ (Input::Text(_) | Input::Point(_))) => {
|
|
let Some((h, pick)) = self.obj else { return Ok(Step::Cancel) };
|
|
let v = match inp {
|
|
Input::Text(t) => number(&t).ok_or_else(|| other("Requires a number."))?,
|
|
Input::Point(q) => pick.dist(q),
|
|
_ => return Ok(Step::Continue),
|
|
};
|
|
let mut params = json!({ "handle": h.hex(), "align": self.align, "from": [pick.x, pick.y] });
|
|
if let Some(b) = &self.block {
|
|
params["block"] = json!(b);
|
|
}
|
|
if self.measure {
|
|
params["length"] = json!(v);
|
|
run_measure(s, ¶ms)?;
|
|
} else {
|
|
if !(2.0..=32767.0).contains(&v) || v.fract() != 0.0 {
|
|
return Err(other("Requires an integer between 2 and 32767."));
|
|
}
|
|
params["segments"] = json!(v as u64);
|
|
run_divide(s, ¶ms)?;
|
|
}
|
|
Ok(Step::Done)
|
|
}
|
|
(_, Input::Enter) => Ok(Step::Cancel),
|
|
_ => Err(other("Invalid input.")),
|
|
}
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// REVCLOUD
|
|
// =====================================================================================
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum CloudKind {
|
|
Rect,
|
|
Poly,
|
|
Free,
|
|
}
|
|
|
|
fn default_arc_len(s: &Session) -> f64 {
|
|
let d = s.doc().ok();
|
|
let metric = d.is_some_and(|d| d.header.i64("MEASUREMENT", 0) == 1);
|
|
d.map(|d| d.header.f64("REVCLOUDARCLEN", if metric { 15.0 } else { 0.5 })).unwrap_or(0.5).max(1e-6)
|
|
}
|
|
|
|
/// Bulged closed polyline vertices for a revision cloud around `boundary`.
|
|
pub(crate) fn cloud_vertices(boundary: &[Vec2], arc_len: f64, per_edge: bool, reverse: bool) -> Option<Vec<PolyVertex>> {
|
|
let mut pts: Vec<Vec2> = Vec::new();
|
|
for p in boundary {
|
|
if p.is_finite() && pts.last().is_none_or(|l| !l.near(*p, 1e-9)) {
|
|
pts.push(*p);
|
|
}
|
|
}
|
|
while pts.len() > 1 && pts.first().zip(pts.last()).is_some_and(|(a, b)| a.near(*b, 1e-9)) {
|
|
pts.pop();
|
|
}
|
|
if pts.len() < 3 || shoelace(&pts).abs() < 1e-12 {
|
|
return None;
|
|
}
|
|
if shoelace(&pts) < 0.0 {
|
|
pts.reverse();
|
|
}
|
|
let n = pts.len();
|
|
let perim: f64 = (0..n).filter_map(|i| Some(pts.get(i)?.dist(*pts.get((i + 1) % n)?))).sum();
|
|
if !(arc_len.is_finite() && arc_len > 0.0) {
|
|
return None;
|
|
}
|
|
let arc_len = arc_len.max(perim / MAX_GEN as f64);
|
|
let mut out: Vec<Vec2> = Vec::new();
|
|
if per_edge {
|
|
for i in 0..n {
|
|
let (Some(a), Some(b)) = (pts.get(i).copied(), pts.get((i + 1) % n).copied()) else { continue };
|
|
let k = ((a.dist(b) / arc_len).round() as usize).clamp(1, MAX_GEN);
|
|
for j in 0..k {
|
|
out.push(a.lerp(b, j as f64 / k as f64));
|
|
}
|
|
}
|
|
} else {
|
|
let chain = Chain {
|
|
segs: (0..n).filter_map(|i| Some(cadcraft_geom::Segment::Line(Line::new(*pts.get(i)?, *pts.get((i + 1) % n)?)))).collect(),
|
|
closed: true,
|
|
};
|
|
let k = ((perim / arc_len).round() as usize).clamp(3, MAX_GEN);
|
|
for j in 0..k {
|
|
out.extend(chain.at_length(perim * j as f64 / k as f64).map(|x| x.0));
|
|
}
|
|
}
|
|
let b = if reverse { -0.5 } else { 0.5 };
|
|
Some(out.into_iter().map(|p| PolyVertex::with_bulge(p, b)).collect())
|
|
}
|
|
|
|
fn object_outline(s: &Session, h: Handle) -> Result<Vec<Vec2>> {
|
|
let e = curves::entity(s, h)?;
|
|
let tol = 1e-3;
|
|
let pts = match &e.kind {
|
|
EntityKind::LwPolyline(p) if p.closed => Polyline { vertices: p.vertices.clone(), closed: true }.tessellate(tol),
|
|
EntityKind::Circle(c) => {
|
|
let mut v = Vec::new();
|
|
Circle::new(c.center.xy(), c.radius).tessellate(c.radius * 1e-3, &mut v);
|
|
v
|
|
}
|
|
EntityKind::Ellipse(el) => {
|
|
let mut v = Vec::new();
|
|
Ellipse { center: el.center.xy(), major: el.major.xy(), ratio: el.ratio, start: el.start, end: el.end }
|
|
.tessellate(el.major.xy().len() * 1e-3, &mut v);
|
|
v
|
|
}
|
|
EntityKind::Spline(sp) if sp.closed => sp.tessellate(tol),
|
|
_ => return Err(other("Select a closed polyline, circle, ellipse or closed spline.")),
|
|
};
|
|
Ok(pts)
|
|
}
|
|
|
|
fn cloud_entity(vs: Vec<PolyVertex>, calligraphy: bool, arc_len: f64) -> EntityKind {
|
|
let vs = if calligraphy { vs.into_iter().map(|v| PolyVertex { start_width: 0.0, end_width: arc_len * 0.15, ..v }).collect() } else { vs };
|
|
lwpoly(vs, true)
|
|
}
|
|
|
|
fn run_revcloud(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let arc_len = p.get("arcLength").and_then(Value::as_f64).filter(|v| v.is_finite() && *v > 0.0).unwrap_or_else(|| default_arc_len(s));
|
|
let reverse = bool_or(p, "reverse", false);
|
|
let calligraphy = str_param(p, "style").is_some_and(|x| x.eq_ignore_ascii_case("calligraphy"));
|
|
let (pts, per_edge, src) = if let (Some(a), Some(b)) = (point_param(p, "p1"), point_param(p, "p2")) {
|
|
(rect_vertices(a, b).into_iter().map(|v| v.p).collect(), true, None)
|
|
} else if let Some(pts) = points_param(p, "points") {
|
|
(pts, true, None)
|
|
} else if let Some(pts) = points_param(p, "freehand") {
|
|
(pts, false, None)
|
|
} else if let Some(h) = curves::handle_param(p, "handle") {
|
|
(object_outline(s, h)?, false, Some(h))
|
|
} else {
|
|
return Err(bad("revcloud", "give {p1, p2}, {points}, {freehand} or {handle}"));
|
|
};
|
|
if pts.len() > MAX_GEN {
|
|
return Err(bad("revcloud", "too many points"));
|
|
}
|
|
let vs = cloud_vertices(&pts, arc_len, per_edge, reverse).ok_or_else(|| bad("revcloud", "the boundary needs an area"))?;
|
|
let h = s.add_entity(cloud_entity(vs, calligraphy, arc_len))?;
|
|
if let Some(src) = src {
|
|
let common = curves::entity(s, src)?.common;
|
|
let d = s.doc_mut()?;
|
|
d.modify_entity(h, |e| e.common = common)?;
|
|
d.remove_entity(src);
|
|
}
|
|
added(h)
|
|
}
|
|
|
|
struct RevcloudM {
|
|
kind: CloudKind,
|
|
pts: Vec<Vec2>,
|
|
arc_len: f64,
|
|
asking_arc: bool,
|
|
object: bool,
|
|
calligraphy: bool,
|
|
asking_style: bool,
|
|
}
|
|
|
|
impl RevcloudM {
|
|
fn new(s: &Session, kind: CloudKind) -> Self {
|
|
RevcloudM { kind, pts: Vec::new(), arc_len: default_arc_len(s), asking_arc: false, object: false, calligraphy: false, asking_style: false }
|
|
}
|
|
fn boundary(&self, extra: Option<Vec2>) -> Vec<Vec2> {
|
|
let mut pts = self.pts.clone();
|
|
pts.extend(extra);
|
|
if self.kind == CloudKind::Rect
|
|
&& let (Some(a), Some(b)) = (pts.first().copied(), pts.get(1).copied())
|
|
{
|
|
return rect_vertices(a, b).into_iter().map(|v| v.p).collect();
|
|
}
|
|
pts
|
|
}
|
|
fn finish(&mut self, s: &mut Session) -> Result<Step> {
|
|
let b = self.boundary(None);
|
|
match cloud_vertices(&b, self.arc_len, self.kind != CloudKind::Free, false) {
|
|
Some(vs) => {
|
|
s.add_entity(cloud_entity(vs, self.calligraphy, self.arc_len))?;
|
|
s.echo("Revision cloud finished.");
|
|
Ok(Step::Done)
|
|
}
|
|
None => Err(other("The revision cloud needs at least three points enclosing an area.")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Interactive for RevcloudM {
|
|
fn name(&self) -> &'static str {
|
|
"REVCLOUD"
|
|
}
|
|
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
|
let t = match self.kind {
|
|
CloudKind::Rect => "Rectangular",
|
|
CloudKind::Poly => "Polygonal",
|
|
CloudKind::Free => "Freehand",
|
|
};
|
|
s.echo(format!("Minimum arc length: {0:.4} Maximum arc length: {0:.4} Style: Normal Type: {t}", self.arc_len));
|
|
Ok(Step::Continue)
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
if self.asking_arc {
|
|
return Prompt::new("Specify minimum length of arc", Accept::NUMBER).default(format!("{:.4}", self.arc_len));
|
|
}
|
|
if self.asking_style {
|
|
return Prompt::new("Select arc style", curves::KW).kw(&["Normal", "Calligraphy"]).default("Normal");
|
|
}
|
|
if self.object {
|
|
return Prompt::new("Select object", Accept::POINT);
|
|
}
|
|
let first_kw: &[&str] = &["Arc length", "Object", "Rectangular", "Polygonal", "Freehand", "Style"];
|
|
match (self.kind, self.pts.len()) {
|
|
(CloudKind::Rect, 0) => Prompt::new("Specify first corner point", Accept::POINT).kw(first_kw).default("Object"),
|
|
(CloudKind::Rect, _) => Prompt::new("Specify opposite corner", Accept::POINT).base_opt(self.pts.first().copied()),
|
|
(CloudKind::Poly, 0) => Prompt::new("Specify start point", Accept::POINT).kw(first_kw).default("Object"),
|
|
(CloudKind::Poly, 1) => Prompt::new("Specify next point", Accept::POINT).base_opt(self.pts.last().copied()),
|
|
(CloudKind::Poly, _) => Prompt::new("Specify next point", Accept::POINT).kw(&["Undo"]).base_opt(self.pts.last().copied()),
|
|
(CloudKind::Free, 0) => Prompt::new("Specify first point", Accept::POINT).kw(first_kw).default("Object"),
|
|
(CloudKind::Free, _) => Prompt::new("Guide crosshairs along cloud path", Accept::POINT).base_opt(self.pts.last().copied()),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
if self.asking_arc {
|
|
if let Input::Text(t) = &i {
|
|
let v = number(t).filter(|v| *v > 0.0).ok_or_else(|| other("Requires a positive value."))?;
|
|
self.arc_len = v;
|
|
s.doc_mut()?.header.set_f64("REVCLOUDARCLEN", v);
|
|
}
|
|
self.asking_arc = false;
|
|
return Ok(Step::Continue);
|
|
}
|
|
if self.asking_style {
|
|
if let Input::Keyword(k) | Input::Text(k) = &i {
|
|
self.calligraphy = k.to_ascii_lowercase().starts_with('c');
|
|
}
|
|
self.asking_style = false;
|
|
return Ok(Step::Continue);
|
|
}
|
|
if self.object {
|
|
let Input::Point(p) = i else { return if i == Input::Enter { Ok(Step::Cancel) } else { Ok(Step::Continue) } };
|
|
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
|
|
let pts = object_outline(s, h)?;
|
|
let vs = cloud_vertices(&pts, self.arc_len, false, false).ok_or_else(|| other("The object has no area."))?;
|
|
let nh = s.add_entity(cloud_entity(vs, self.calligraphy, self.arc_len))?;
|
|
let common = curves::entity(s, h)?.common;
|
|
let d = s.doc_mut()?;
|
|
d.modify_entity(nh, |e| e.common = common)?;
|
|
d.remove_entity(h);
|
|
s.echo("Revision cloud finished.");
|
|
return Ok(Step::Done);
|
|
}
|
|
match i {
|
|
Input::Keyword(k) => {
|
|
match k.as_str() {
|
|
"Arc length" => self.asking_arc = true,
|
|
"Object" => self.object = true,
|
|
"Rectangular" => self.kind = CloudKind::Rect,
|
|
"Polygonal" => self.kind = CloudKind::Poly,
|
|
"Freehand" => self.kind = CloudKind::Free,
|
|
"Style" => self.asking_style = true,
|
|
"Undo" => {
|
|
self.pts.pop();
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Point(p) => {
|
|
self.pts.push(p);
|
|
if self.kind == CloudKind::Rect && self.pts.len() == 2 {
|
|
return self.finish(s);
|
|
}
|
|
if self.pts.len() > MAX_GEN {
|
|
return self.finish(s);
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Enter if self.pts.is_empty() => {
|
|
self.object = true;
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Enter => self.finish(s),
|
|
_ => Ok(Step::Continue),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
if self.pts.is_empty() || self.object {
|
|
return Vec::new();
|
|
}
|
|
let b = self.boundary(Some(c));
|
|
match cloud_vertices(&b, self.arc_len, self.kind != CloudKind::Free, false) {
|
|
Some(vs) if vs.len() < 2000 => vec![lwpoly(vs, true)],
|
|
_ => {
|
|
let mut pts = self.pts.clone();
|
|
pts.push(c);
|
|
vec![lwpoly(pts.into_iter().map(PolyVertex::new).collect(), false)]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// WIPEOUT
|
|
// =====================================================================================
|
|
|
|
fn set_frames(s: &mut Session, mode: &str) -> Result<i64> {
|
|
let v = match mode.trim().to_ascii_lowercase().as_str() {
|
|
"on" | "1" => 1,
|
|
"off" | "0" => 0,
|
|
"display" | "d" | "2" | "display but not plot" => 2,
|
|
_ => return Err(other("Frames mode must be ON, OFF or Display.")),
|
|
};
|
|
s.doc_mut()?.header.set_i64("WIPEOUTFRAME", v);
|
|
Ok(v)
|
|
}
|
|
|
|
fn wipeout_from_polyline(s: &mut Session, h: Handle, erase: bool) -> Result<Handle> {
|
|
let e = curves::entity(s, h)?;
|
|
let EntityKind::LwPolyline(p) = &e.kind else { return Err(other("Select a closed polyline.")) };
|
|
if !p.closed || p.vertices.len() < 3 {
|
|
return Err(other("The polyline must be closed with at least three vertices."));
|
|
}
|
|
if p.vertices.iter().any(|v| v.bulge.abs() > 1e-12) {
|
|
return Err(other("Polylines with arc segments cannot be used for wipeouts."));
|
|
}
|
|
let nh = s.add_entity(EntityKind::Wipeout(Wipeout { boundary: p.vertices.iter().map(|v| v.p).collect() }))?;
|
|
if erase {
|
|
s.doc_mut()?.remove_entity(h);
|
|
}
|
|
Ok(nh)
|
|
}
|
|
|
|
fn run_wipeout(s: &mut Session, p: &Value) -> Result<Value> {
|
|
if let Some(f) = str_param(p, "frames") {
|
|
let v = set_frames(s, f)?;
|
|
return Ok(json!({ "WIPEOUTFRAME": v }));
|
|
}
|
|
if let Some(h) = curves::handle_param(p, "handle") {
|
|
return added(wipeout_from_polyline(s, h, bool_or(p, "erase", false))?);
|
|
}
|
|
let pts = points_param(p, "points").ok_or_else(|| bad("wipeout", "`points` (3 or more) or `handle` is required"))?;
|
|
if pts.len() < 3 || pts.len() > MAX_GEN || shoelace(&pts).abs() < 1e-12 {
|
|
return Err(bad("wipeout", "need 3+ points enclosing an area"));
|
|
}
|
|
added(s.add_entity(EntityKind::Wipeout(Wipeout { boundary: pts }))?)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct WipeoutM {
|
|
pts: Vec<Vec2>,
|
|
phase: u8, // 0 points, 1 select polyline, 2 erase?, 3 frames
|
|
poly: Option<Handle>,
|
|
}
|
|
|
|
impl Interactive for WipeoutM {
|
|
fn name(&self) -> &'static str {
|
|
"WIPEOUT"
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
match (self.phase, self.pts.len()) {
|
|
(1, _) => Prompt::new("Select a closed polyline", Accept::POINT),
|
|
(2, _) => Prompt::new("Erase polyline?", curves::KW).kw(&["Yes", "No"]).default("No"),
|
|
(3, _) => Prompt::new("Enter mode", curves::KW).kw(&["ON", "OFF", "Display but not plot"]).default("ON"),
|
|
(_, 0) => Prompt::new("Specify first point", Accept::POINT).kw(&["Frames", "Polyline"]).default("Polyline"),
|
|
(_, 1) => Prompt::new("Specify next point", Accept::POINT).base_opt(self.pts.last().copied()),
|
|
(_, _) => Prompt::new("Specify next point", Accept::POINT).kw(&["Undo"]).base_opt(self.pts.last().copied()),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
match (self.phase, i) {
|
|
(1, Input::Point(p)) => {
|
|
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
|
|
let e = curves::entity(s, h)?;
|
|
if !matches!(&e.kind, EntityKind::LwPolyline(pl) if pl.closed) {
|
|
return Err(other("Select a closed polyline."));
|
|
}
|
|
self.poly = Some(h);
|
|
self.phase = 2;
|
|
Ok(Step::Continue)
|
|
}
|
|
(2, inp @ (Input::Keyword(_) | Input::Text(_) | Input::Enter)) => {
|
|
let erase = matches!(&inp, Input::Keyword(k) | Input::Text(k) if k.to_ascii_lowercase().starts_with('y'));
|
|
let h = self.poly.ok_or_else(|| other("No polyline."))?;
|
|
wipeout_from_polyline(s, h, erase)?;
|
|
Ok(Step::Done)
|
|
}
|
|
(3, inp @ (Input::Keyword(_) | Input::Text(_) | Input::Enter)) => {
|
|
let m = match &inp {
|
|
Input::Keyword(k) | Input::Text(k) => k.clone(),
|
|
_ => "on".into(),
|
|
};
|
|
let m = if m.to_ascii_lowercase().starts_with('d') { "display".to_string() } else { m };
|
|
set_frames(s, &m)?;
|
|
Ok(Step::Done)
|
|
}
|
|
(0, Input::Keyword(k)) => {
|
|
match k.as_str() {
|
|
"Frames" => self.phase = 3,
|
|
"Polyline" => self.phase = 1,
|
|
"Undo" => {
|
|
self.pts.pop();
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
(0, Input::Point(p)) => {
|
|
if self.pts.len() < MAX_GEN {
|
|
self.pts.push(p);
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
(0, Input::Enter) if self.pts.is_empty() => {
|
|
self.phase = 1;
|
|
Ok(Step::Continue)
|
|
}
|
|
(0, Input::Enter) => {
|
|
if self.pts.len() < 3 || shoelace(&self.pts).abs() < 1e-12 {
|
|
return Err(other("A wipeout needs three or more points enclosing an area."));
|
|
}
|
|
s.add_entity(EntityKind::Wipeout(Wipeout { boundary: self.pts.clone() }))?;
|
|
Ok(Step::Done)
|
|
}
|
|
(_, Input::Enter) => Ok(Step::Cancel),
|
|
_ => Ok(Step::Continue),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
if self.phase != 0 || self.pts.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let mut pts = self.pts.clone();
|
|
pts.push(c);
|
|
vec![lwpoly(pts.into_iter().map(PolyVertex::new).collect(), true)]
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// XLINE with Hor/Ver/Ang/Bisect/Offset
|
|
// =====================================================================================
|
|
|
|
fn add_xline(s: &mut Session, base: Vec2, dir: Vec2) -> Result<Handle> {
|
|
let d = dir.normalized();
|
|
if d == Vec2::ZERO || !base.is_finite() {
|
|
return Err(other("Invalid direction."));
|
|
}
|
|
s.add_entity(EntityKind::XLine(RayLine { base: v3(base), dir: v3(d) }))
|
|
}
|
|
|
|
/// A line-like object for XLINE Offset/Reference: (base, direction). For a polyline, the
|
|
/// segment nearest `pick` (the first without one), which must be straight.
|
|
fn linear_of(s: &Session, h: Handle, pick: Option<Vec2>) -> Result<(Vec2, Vec2)> {
|
|
let e = curves::entity(s, h)?;
|
|
match &e.kind {
|
|
EntityKind::Line(l) => Ok((l.a.xy(), (l.b.xy() - l.a.xy()).normalized())),
|
|
EntityKind::XLine(r) | EntityKind::Ray(r) => Ok((r.base.xy(), r.dir.xy().normalized())),
|
|
EntityKind::LwPolyline(p) => {
|
|
let segs = Polyline { vertices: p.vertices.clone(), closed: p.closed }.segments();
|
|
let seg = match pick {
|
|
Some(q) => segs.iter().min_by(|a, b| a.closest(q).dist(q).total_cmp(&b.closest(q).dist(q))),
|
|
None => segs.first(),
|
|
};
|
|
match seg {
|
|
Some(cadcraft_geom::Segment::Line(l)) => Ok((l.a, l.dir())),
|
|
_ => Err(other("Select a line object.")),
|
|
}
|
|
}
|
|
_ => Err(other("Select a line object.")),
|
|
}
|
|
}
|
|
|
|
fn bisect_dir(v: Vec2, a: Vec2, b: Vec2) -> Option<Vec2> {
|
|
let u1 = (a - v).normalized();
|
|
let u2 = (b - v).normalized();
|
|
if u1 == Vec2::ZERO || u2 == Vec2::ZERO {
|
|
return None;
|
|
}
|
|
let s = u1 + u2;
|
|
if s.len() < 1e-12 { Some(u1.perp()) } else { Some(s.normalized()) }
|
|
}
|
|
|
|
fn offset_xline(base: Vec2, dir: Vec2, dist: f64, side: Vec2) -> (Vec2, Vec2) {
|
|
let n = dir.perp();
|
|
let sgn = if n.dot(side - base) >= 0.0 { 1.0 } else { -1.0 };
|
|
(base + n * (dist * sgn), dir)
|
|
}
|
|
|
|
pub(crate) fn run_xline2(s: &mut Session, p: &Value) -> Result<Value> {
|
|
if let (Some(v), Some(a), Some(b)) = (point_param(p, "vertex"), point_param(p, "start"), point_param(p, "end")) {
|
|
let d = bisect_dir(v, a, b).ok_or_else(|| bad("xline", "degenerate angle"))?;
|
|
return added(add_xline(s, v, d)?);
|
|
}
|
|
if let Some(h) = curves::handle_param(p, "handle") {
|
|
let (base, dir) = linear_of(s, h, None)?;
|
|
let (nb, nd) = if let Some(t) = point_param(p, "through") {
|
|
(t, dir)
|
|
} else {
|
|
let dist = f64_req("xline", p, "distance")?;
|
|
let side = point_req("xline", p, "side")?;
|
|
offset_xline(base, dir, dist, side)
|
|
};
|
|
return added(add_xline(s, nb, nd)?);
|
|
}
|
|
let b = point_param(p, "base").or_else(|| point_param(p, "through")).ok_or_else(|| bad("xline", "`base` is required"))?;
|
|
let dir = if bool_or(p, "hor", false) {
|
|
Vec2::X
|
|
} else if bool_or(p, "ver", false) {
|
|
Vec2::Y
|
|
} else if let (Some(t), Some(_)) = (point_param(p, "through"), point_param(p, "base")) {
|
|
t - b
|
|
} else {
|
|
Vec2::from_angle(f64_req("xline", p, "angle")?.to_radians())
|
|
};
|
|
if dir.len() < 1e-12 {
|
|
return Err(bad("xline", "through point equals base"));
|
|
}
|
|
added(add_xline(s, b, dir)?)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
enum XMode {
|
|
#[default]
|
|
Two,
|
|
Fixed(f64),
|
|
AngAsk,
|
|
RefSelect,
|
|
RefAngle(f64),
|
|
Bisect,
|
|
OffDist,
|
|
OffSelect,
|
|
OffSide,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct XlineM2 {
|
|
mode: XMode,
|
|
base: Option<Vec2>,
|
|
bis: Vec<Vec2>,
|
|
offset: Option<f64>, // None = Through
|
|
line: Option<(Vec2, Vec2)>,
|
|
}
|
|
|
|
impl Interactive for XlineM2 {
|
|
fn name(&self) -> &'static str {
|
|
"XLINE"
|
|
}
|
|
fn prompt(&self, s: &Session) -> Prompt {
|
|
match self.mode {
|
|
XMode::Two => match self.base {
|
|
None => Prompt::new("Specify a point", Accept::POINT).kw(&["Hor", "Ver", "Ang", "Bisect", "Offset"]),
|
|
Some(b) => Prompt::new("Specify through point", Accept::POINT).base(b),
|
|
},
|
|
XMode::Fixed(_) => Prompt::new("Specify through point", Accept::POINT),
|
|
XMode::AngAsk => Prompt::new("Enter angle of xline (0) or", Accept::NUMBER).kw(&["Reference"]),
|
|
XMode::RefSelect => Prompt::new("Select a line object", Accept::POINT),
|
|
XMode::RefAngle(_) => Prompt::new("Enter angle of xline", Accept::NUMBER).default("0"),
|
|
XMode::Bisect => match self.bis.len() {
|
|
0 => Prompt::new("Specify angle vertex point", Accept::POINT),
|
|
1 => Prompt::new("Specify angle start point", Accept::POINT).base_opt(self.bis.first().copied()),
|
|
_ => Prompt::new("Specify angle end point", Accept::POINT).base_opt(self.bis.first().copied()),
|
|
},
|
|
XMode::OffDist => {
|
|
let d = s.doc().map(|d| d.header.f64("OFFSETDIST", 1.0)).unwrap_or(1.0);
|
|
Prompt::new("Specify offset distance or", Accept::NUMBER).kw(&["Through"]).default(if d < 0.0 {
|
|
"Through".into()
|
|
} else {
|
|
format!("{d:.4}")
|
|
})
|
|
}
|
|
XMode::OffSelect => Prompt::new("Select a line object", Accept::POINT),
|
|
XMode::OffSide => Prompt::new(if self.offset.is_some() { "Specify side to offset" } else { "Specify through point" }, Accept::POINT),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
match (self.mode, i) {
|
|
(_, Input::Enter) if matches!(self.mode, XMode::OffDist) => {
|
|
let d = s.doc()?.header.f64("OFFSETDIST", 1.0);
|
|
self.offset = (d >= 0.0).then_some(d);
|
|
self.mode = XMode::OffSelect;
|
|
Ok(Step::Continue)
|
|
}
|
|
(_, Input::Enter) => Ok(Step::Done),
|
|
(XMode::Two, Input::Keyword(k)) => {
|
|
self.mode = match k.as_str() {
|
|
"Hor" => XMode::Fixed(0.0),
|
|
"Ver" => XMode::Fixed(std::f64::consts::FRAC_PI_2),
|
|
"Ang" => XMode::AngAsk,
|
|
"Bisect" => XMode::Bisect,
|
|
_ => XMode::OffDist,
|
|
};
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::Two, Input::Point(p)) => {
|
|
match self.base {
|
|
None => self.base = Some(p),
|
|
Some(b) => {
|
|
if !b.near(p, 1e-12) {
|
|
add_xline(s, b, p - b)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::Fixed(a), Input::Point(p)) => {
|
|
add_xline(s, p, Vec2::from_angle(a))?;
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::AngAsk, Input::Keyword(_)) => {
|
|
self.mode = XMode::RefSelect;
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::AngAsk, Input::Text(t)) => {
|
|
let a = s.angle_settings().direction(&t).ok_or_else(|| other("Requires a valid angle."))?;
|
|
self.mode = XMode::Fixed(a);
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::RefSelect, Input::Point(p)) => {
|
|
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
|
|
let (_, d) = linear_of(s, h, Some(p))?;
|
|
self.mode = XMode::RefAngle(d.angle());
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::RefAngle(base), Input::Text(t)) => {
|
|
let a = s.angle_settings().rotation(&t).ok_or_else(|| other("Requires a valid angle."))?;
|
|
self.mode = XMode::Fixed(base + a);
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::Bisect, Input::Point(p)) => {
|
|
if self.bis.len() < 2 {
|
|
self.bis.push(p);
|
|
} else if let (Some(v), Some(a)) = (self.bis.first().copied(), self.bis.get(1).copied()) {
|
|
let d = bisect_dir(v, a, p).ok_or_else(|| other("Invalid angle."))?;
|
|
add_xline(s, v, d)?;
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::OffDist, Input::Keyword(_)) => {
|
|
self.offset = None;
|
|
s.doc_mut()?.header.set_f64("OFFSETDIST", -1.0);
|
|
self.mode = XMode::OffSelect;
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::OffDist, Input::Text(t)) => {
|
|
let d = number(&t).filter(|d| *d >= 0.0).ok_or_else(|| other("Requires a non-negative distance."))?;
|
|
self.offset = Some(d);
|
|
s.doc_mut()?.header.set_f64("OFFSETDIST", d);
|
|
self.mode = XMode::OffSelect;
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::OffSelect, Input::Point(p)) => {
|
|
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
|
|
self.line = Some(linear_of(s, h, Some(p))?);
|
|
self.mode = XMode::OffSide;
|
|
Ok(Step::Continue)
|
|
}
|
|
(XMode::OffSide, Input::Point(p)) => {
|
|
let (b, d) = self.line.ok_or_else(|| other("No line selected."))?;
|
|
let (nb, nd) = match self.offset {
|
|
Some(dist) => offset_xline(b, d, dist, p),
|
|
None => (p, d),
|
|
};
|
|
add_xline(s, nb, nd)?;
|
|
self.mode = XMode::OffSelect;
|
|
Ok(Step::Continue)
|
|
}
|
|
_ => Err(other("Invalid input.")),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
let xl = |b: Vec2, d: Vec2| {
|
|
let d = d.normalized();
|
|
(d != Vec2::ZERO).then(|| EntityKind::XLine(RayLine { base: v3(b), dir: v3(d) }))
|
|
};
|
|
match self.mode {
|
|
XMode::Two => self.base.and_then(|b| xl(b, c - b)).into_iter().collect(),
|
|
XMode::Fixed(a) => xl(c, Vec2::from_angle(a)).into_iter().collect(),
|
|
XMode::Bisect => match self.bis.as_slice() {
|
|
[v, a] => bisect_dir(*v, *a, c).and_then(|d| xl(*v, d)).into_iter().collect(),
|
|
_ => Vec::new(),
|
|
},
|
|
XMode::OffSide => self
|
|
.line
|
|
.and_then(|(b, d)| {
|
|
let (nb, nd) = match self.offset {
|
|
Some(dist) => offset_xline(b, d, dist, c),
|
|
None => (c, d),
|
|
};
|
|
xl(nb, nd)
|
|
})
|
|
.into_iter()
|
|
.collect(),
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// RECTANG with Area / Dimensions / Rotation / Width / Elevation / Thickness
|
|
// =====================================================================================
|
|
|
|
fn rect_kind(first: Vec2, w: f64, h: f64, rot: f64, fillet: f64, chamfer: (f64, f64), width: f64, elevation: f64) -> EntityKind {
|
|
let local = rect_with_corners(Vec2::ZERO, Vec2::new(w, h), fillet, chamfer);
|
|
let vs = local.into_iter().map(|v| PolyVertex { p: first + v.p.rotate(rot), ..v }).collect();
|
|
let mut k = lwpoly(vs, true);
|
|
if let EntityKind::LwPolyline(pl) = &mut k {
|
|
pl.const_width = width.max(0.0);
|
|
pl.elevation = elevation;
|
|
}
|
|
k
|
|
}
|
|
|
|
pub(crate) fn run_rectang2(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let a = point_req("rectang", p, "p1")?;
|
|
let rot = f64_or(p, "rotation", 0.0).to_radians();
|
|
let ch = p
|
|
.get("chamfer")
|
|
.and_then(Value::as_array)
|
|
.map(|c| (c.first().and_then(Value::as_f64).unwrap_or(0.0), c.get(1).and_then(Value::as_f64).unwrap_or(0.0)))
|
|
.map(|(x, y)| (if x.is_finite() { x } else { 0.0 }, if y.is_finite() { y } else { 0.0 }))
|
|
.unwrap_or((0.0, 0.0));
|
|
if ch.0 < 0.0 || ch.1 < 0.0 {
|
|
return Err(bad("rectang", "`chamfer` distances must be zero or positive"));
|
|
}
|
|
let fillet = size_param("rectang", p, "fillet", true)?.unwrap_or(0.0);
|
|
let width = size_param("rectang", p, "width", true)?.unwrap_or(0.0);
|
|
let (w, h) = if let Some(d) = p.get("dimensions").and_then(Value::as_array) {
|
|
let l = d.first().and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad("rectang", "dimensions: [length, width]"))?;
|
|
let wd = d.get(1).and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad("rectang", "dimensions: [length, width]"))?;
|
|
let (sx, sy) = match point_param(p, "p2") {
|
|
Some(q) => {
|
|
let loc = (q - a).rotate(-rot);
|
|
(if loc.x < 0.0 { -1.0 } else { 1.0 }, if loc.y < 0.0 { -1.0 } else { 1.0 })
|
|
}
|
|
None => (1.0, 1.0),
|
|
};
|
|
(l.abs() * sx, wd.abs() * sy)
|
|
} else if let Some(area) = p.get("area").and_then(Value::as_f64).filter(|v| v.is_finite()) {
|
|
if area <= 0.0 {
|
|
return Err(bad("rectang", "area must be positive"));
|
|
}
|
|
match (p.get("length").and_then(Value::as_f64), p.get("breadth").and_then(Value::as_f64)) {
|
|
(Some(l), _) if l.is_finite() && l > 0.0 => (l, area / l),
|
|
(_, Some(b)) if b.is_finite() && b > 0.0 => (area / b, b),
|
|
_ => return Err(bad("rectang", "area needs `length` or `breadth`")),
|
|
}
|
|
} else {
|
|
let b = point_req("rectang", p, "p2")?;
|
|
let loc = (b - a).rotate(-rot);
|
|
(loc.x, loc.y)
|
|
};
|
|
if w.abs() < 1e-12 || h.abs() < 1e-12 || !w.is_finite() || !h.is_finite() {
|
|
return Err(bad("rectang", "rectangle has no area"));
|
|
}
|
|
let k = rect_kind(a, w, h, rot, fillet, ch, width, f64_or(p, "elevation", 0.0));
|
|
let hd = s.add_entity(k)?;
|
|
let th = f64_or(p, "thickness", 0.0);
|
|
if th != 0.0 {
|
|
s.doc_mut()?.modify_entity(hd, |e| e.common.thickness = th)?;
|
|
}
|
|
added(hd)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
enum RAsk {
|
|
Chamfer1,
|
|
Chamfer2,
|
|
Fillet,
|
|
Width,
|
|
Elevation,
|
|
Thickness,
|
|
Area,
|
|
AreaBasis,
|
|
AreaLength,
|
|
AreaWidth,
|
|
DimL,
|
|
DimW,
|
|
Rotation,
|
|
RotPick1,
|
|
RotPick2(Vec2),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct RectM2 {
|
|
first: Option<Vec2>,
|
|
fillet: f64,
|
|
chamfer: (f64, f64),
|
|
width: f64,
|
|
elevation: f64,
|
|
thickness: f64,
|
|
rotation: f64,
|
|
dims: Option<(f64, f64)>,
|
|
area: f64,
|
|
asking: Option<RAsk>,
|
|
}
|
|
|
|
impl RectM2 {
|
|
fn make(&self, s: &mut Session, w: f64, h: f64) -> Result<Step> {
|
|
let Some(a) = self.first else { return Ok(Step::Cancel) };
|
|
if w.abs() < 1e-12 || h.abs() < 1e-12 {
|
|
return Err(other("The rectangle has no area."));
|
|
}
|
|
let hd = s.add_entity(rect_kind(a, w, h, self.rotation, self.fillet, self.chamfer, self.width, self.elevation))?;
|
|
if self.thickness != 0.0 {
|
|
let t = self.thickness;
|
|
s.doc_mut()?.modify_entity(hd, |e| e.common.thickness = t)?;
|
|
}
|
|
Ok(Step::Done)
|
|
}
|
|
fn other_corner(&self, p: Vec2) -> Option<(f64, f64)> {
|
|
let a = self.first?;
|
|
let loc = (p - a).rotate(-self.rotation);
|
|
Some(match self.dims {
|
|
Some((l, w)) => (if loc.x < 0.0 { -l } else { l }, if loc.y < 0.0 { -w } else { w }),
|
|
None => (loc.x, loc.y),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Interactive for RectM2 {
|
|
fn name(&self) -> &'static str {
|
|
"RECTANG"
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
let num = |m: &str, d: f64| Prompt::new(m, Accept::NUMBER).default(format!("{d:.4}"));
|
|
if let Some(a) = self.asking {
|
|
return match a {
|
|
RAsk::Chamfer1 => num("Specify first chamfer distance for rectangles", self.chamfer.0),
|
|
RAsk::Chamfer2 => num("Specify second chamfer distance for rectangles", self.chamfer.1),
|
|
RAsk::Fillet => num("Specify fillet radius for rectangles", self.fillet),
|
|
RAsk::Width => num("Specify line width for rectangles", self.width),
|
|
RAsk::Elevation => num("Specify the elevation for rectangles", self.elevation),
|
|
RAsk::Thickness => num("Specify thickness for rectangles", self.thickness),
|
|
RAsk::Area => num("Enter area of rectangle in current units", if self.area > 0.0 { self.area } else { 100.0 }),
|
|
RAsk::AreaBasis => Prompt::new("Calculate rectangle dimensions based on", curves::KW).kw(&["Length", "Width"]).default("Length"),
|
|
RAsk::AreaLength => num("Enter rectangle length", 10.0),
|
|
RAsk::AreaWidth => num("Enter rectangle width", 10.0),
|
|
RAsk::DimL => num("Specify length for rectangles", self.dims.map(|d| d.0).unwrap_or(10.0)),
|
|
RAsk::DimW => num("Specify width for rectangles", self.dims.map(|d| d.1).unwrap_or(10.0)),
|
|
RAsk::Rotation => Prompt::new("Specify rotation angle or", Accept::POINT_OR_NUMBER)
|
|
.kw(&["Pick points"])
|
|
.default(format!("{:.0}", self.rotation.to_degrees()))
|
|
.base_opt(self.first),
|
|
RAsk::RotPick1 => Prompt::new("Specify first point", Accept::POINT),
|
|
RAsk::RotPick2(b) => Prompt::new("Specify second point", Accept::POINT).base(b),
|
|
};
|
|
}
|
|
match self.first {
|
|
None => Prompt::new("Specify first corner point", Accept::POINT).kw(&["Chamfer", "Elevation", "Fillet", "Thickness", "Width"]),
|
|
Some(p) => Prompt::new("Specify other corner point", Accept::POINT).kw(&["Area", "Dimensions", "Rotation"]).corner(p),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
if let Some(a) = self.asking {
|
|
let val = |i: &Input, default: f64| -> Result<f64> {
|
|
match i {
|
|
Input::Text(t) => number(t).ok_or_else(|| other("Requires a number.")),
|
|
Input::Enter => Ok(default),
|
|
_ => Err(other("Requires a number.")),
|
|
}
|
|
};
|
|
self.asking = None;
|
|
match a {
|
|
RAsk::Chamfer1 => {
|
|
self.chamfer.0 = val(&i, self.chamfer.0)?.max(0.0);
|
|
self.chamfer.1 = self.chamfer.0;
|
|
self.asking = Some(RAsk::Chamfer2);
|
|
}
|
|
RAsk::Chamfer2 => self.chamfer.1 = val(&i, self.chamfer.1)?.max(0.0),
|
|
RAsk::Fillet => self.fillet = val(&i, self.fillet)?.max(0.0),
|
|
RAsk::Width => self.width = val(&i, self.width)?.max(0.0),
|
|
RAsk::Elevation => self.elevation = val(&i, self.elevation)?,
|
|
RAsk::Thickness => self.thickness = val(&i, self.thickness)?,
|
|
RAsk::Area => {
|
|
let v = val(&i, if self.area > 0.0 { self.area } else { 100.0 })?;
|
|
if v <= 0.0 {
|
|
self.asking = Some(RAsk::Area);
|
|
return Err(other("Value must be positive."));
|
|
}
|
|
self.area = v;
|
|
self.asking = Some(RAsk::AreaBasis);
|
|
}
|
|
RAsk::AreaBasis => {
|
|
let w = matches!(&i, Input::Keyword(k) | Input::Text(k) if k.to_ascii_lowercase().starts_with('w'));
|
|
self.asking = Some(if w { RAsk::AreaWidth } else { RAsk::AreaLength });
|
|
}
|
|
RAsk::AreaLength | RAsk::AreaWidth => {
|
|
let v = val(&i, 10.0)?;
|
|
if v <= 0.0 {
|
|
self.asking = Some(a);
|
|
return Err(other("Value must be positive."));
|
|
}
|
|
let (w, h) = if a == RAsk::AreaLength { (v, self.area / v) } else { (self.area / v, v) };
|
|
return self.make(s, w, h);
|
|
}
|
|
RAsk::DimL => {
|
|
let l = val(&i, self.dims.map(|d| d.0).unwrap_or(10.0))?.abs();
|
|
self.dims = Some((l, self.dims.map(|d| d.1).unwrap_or(10.0)));
|
|
self.asking = Some(RAsk::DimW);
|
|
}
|
|
RAsk::DimW => {
|
|
let w = val(&i, self.dims.map(|d| d.1).unwrap_or(10.0))?.abs();
|
|
self.dims = Some((self.dims.map(|d| d.0).unwrap_or(10.0), w));
|
|
}
|
|
RAsk::Rotation => match i {
|
|
Input::Keyword(_) => self.asking = Some(RAsk::RotPick1),
|
|
Input::Point(p) => self.rotation = self.first.map(|f| f.angle_to(p)).unwrap_or(0.0),
|
|
Input::Text(t) => self.rotation = s.angle_settings().direction(&t).ok_or_else(|| other("Requires a valid angle."))?,
|
|
_ => {}
|
|
},
|
|
RAsk::RotPick1 => {
|
|
if let Input::Point(p) = i {
|
|
self.asking = Some(RAsk::RotPick2(p));
|
|
}
|
|
}
|
|
RAsk::RotPick2(b) => {
|
|
if let Input::Point(p) = i {
|
|
self.rotation = b.angle_to(p);
|
|
}
|
|
}
|
|
}
|
|
return Ok(Step::Continue);
|
|
}
|
|
match i {
|
|
Input::Keyword(k) => {
|
|
self.asking = Some(match k.as_str() {
|
|
"Chamfer" => RAsk::Chamfer1,
|
|
"Elevation" => RAsk::Elevation,
|
|
"Fillet" => RAsk::Fillet,
|
|
"Thickness" => RAsk::Thickness,
|
|
"Width" => RAsk::Width,
|
|
"Area" => RAsk::Area,
|
|
"Dimensions" => RAsk::DimL,
|
|
_ => RAsk::Rotation,
|
|
});
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Point(p) => match self.first {
|
|
None => {
|
|
self.first = Some(p);
|
|
Ok(Step::Continue)
|
|
}
|
|
Some(_) => {
|
|
let (w, h) = self.other_corner(p).ok_or_else(|| other("No first corner."))?;
|
|
self.make(s, w, h)
|
|
}
|
|
},
|
|
Input::Enter => Ok(Step::Cancel),
|
|
_ => Ok(Step::Continue),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
if self.asking.is_some() {
|
|
return Vec::new();
|
|
}
|
|
match (self.first, self.other_corner(c)) {
|
|
(Some(a), Some((w, h))) if w.abs() > 1e-12 && h.abs() > 1e-12 => {
|
|
vec![rect_kind(a, w, h, self.rotation, self.fillet, self.chamfer, self.width, self.elevation)]
|
|
}
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// 3DPOLY
|
|
// =====================================================================================
|
|
|
|
fn point3_value(v: &Value) -> Option<Vec3> {
|
|
let a = v.as_array()?;
|
|
let x = a.first()?.as_f64()?;
|
|
let y = a.get(1)?.as_f64()?;
|
|
let z = a.get(2).and_then(Value::as_f64).unwrap_or(0.0);
|
|
let p = Vec3::new(x, y, z);
|
|
p.is_finite().then_some(p)
|
|
}
|
|
|
|
fn run_3dpoly(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let pts: Vec<Vec3> = p.get("points").and_then(Value::as_array).map(|a| a.iter().filter_map(point3_value).collect()).unwrap_or_default();
|
|
if pts.len() < 2 || pts.len() > MAX_GEN {
|
|
return Err(bad("3dpoly", "`points` needs 2 or more [x,y,z] points"));
|
|
}
|
|
added(s.add_entity(EntityKind::Polyline3d(Polyline3d { points: pts, closed: bool_or(p, "closed", false) }))?)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Poly3dM {
|
|
pts: Vec<Vec2>,
|
|
handle: Option<Handle>,
|
|
}
|
|
|
|
impl Poly3dM {
|
|
fn sync(&mut self, s: &mut Session, closed: bool) -> Result<()> {
|
|
let k = EntityKind::Polyline3d(Polyline3d { points: self.pts.iter().map(|p| v3(*p)).collect(), closed });
|
|
match (self.handle, self.pts.len() >= 2) {
|
|
(Some(h), true) => s.doc_mut()?.modify_entity(h, |e| e.kind = k)?,
|
|
(None, true) => self.handle = Some(s.add_entity(k)?),
|
|
(Some(h), false) => {
|
|
s.doc_mut()?.remove_entity(h);
|
|
self.handle = None;
|
|
}
|
|
(None, false) => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Interactive for Poly3dM {
|
|
fn name(&self) -> &'static str {
|
|
"3DPOLY"
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
match self.pts.len() {
|
|
0 => Prompt::new("Specify start point of polyline", Accept::POINT),
|
|
1 | 2 => Prompt::new("Specify endpoint of line", Accept::POINT).kw(&["Undo"]).base_opt(self.pts.last().copied()),
|
|
_ => Prompt::new("Specify endpoint of line", Accept::POINT).kw(&["Close", "Undo"]).base_opt(self.pts.last().copied()),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
match i {
|
|
Input::Point(p) => {
|
|
if self.pts.last().is_none_or(|l| !l.near(p, 1e-12)) && self.pts.len() < MAX_GEN {
|
|
self.pts.push(p);
|
|
self.sync(s, false)?;
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Keyword(k) if k == "Undo" => {
|
|
self.pts.pop();
|
|
self.sync(s, false)?;
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Keyword(k) if k == "Close" => {
|
|
self.sync(s, true)?;
|
|
Ok(Step::Done)
|
|
}
|
|
Input::Enter => Ok(Step::Done),
|
|
_ => Ok(Step::Continue),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
self.pts.last().map(|l| vec![line(*l, c)]).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// HELIX (3D polyline approximation)
|
|
// =====================================================================================
|
|
|
|
/// A helix entity (base centre `c`, radii, height, turns, twist, start angle).
|
|
fn helix_kind(c: Vec2, rb: f64, rt: f64, height: f64, turns: f64, ccw: bool, a0: f64) -> EntityKind {
|
|
let h = Helix { center: c.to3(0.0), base_radius: rb, top_radius: rt, height, turns, start_angle: a0, ccw, constrain: HelixConstrain::Height };
|
|
EntityKind::Helix(h.sanitized())
|
|
}
|
|
|
|
fn run_helix(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let c = point_req("helix", p, "center")?;
|
|
let rb = f64_req("helix", p, "baseRadius")?;
|
|
let rt = f64_or(p, "topRadius", rb);
|
|
let turns = f64_or(p, "turns", 3.0);
|
|
if !(rb > 0.0 && rt >= 0.0 && turns > 0.0 && turns <= 500.0) {
|
|
return Err(bad("helix", "radii must be positive and turns in (0, 500]"));
|
|
}
|
|
let height = f64_or(p, "height", 1.0);
|
|
if height.abs() > 1e12 || rb > 1e12 || rt > 1e12 {
|
|
return Err(bad("helix", "sizes are out of range"));
|
|
}
|
|
added(s.add_entity(helix_kind(c, rb, rt, height, turns, bool_or(p, "ccw", true), f64_or(p, "startAngle", 0.0).to_radians()))?)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct HelixM {
|
|
center: Option<Vec2>,
|
|
rb: Option<f64>,
|
|
rt: Option<f64>,
|
|
turns: Option<f64>,
|
|
cw: bool,
|
|
diameter: bool,
|
|
asking: u8, // 0 main, 1 turns, 2 twist, 3 turn height
|
|
start: Option<f64>,
|
|
}
|
|
|
|
impl HelixM {
|
|
fn turns(&self) -> f64 {
|
|
self.turns.unwrap_or(3.0)
|
|
}
|
|
fn create(&self, s: &mut Session, height: f64) -> Result<Step> {
|
|
let (Some(c), Some(rb)) = (self.center, self.rb) else { return Ok(Step::Cancel) };
|
|
let rt = self.rt.unwrap_or(rb);
|
|
if !height.is_finite() || height.abs() > 1e12 {
|
|
return Err(other("Requires a distance."));
|
|
}
|
|
s.add_entity(helix_kind(c, rb, rt, height, self.turns(), !self.cw, self.start.unwrap_or(0.0)))?;
|
|
Ok(Step::Done)
|
|
}
|
|
}
|
|
|
|
impl Interactive for HelixM {
|
|
fn name(&self) -> &'static str {
|
|
"HELIX"
|
|
}
|
|
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
|
s.echo(format!("Number of turns = {:.4} Twist=CCW", self.turns()));
|
|
Ok(Step::Continue)
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
match self.asking {
|
|
1 => return Prompt::new("Enter number of turns", Accept::NUMBER).default(format!("{:.4}", self.turns())),
|
|
2 => return Prompt::new("Enter twist direction of helix", curves::KW).kw(&["CW", "CCW"]).default("CCW"),
|
|
3 => return Prompt::new("Specify distance between turns", Accept::NUMBER),
|
|
_ => {}
|
|
}
|
|
match (self.center, self.rb, self.rt) {
|
|
(None, ..) => Prompt::new("Specify center point of base", Accept::POINT),
|
|
(Some(c), None, _) => {
|
|
Prompt::new(if self.diameter { "Specify base diameter" } else { "Specify base radius or" }, Accept::POINT_OR_NUMBER)
|
|
.kw(if self.diameter { &[] } else { &["Diameter"] })
|
|
.default("1.0000")
|
|
.base(c)
|
|
}
|
|
(Some(c), Some(rb), None) => {
|
|
Prompt::new(if self.diameter { "Specify top diameter" } else { "Specify top radius or" }, Accept::POINT_OR_NUMBER)
|
|
.kw(if self.diameter { &[] } else { &["Diameter"] })
|
|
.default(format!("{rb:.4}"))
|
|
.base(c)
|
|
}
|
|
(Some(c), Some(_), Some(_)) => Prompt::new("Specify helix height or", Accept::POINT_OR_NUMBER)
|
|
.kw(&["Axis endpoint", "Turns", "turn Height", "tWist"])
|
|
.default("1.0000")
|
|
.base(c),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
match self.asking {
|
|
1 => {
|
|
if let Input::Text(t) = &i {
|
|
let v = number(t).filter(|v| *v > 0.0 && *v <= 500.0).ok_or_else(|| other("Turns must be in (0, 500]."))?;
|
|
self.turns = Some(v);
|
|
}
|
|
self.asking = 0;
|
|
return Ok(Step::Continue);
|
|
}
|
|
2 => {
|
|
if let Input::Keyword(k) | Input::Text(k) = &i {
|
|
self.cw = k.eq_ignore_ascii_case("cw");
|
|
}
|
|
self.asking = 0;
|
|
return Ok(Step::Continue);
|
|
}
|
|
3 => {
|
|
self.asking = 0;
|
|
if let Input::Text(t) = &i {
|
|
let v = number(t).ok_or_else(|| other("Requires a distance."))?;
|
|
return self.create(s, v * self.turns());
|
|
}
|
|
return Ok(Step::Continue);
|
|
}
|
|
_ => {}
|
|
}
|
|
let num = |i: &Input, base: Option<Vec2>| -> Option<f64> {
|
|
match i {
|
|
Input::Point(p) => base.map(|b| b.dist(*p)),
|
|
Input::Text(t) => number(t),
|
|
_ => None,
|
|
}
|
|
};
|
|
match (self.center, self.rb, self.rt, &i) {
|
|
(None, _, _, Input::Point(p)) => {
|
|
self.center = Some(*p);
|
|
Ok(Step::Continue)
|
|
}
|
|
(Some(_), None | Some(_), _, Input::Keyword(k)) if k == "Diameter" => {
|
|
self.diameter = true;
|
|
Ok(Step::Continue)
|
|
}
|
|
(Some(c), None, _, _) => {
|
|
let v = match &i {
|
|
Input::Enter => Some(1.0),
|
|
_ => num(&i, Some(c)),
|
|
};
|
|
let v = v.filter(|v| *v > 0.0).ok_or_else(|| other("Requires a positive distance."))?;
|
|
if let Input::Point(p) = &i {
|
|
self.start = Some(c.angle_to(*p));
|
|
}
|
|
self.rb = Some(if self.diameter { v / 2.0 } else { v });
|
|
self.diameter = false;
|
|
Ok(Step::Continue)
|
|
}
|
|
(Some(c), Some(rb), None, _) => {
|
|
let v = match &i {
|
|
Input::Enter => Some(if self.diameter { rb * 2.0 } else { rb }),
|
|
_ => num(&i, Some(c)),
|
|
};
|
|
let v = v.filter(|v| *v >= 0.0).ok_or_else(|| other("Requires a distance."))?;
|
|
self.rt = Some(if self.diameter { v / 2.0 } else { v });
|
|
Ok(Step::Continue)
|
|
}
|
|
(Some(c), Some(_), Some(_), _) => match &i {
|
|
Input::Keyword(k) => {
|
|
self.asking = match k.as_str() {
|
|
"Turns" => 1,
|
|
"tWist" => 2,
|
|
"turn Height" => 3,
|
|
_ => 0,
|
|
};
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Enter => self.create(s, 1.0),
|
|
_ => {
|
|
let h = num(&i, Some(c)).ok_or_else(|| other("Requires a distance."))?;
|
|
self.create(s, h)
|
|
}
|
|
},
|
|
_ => Ok(Step::Continue),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
match (self.center, self.rb, self.rt) {
|
|
(Some(ct), None, _) => vec![circle(ct, ct.dist(c))],
|
|
(Some(ct), Some(rb), None) => {
|
|
vec![helix_kind(ct, rb, ct.dist(c), 0.0, self.turns(), !self.cw, self.start.unwrap_or(0.0))]
|
|
}
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// SPLINE by control vertices
|
|
// =====================================================================================
|
|
|
|
fn run_spline_cv(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let mut c = points_param(p, "control").ok_or_else(|| bad("spline.cv", "`control` points are required"))?;
|
|
if c.len() < 2 || c.len() > MAX_GEN {
|
|
return Err(bad("spline.cv", "need 2+ control points"));
|
|
}
|
|
let closed = bool_or(p, "closed", false);
|
|
if closed && let Some(f) = c.first().copied() {
|
|
c.push(f);
|
|
}
|
|
let deg = p.get("degree").and_then(Value::as_u64).unwrap_or(3).clamp(1, 10) as usize;
|
|
let mut sp = Spline::from_control(c, deg);
|
|
sp.closed = closed;
|
|
added(s.add_entity(EntityKind::Spline(sp))?)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct SplineCvM {
|
|
pts: Vec<Vec2>,
|
|
degree: Option<usize>,
|
|
asking_degree: bool,
|
|
}
|
|
|
|
impl Interactive for SplineCvM {
|
|
fn name(&self) -> &'static str {
|
|
"SPLINE"
|
|
}
|
|
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
|
s.echo(format!("Current settings: Method=CV Degree={}", self.degree.unwrap_or(3)));
|
|
Ok(Step::Continue)
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
if self.asking_degree {
|
|
return Prompt::new("Enter degree of spline", Accept::NUMBER).default(self.degree.unwrap_or(3).to_string());
|
|
}
|
|
match self.pts.len() {
|
|
0 => Prompt::new("Specify first point", Accept::POINT).kw(&["Method", "Degree", "Object"]),
|
|
1 => Prompt::new("Enter next point", Accept::POINT).base_opt(self.pts.last().copied()),
|
|
2 => Prompt::new("Enter next point", Accept::POINT).kw(&["Undo"]).base_opt(self.pts.last().copied()),
|
|
_ => Prompt::new("Enter next point", Accept::POINT).kw(&["Close", "Undo"]).base_opt(self.pts.last().copied()),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
if self.asking_degree {
|
|
if let Input::Text(t) = &i {
|
|
let d = number(t).filter(|d| (1.0..=10.0).contains(d)).ok_or_else(|| other("Degree must be 1..10."))?;
|
|
self.degree = Some(d as usize);
|
|
}
|
|
self.asking_degree = false;
|
|
return Ok(Step::Continue);
|
|
}
|
|
let deg = self.degree.unwrap_or(3);
|
|
match i {
|
|
Input::Point(p) => {
|
|
if self.pts.len() < MAX_GEN {
|
|
self.pts.push(p);
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Keyword(k) => {
|
|
match k.as_str() {
|
|
"Degree" => self.asking_degree = true,
|
|
"Undo" => {
|
|
self.pts.pop();
|
|
}
|
|
"Close" => {
|
|
let mut c = self.pts.clone();
|
|
c.extend(self.pts.first().copied());
|
|
let mut sp = Spline::from_control(c, deg);
|
|
sp.closed = true;
|
|
s.add_entity(EntityKind::Spline(sp))?;
|
|
return Ok(Step::Done);
|
|
}
|
|
_ => s.echo(format!("{k}: use SPLINE for fit points.")),
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Enter => {
|
|
if self.pts.len() >= 2 {
|
|
s.add_entity(EntityKind::Spline(Spline::from_control(self.pts.clone(), deg)))?;
|
|
}
|
|
Ok(Step::Done)
|
|
}
|
|
_ => Ok(Step::Continue),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
if self.pts.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let mut pts = self.pts.clone();
|
|
pts.push(c);
|
|
vec![EntityKind::Spline(Spline::from_control(pts, self.degree.unwrap_or(3)))]
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// CENTERMARK / CENTERLINE
|
|
// =====================================================================================
|
|
|
|
/// Add a line on the CENTER linetype (loading it from the standard library when missing).
|
|
fn add_center_line(s: &mut Session, a: Vec2, b: Vec2) -> Result<Handle> {
|
|
let d = s.doc_mut()?;
|
|
if d.linetype("CENTER").is_none()
|
|
&& let Some(lt) = cadcraft_doc::library::standard_linetypes_for(d).into_iter().find(|l| l.name == "CENTER")
|
|
{
|
|
d.linetypes.push(lt);
|
|
}
|
|
let h = s.add_entity(line(a, b))?;
|
|
s.doc_mut()?.modify_entity(h, |e| e.common.linetype = "CENTER".into())?;
|
|
Ok(h)
|
|
}
|
|
|
|
fn center_ext(s: &Session) -> f64 {
|
|
let d = s.doc().ok();
|
|
let metric = d.is_some_and(|d| d.header.i64("MEASUREMENT", 0) == 1);
|
|
d.map(|d| d.header.f64("CENTEREXE", if metric { 3.5 } else { 0.12 })).unwrap_or(0.12)
|
|
}
|
|
|
|
fn center_mark(s: &mut Session, h: Handle) -> Result<Vec<String>> {
|
|
let e = curves::entity(s, h)?;
|
|
let (c, r) = match &e.kind {
|
|
EntityKind::Circle(c) => (c.center.xy(), c.radius),
|
|
EntityKind::Arc(a) => (a.center.xy(), a.radius),
|
|
_ => return Err(other("Select a circle or arc.")),
|
|
};
|
|
let ext = r + center_ext(s);
|
|
let h1 = add_center_line(s, c - Vec2::X * ext, c + Vec2::X * ext)?;
|
|
let h2 = add_center_line(s, c - Vec2::Y * ext, c + Vec2::Y * ext)?;
|
|
Ok(vec![h1.hex(), h2.hex()])
|
|
}
|
|
|
|
fn center_line(s: &mut Session, h1: Handle, h2: Handle) -> Result<Handle> {
|
|
let l = |h: Handle| -> Result<(Vec2, Vec2)> {
|
|
match curves::entity(s, h)?.kind {
|
|
EntityKind::Line(l) => Ok((l.a.xy(), l.b.xy())),
|
|
_ => Err(other("Select a line.")),
|
|
}
|
|
};
|
|
let (a1, b1) = l(h1)?;
|
|
let (mut a2, mut b2) = l(h2)?;
|
|
if (b1 - a1).dot(b2 - a2) < 0.0 {
|
|
std::mem::swap(&mut a2, &mut b2);
|
|
}
|
|
let (st, en) = (a1.mid(a2), b1.mid(b2));
|
|
let dir = (en - st).normalized();
|
|
if dir == Vec2::ZERO {
|
|
return Err(other("The lines do not define a center line."));
|
|
}
|
|
let ext = center_ext(s);
|
|
add_center_line(s, st - dir * ext, en + dir * ext)
|
|
}
|
|
|
|
fn run_centermark(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let hs = targets(s, p)?;
|
|
if hs.is_empty() {
|
|
return Err(bad("centermark", "`handle` or `handles` is required"));
|
|
}
|
|
let mut out = Vec::new();
|
|
for h in hs {
|
|
out.extend(center_mark(s, h)?);
|
|
}
|
|
Ok(json!({ "handles": out }))
|
|
}
|
|
|
|
fn run_centerline(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let h1 = curves::handle_param(p, "h1").ok_or_else(|| bad("centerline", "`h1` is required"))?;
|
|
let h2 = curves::handle_param(p, "h2").ok_or_else(|| bad("centerline", "`h2` is required"))?;
|
|
added(center_line(s, h1, h2)?)
|
|
}
|
|
|
|
struct CenterM {
|
|
first: Option<Handle>,
|
|
mark: bool,
|
|
}
|
|
|
|
impl Interactive for CenterM {
|
|
fn name(&self) -> &'static str {
|
|
if self.mark { "CENTERMARK" } else { "CENTERLINE" }
|
|
}
|
|
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
|
s.set_selection(Vec::new());
|
|
Ok(Step::Continue)
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
match (self.mark, self.first) {
|
|
(true, _) => Prompt::new("Select circle or arc to add center mark", Accept::POINT),
|
|
(false, None) => Prompt::new("Select first line", Accept::POINT),
|
|
(false, Some(_)) => Prompt::new("Select second line", Accept::POINT),
|
|
}
|
|
}
|
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
|
match i {
|
|
Input::Point(p) => {
|
|
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
|
|
if self.mark {
|
|
center_mark(s, h)?;
|
|
return Ok(Step::Continue);
|
|
}
|
|
match self.first {
|
|
None => {
|
|
if !matches!(curves::entity(s, h)?.kind, EntityKind::Line(_)) {
|
|
return Err(other("Select a line."));
|
|
}
|
|
self.first = Some(h);
|
|
Ok(Step::Continue)
|
|
}
|
|
Some(h1) => {
|
|
center_line(s, h1, h)?;
|
|
Ok(Step::Done)
|
|
}
|
|
}
|
|
}
|
|
Input::Enter => Ok(Step::Done),
|
|
_ => Ok(Step::Continue),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "draw2_tests.rs"]
|
|
mod tests;
|