mirror of
https://github.com/aphrody-labs/cadcraft.git
synced 2026-10-13 19:26:54 +00:00
PLINE arc mode answered "not available yet" for Angle, CEnter, Direction, Radius and Second pt, and line mode for Length. Each now asks its questions (included angles and directions read the drawing's angle settings) and adds the segment. XLINE's options were already in draw2's XlineM2; the dead keyword stub in draw.rs's RAY machine goes, and Offset/Reference use the polyline segment picked rather than the first one. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2485 lines
102 KiB
Rust
2485 lines
102 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, 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 fn specs() -> Vec<CommandSpec> {
|
|
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("mline", "Multiline", run_mline)
|
|
.menu(&["Draw", "Multiline"])
|
|
.alias(&["ml"])
|
|
.params("{points, scale?, justification?: top|zero|bottom, closed?} (two offset polylines)")
|
|
.interactive(|s| Ok(Box::new(MlineM::new(s)))),
|
|
CommandSpec::new("helix", "Helix", run_helix)
|
|
.menu(&["Draw", "Helix"])
|
|
.params("{center, baseRadius, topRadius?, height?, turns?, ccw?} (3D polyline approximation)")
|
|
.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 }))),
|
|
]
|
|
}
|
|
|
|
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"))?;
|
|
added(s.add_entity(arc(&a))?)
|
|
}
|
|
|
|
#[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));
|
|
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"]).base(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()
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// MLINE (two offset polylines)
|
|
// =====================================================================================
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum Just {
|
|
Top,
|
|
Zero,
|
|
Bottom,
|
|
}
|
|
|
|
fn mline_offsets(j: Just, scale: f64) -> [f64; 2] {
|
|
match j {
|
|
Just::Top => [0.0, -scale],
|
|
Just::Zero => [scale / 2.0, -scale / 2.0],
|
|
Just::Bottom => [scale, 0.0],
|
|
}
|
|
}
|
|
|
|
fn mline_kinds(pts: &[Vec2], closed: bool, j: Just, scale: f64) -> Vec<EntityKind> {
|
|
if pts.len() < 2 {
|
|
return Vec::new();
|
|
}
|
|
let pl = Polyline::from_points(pts, closed);
|
|
mline_offsets(j, scale)
|
|
.iter()
|
|
.filter_map(|d| {
|
|
let vs = if d.abs() < 1e-15 { Some(pl.vertices.clone()) } else { super::modify::offset_polyline(&pl, *d) };
|
|
vs.map(|vs| lwpoly(vs, closed))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn just_of(s: &str) -> Option<Just> {
|
|
match s.trim().to_ascii_lowercase().as_str() {
|
|
"t" | "top" => Some(Just::Top),
|
|
"z" | "zero" => Some(Just::Zero),
|
|
"b" | "bottom" => Some(Just::Bottom),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn mline_defaults(s: &Session) -> (Just, f64) {
|
|
let d = s.doc().ok();
|
|
let metric = d.is_some_and(|d| d.header.i64("MEASUREMENT", 0) == 1);
|
|
let scale = d.map(|d| d.header.f64("CMLSCALE", if metric { 20.0 } else { 1.0 })).unwrap_or(1.0);
|
|
let j = match d.map(|d| d.header.i64("CMLJUST", 0)).unwrap_or(0) {
|
|
1 => Just::Zero,
|
|
2 => Just::Bottom,
|
|
_ => Just::Top,
|
|
};
|
|
(j, scale)
|
|
}
|
|
|
|
fn run_mline(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let pts = points_param(p, "points").ok_or_else(|| bad("mline", "`points` (2 or more) is required"))?;
|
|
if pts.len() < 2 || pts.len() > MAX_GEN {
|
|
return Err(bad("mline", "need 2 or more points"));
|
|
}
|
|
let (dj, ds) = mline_defaults(s);
|
|
let j = match str_param(p, "justification") {
|
|
Some(t) => just_of(t).ok_or_else(|| bad("mline", "justification must be top, zero or bottom"))?,
|
|
None => dj,
|
|
};
|
|
let scale = f64_or(p, "scale", ds);
|
|
let mut hs = Vec::new();
|
|
for k in mline_kinds(&pts, bool_or(p, "closed", false), j, scale) {
|
|
hs.push(s.add_entity(k)?.hex());
|
|
}
|
|
Ok(json!({ "handles": hs }))
|
|
}
|
|
|
|
struct MlineM {
|
|
pts: Vec<Vec2>,
|
|
just: Just,
|
|
scale: f64,
|
|
asking: u8, // 0 none, 1 justification, 2 scale
|
|
}
|
|
|
|
impl MlineM {
|
|
fn new(s: &Session) -> Self {
|
|
let (just, scale) = mline_defaults(s);
|
|
MlineM { pts: Vec::new(), just, scale, asking: 0 }
|
|
}
|
|
fn finish(&self, s: &mut Session, closed: bool) -> Result<Step> {
|
|
for k in mline_kinds(&self.pts, closed, self.just, self.scale) {
|
|
s.add_entity(k)?;
|
|
}
|
|
Ok(Step::Done)
|
|
}
|
|
}
|
|
|
|
impl Interactive for MlineM {
|
|
fn name(&self) -> &'static str {
|
|
"MLINE"
|
|
}
|
|
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
|
let j = match self.just {
|
|
Just::Top => "Top",
|
|
Just::Zero => "Zero",
|
|
Just::Bottom => "Bottom",
|
|
};
|
|
s.echo(format!("Current settings: Justification = {j}, Scale = {:.2}, Style = STANDARD", self.scale));
|
|
Ok(Step::Continue)
|
|
}
|
|
fn prompt(&self, _s: &Session) -> Prompt {
|
|
match self.asking {
|
|
1 => return Prompt::new("Enter justification type", curves::KW).kw(&["Top", "Zero", "Bottom"]).default("top"),
|
|
2 => return Prompt::new("Enter mline scale", Accept::NUMBER).default(format!("{:.2}", self.scale)),
|
|
_ => {}
|
|
}
|
|
match self.pts.len() {
|
|
0 => Prompt::new("Specify start point", Accept::POINT).kw(&["Justification", "Scale", "STyle"]),
|
|
1 => Prompt::new("Specify next point", Accept::POINT).base_opt(self.pts.last().copied()),
|
|
2 => Prompt::new("Specify next point", Accept::POINT).kw(&["Undo"]).base_opt(self.pts.last().copied()),
|
|
_ => Prompt::new("Specify 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 == 1 {
|
|
if let Input::Keyword(k) | Input::Text(k) = &i {
|
|
self.just = just_of(k).ok_or_else(|| other("Enter Top, Zero or Bottom."))?;
|
|
s.doc_mut()?.header.set_i64(
|
|
"CMLJUST",
|
|
match self.just {
|
|
Just::Top => 0,
|
|
Just::Zero => 1,
|
|
Just::Bottom => 2,
|
|
},
|
|
);
|
|
}
|
|
self.asking = 0;
|
|
return Ok(Step::Continue);
|
|
}
|
|
if self.asking == 2 {
|
|
if let Input::Text(t) = &i {
|
|
self.scale = number(t).filter(|v| v.is_finite()).ok_or_else(|| other("Requires a number."))?;
|
|
s.doc_mut()?.header.set_f64("CMLSCALE", self.scale);
|
|
}
|
|
self.asking = 0;
|
|
return Ok(Step::Continue);
|
|
}
|
|
match i {
|
|
Input::Keyword(k) => {
|
|
match k.as_str() {
|
|
"Justification" => self.asking = 1,
|
|
"Scale" => self.asking = 2,
|
|
"Undo" => {
|
|
self.pts.pop();
|
|
}
|
|
"Close" => return self.finish(s, true),
|
|
_ => s.echo("Only the STANDARD multiline style is available."),
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
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);
|
|
}
|
|
Ok(Step::Continue)
|
|
}
|
|
Input::Enter => {
|
|
if self.pts.len() >= 2 {
|
|
self.finish(s, false)
|
|
} else {
|
|
Ok(Step::Cancel)
|
|
}
|
|
}
|
|
_ => Ok(Step::Continue),
|
|
}
|
|
}
|
|
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
|
if self.pts.is_empty() || self.asking != 0 {
|
|
return Vec::new();
|
|
}
|
|
let mut pts = self.pts.clone();
|
|
pts.push(c);
|
|
mline_kinds(&pts, false, self.just, self.scale)
|
|
}
|
|
}
|
|
|
|
// =====================================================================================
|
|
// HELIX (3D polyline approximation)
|
|
// =====================================================================================
|
|
|
|
pub(crate) fn helix_points(c: Vec2, rb: f64, rt: f64, height: f64, turns: f64, ccw: bool, a0: f64) -> Vec<Vec3> {
|
|
let turns = turns.clamp(1e-3, 500.0);
|
|
let n = ((turns * 36.0).ceil() as usize).clamp(8, MAX_GEN);
|
|
let sign = if ccw { 1.0 } else { -1.0 };
|
|
(0..=n)
|
|
.map(|i| {
|
|
let t = i as f64 / n as f64;
|
|
let a = a0 + sign * TAU * turns * t;
|
|
let r = rb + (rt - rb) * t;
|
|
let p = Vec2::polar(c, r, a);
|
|
Vec3::new(p.x, p.y, height * t)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
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 pts = helix_points(c, rb, rt, f64_or(p, "height", 1.0), turns, bool_or(p, "ccw", true), f64_or(p, "startAngle", 0.0).to_radians());
|
|
added(s.add_entity(EntityKind::Polyline3d(Polyline3d { points: pts, closed: false }))?)
|
|
}
|
|
|
|
#[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);
|
|
let pts = helix_points(c, rb, rt, height, self.turns(), !self.cw, self.start.unwrap_or(0.0));
|
|
s.add_entity(EntityKind::Polyline3d(Polyline3d { points: pts, closed: false }))?;
|
|
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) => {
|
|
let pts = helix_points(ct, rb, ct.dist(c), 0.0, self.turns(), !self.cw, self.start.unwrap_or(0.0));
|
|
vec![EntityKind::Polyline3d(Polyline3d { points: pts, closed: false })]
|
|
}
|
|
_ => 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;
|