mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
BREAKLINE: a polyline with a break symbol between two points, with Block, Size and Extension (#1340)
Tools > Express Tools > Break-line Symbol draws one polyline from the first point to the second with a zigzag break symbol at a picked location (the midpoint on Enter), as AutoCAD's Express Tool does. Size scales the symbol (times DIMSCALE), Extension runs the line past both points, and Block takes a block of the drawing whose two points on the Defpoints layer mark where the line meets the symbol. The built-in symbol is our own geometry. One undo step; the options are remembered for the session. Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Brandon Thomas
Claude Opus 5.5
parent
006f71f650
commit
7adf8d89cd
@@ -12,6 +12,7 @@ use super::machines::number;
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
||||||
|
|
||||||
|
pub(crate) mod breakline;
|
||||||
pub(crate) mod mline;
|
pub(crate) mod mline;
|
||||||
|
|
||||||
pub fn specs() -> Vec<CommandSpec> {
|
pub fn specs() -> Vec<CommandSpec> {
|
||||||
@@ -133,6 +134,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||||||
.interactive(|_| Ok(Box::new(CenterM { first: None, mark: false }))),
|
.interactive(|_| Ok(Box::new(CenterM { first: None, mark: false }))),
|
||||||
];
|
];
|
||||||
v.extend(mline::specs());
|
v.extend(mline::specs());
|
||||||
|
v.extend(breakline::specs());
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
//! BREAKLINE (Express Tools): one polyline from the first point to the second with a break
|
||||||
|
//! symbol at a location along it, the line optionally extended past both points.
|
||||||
|
//!
|
||||||
|
//! The symbol is a run of vertices between two attachment points. The built-in one is our own
|
||||||
|
//! zigzag; the Block option takes a block of the drawing that marks its attachment points with
|
||||||
|
//! two POINT objects on the Defpoints layer and draws the symbol with lines and polylines.
|
||||||
|
|
||||||
|
use cadcraft_doc::{Drawing, EntityKind};
|
||||||
|
use cadcraft_geom::{PolyVertex, Vec2};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::added;
|
||||||
|
use super::curves::MAX_GEN;
|
||||||
|
use super::machines::number;
|
||||||
|
use super::*;
|
||||||
|
use crate::cmd::modify2::as_lwpoly;
|
||||||
|
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
||||||
|
|
||||||
|
pub fn specs() -> Vec<CommandSpec> {
|
||||||
|
vec![
|
||||||
|
CommandSpec::new("breakline", "Break-line Symbol", run_breakline)
|
||||||
|
.menu(&["Tools", "Express Tools", "Break-line Symbol"])
|
||||||
|
.params(
|
||||||
|
"{p1, p2, at?: [x,y] (symbol location; default the midpoint), size? (default 1, times DIMSCALE), \
|
||||||
|
extension? (past both points; default 0), block? (a block with two points on Defpoints; default the built-in symbol)} → {handle}",
|
||||||
|
)
|
||||||
|
.interactive(|s| Ok(Box::new(BreaklineM::new(s)))),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The values BREAKLINE offers as defaults: the symbol block (empty = built-in), its size and
|
||||||
|
/// how far the line runs past the two points.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct BreaklineOptions {
|
||||||
|
pub block: String,
|
||||||
|
pub size: f64,
|
||||||
|
pub extension: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BreaklineOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
BreaklineOptions { block: String::new(), size: 1.0, extension: 0.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn other(m: impl Into<String>) -> EngineError {
|
||||||
|
EngineError::Other(m.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A break symbol in its own coordinates: the vertices from one attachment point to the other.
|
||||||
|
struct Symbol {
|
||||||
|
path: Vec<PolyVertex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Symbol {
|
||||||
|
/// Our own zigzag: up and across to the other side of the line, one unit between the
|
||||||
|
/// attachment points and one unit tall.
|
||||||
|
fn built_in() -> Self {
|
||||||
|
let pts = [(-0.5, 0.0), (-0.25, 0.5), (0.25, -0.5), (0.5, 0.0)];
|
||||||
|
Symbol { path: pts.into_iter().map(|(x, y)| PolyVertex::new(Vec2::new(x, y))).collect() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn of_block(d: &Drawing, name: &str) -> Result<Self> {
|
||||||
|
let b = d.block(name).ok_or_else(|| other(format!("Block \"{name}\" not found.")))?;
|
||||||
|
let mut anchors = Vec::new();
|
||||||
|
let mut pieces: Vec<Vec<PolyVertex>> = Vec::new();
|
||||||
|
for e in b.entities.iter() {
|
||||||
|
match &e.kind {
|
||||||
|
EntityKind::Point(p) if e.common.layer.eq_ignore_ascii_case("Defpoints") => anchors.push(p.p.xy()),
|
||||||
|
k => {
|
||||||
|
if let Some(pl) = as_lwpoly(k).filter(|pl| !pl.closed && pl.vertices.len() >= 2)
|
||||||
|
&& pieces.len() < MAX_GEN
|
||||||
|
{
|
||||||
|
pieces.push(pl.vertices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let [a, z] = anchors[..] else {
|
||||||
|
return Err(other(format!("Block \"{name}\" needs exactly two points on the Defpoints layer.")));
|
||||||
|
};
|
||||||
|
let tol = a.dist(z) * 1e-6;
|
||||||
|
if !(tol > 0.0 && tol.is_finite()) {
|
||||||
|
return Err(other(format!("The two Defpoints points of block \"{name}\" coincide.")));
|
||||||
|
}
|
||||||
|
// Chain the pieces end to end from one attachment point until the other is reached.
|
||||||
|
let mut path = vec![PolyVertex::new(a)];
|
||||||
|
let mut at = a;
|
||||||
|
while !at.near(z, tol) {
|
||||||
|
let next = pieces.iter().position(|p| p.first().is_some_and(|v| v.p.near(at, tol)) || p.last().is_some_and(|v| v.p.near(at, tol)));
|
||||||
|
let Some(i) = next else {
|
||||||
|
return Err(other(format!("The lines of block \"{name}\" must run from one Defpoints point to the other.")));
|
||||||
|
};
|
||||||
|
let mut piece = pieces.swap_remove(i);
|
||||||
|
if !piece.first().is_some_and(|v| v.p.near(at, tol)) {
|
||||||
|
piece = reversed(&piece);
|
||||||
|
}
|
||||||
|
if let Some(last) = path.last_mut() {
|
||||||
|
last.bulge = piece.first().map_or(0.0, |v| v.bulge);
|
||||||
|
}
|
||||||
|
path.extend(piece.iter().skip(1).copied());
|
||||||
|
at = path.last().map_or(z, |v| v.p);
|
||||||
|
}
|
||||||
|
Ok(Symbol { path })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span(&self) -> f64 {
|
||||||
|
match (self.path.first(), self.path.last()) {
|
||||||
|
(Some(a), Some(z)) => a.p.dist(z.p),
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The symbol scaled by `k`, its attachment points on the line through `from` along `u`.
|
||||||
|
fn placed(&self, from: Vec2, u: Vec2, k: f64) -> Vec<PolyVertex> {
|
||||||
|
let (Some(a), Some(z)) = (self.path.first(), self.path.last()) else { return Vec::new() };
|
||||||
|
let ax = (z.p - a.p).normalized();
|
||||||
|
self.path
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
let q = v.p - a.p;
|
||||||
|
let (x, y) = (q.dot(ax) * k, q.dot(ax.perp()) * k);
|
||||||
|
PolyVertex::with_bulge(from + u * x + u.perp() * y, v.bulge)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same vertices run backwards, each arc keeping its side.
|
||||||
|
fn reversed(vs: &[PolyVertex]) -> Vec<PolyVertex> {
|
||||||
|
let n = vs.len();
|
||||||
|
(0..n)
|
||||||
|
.rev()
|
||||||
|
.filter_map(|i| {
|
||||||
|
let v = vs.get(i)?;
|
||||||
|
let bulge = if i == 0 { 0.0 } else { -vs.get(i - 1).map_or(0.0, |p| p.bulge) };
|
||||||
|
Some(PolyVertex::with_bulge(v.p, bulge))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dimscale(s: &Session) -> f64 {
|
||||||
|
s.doc().ok().map(|d| d.header.f64("DIMSCALE", 1.0)).filter(|k| k.is_finite() && *k > 0.0).unwrap_or(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol(s: &Session, block: &str) -> Result<Symbol> {
|
||||||
|
if block.is_empty() { Ok(Symbol::built_in()) } else { Symbol::of_block(s.doc()?, block) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The breakline's vertices: the extension, the line up to the symbol, the symbol and the rest of
|
||||||
|
/// the line. The symbol is centred on `at` projected onto the line (the midpoint without it) and
|
||||||
|
/// kept between the two points.
|
||||||
|
fn breakline_vertices(sym: &Symbol, p1: Vec2, p2: Vec2, at: Option<Vec2>, k: f64, ext: f64) -> Result<Vec<PolyVertex>> {
|
||||||
|
let len = p1.dist(p2);
|
||||||
|
let u = (p2 - p1).normalized();
|
||||||
|
if !(len.is_finite() && len > 0.0) || u == Vec2::ZERO {
|
||||||
|
return Err(other("The two points of the breakline must differ."));
|
||||||
|
}
|
||||||
|
let span = sym.span() * k;
|
||||||
|
if !span.is_finite() || span <= 0.0 {
|
||||||
|
return Err(other("The break symbol has no size."));
|
||||||
|
}
|
||||||
|
if span > len {
|
||||||
|
return Err(other(format!("The breakline must be at least as long as the symbol ({span:.4}).")));
|
||||||
|
}
|
||||||
|
let t = at.map_or(len / 2.0, |c| (c - p1).dot(u)).clamp(span / 2.0, len - span / 2.0);
|
||||||
|
let mut pts = vec![PolyVertex::new(p1 - u * ext)];
|
||||||
|
pts.extend(sym.placed(p1 + u * (t - span / 2.0), u, k));
|
||||||
|
pts.push(PolyVertex::new(p2 + u * ext));
|
||||||
|
let mut out: Vec<PolyVertex> = Vec::with_capacity(pts.len());
|
||||||
|
for v in pts {
|
||||||
|
if !v.p.is_finite() {
|
||||||
|
return Err(other("The breakline is too large."));
|
||||||
|
}
|
||||||
|
match out.last_mut() {
|
||||||
|
Some(l) if l.p.near(v.p, len * 1e-12) => l.bulge = v.bulge,
|
||||||
|
_ => out.push(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(l) = out.last_mut() {
|
||||||
|
l.bulge = 0.0;
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_breakline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
let p1 = point_req("breakline", p, "p1")?;
|
||||||
|
let p2 = point_req("breakline", p, "p2")?;
|
||||||
|
let size = size_param("breakline", p, "size", false)?.unwrap_or(1.0);
|
||||||
|
let ext = size_param("breakline", p, "extension", true)?.unwrap_or(0.0);
|
||||||
|
let sym = symbol(s, str_param(p, "block").unwrap_or(""))?;
|
||||||
|
let vs = breakline_vertices(&sym, p1, p2, point_param(p, "at"), size * dimscale(s), ext)?;
|
||||||
|
added(s.add_entity(lwpoly(vs, false))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
enum Ask {
|
||||||
|
Points,
|
||||||
|
Block,
|
||||||
|
Size,
|
||||||
|
Extension,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BreaklineM {
|
||||||
|
opts: BreaklineOptions,
|
||||||
|
ask: Ask,
|
||||||
|
p1: Option<Vec2>,
|
||||||
|
p2: Option<Vec2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BreaklineM {
|
||||||
|
fn new(s: &Session) -> Self {
|
||||||
|
BreaklineM { opts: s.last_used.breakline.clone(), ask: Ask::Points, p1: None, p2: None }
|
||||||
|
}
|
||||||
|
fn block_label(&self) -> String {
|
||||||
|
if self.opts.block.is_empty() { "(built-in)".into() } else { self.opts.block.clone() }
|
||||||
|
}
|
||||||
|
fn finish(&self, s: &mut Session, at: Option<Vec2>) -> Result<Step> {
|
||||||
|
let (Some(p1), Some(p2)) = (self.p1, self.p2) else { return Ok(Step::Cancel) };
|
||||||
|
let vs = breakline_vertices(&symbol(s, &self.opts.block)?, p1, p2, at, self.opts.size * dimscale(s), self.opts.extension)?;
|
||||||
|
s.add_entity(lwpoly(vs, false))?;
|
||||||
|
Ok(Step::Done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interactive for BreaklineM {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BREAKLINE"
|
||||||
|
}
|
||||||
|
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||||
|
s.echo(format!("Block= {}, Size= {:.4}, Extension= {:.4}", self.block_label(), self.opts.size, self.opts.extension));
|
||||||
|
Ok(Step::Continue)
|
||||||
|
}
|
||||||
|
fn prompt(&self, _s: &Session) -> Prompt {
|
||||||
|
match (self.ask, self.p1, self.p2) {
|
||||||
|
(Ask::Block, ..) => {
|
||||||
|
Prompt::new("Enter name of breakline symbol block (. for the built-in symbol)", Accept::TEXT).default(self.block_label())
|
||||||
|
}
|
||||||
|
(Ask::Size, ..) => Prompt::new("Breakline symbol size", Accept::NUMBER).default(format!("{:.4}", self.opts.size)),
|
||||||
|
(Ask::Extension, ..) => Prompt::new("Breakline extension distance", Accept::NUMBER).default(format!("{:.4}", self.opts.extension)),
|
||||||
|
(Ask::Points, None, _) => Prompt::new("Specify first point for breakline", Accept::POINT).kw(&["Block", "Size", "Extension"]),
|
||||||
|
(Ask::Points, Some(a), None) => Prompt::new("Specify second point for breakline", Accept::POINT).base(a),
|
||||||
|
(Ask::Points, Some(_), Some(_)) => Prompt::new("Specify location for break symbol", Accept::POINT).default("Midpoint"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||||
|
match (self.ask, i) {
|
||||||
|
(Ask::Block, Input::Text(t) | Input::Keyword(t)) => {
|
||||||
|
let t = t.trim();
|
||||||
|
let name = if t == "." { String::new() } else { t.to_string() };
|
||||||
|
symbol(s, &name)?;
|
||||||
|
self.opts.block = name;
|
||||||
|
self.ask = Ask::Points;
|
||||||
|
}
|
||||||
|
(Ask::Size, Input::Text(t)) => {
|
||||||
|
self.opts.size = number(&t).filter(|v| v.is_finite() && *v > 0.0).ok_or_else(|| other("Requires a positive value."))?;
|
||||||
|
self.ask = Ask::Points;
|
||||||
|
}
|
||||||
|
(Ask::Extension, Input::Text(t)) => {
|
||||||
|
self.opts.extension = number(&t).filter(|v| v.is_finite() && *v >= 0.0).ok_or_else(|| other("Requires zero or a positive value."))?;
|
||||||
|
self.ask = Ask::Points;
|
||||||
|
}
|
||||||
|
(Ask::Block | Ask::Size | Ask::Extension, Input::Enter) => self.ask = Ask::Points,
|
||||||
|
(Ask::Points, Input::Keyword(k)) if self.p1.is_none() => {
|
||||||
|
self.ask = match k.as_str() {
|
||||||
|
"Block" => Ask::Block,
|
||||||
|
"Size" => Ask::Size,
|
||||||
|
"Extension" => Ask::Extension,
|
||||||
|
_ => Ask::Points,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(Ask::Points, Input::Point(p)) => match (self.p1, self.p2) {
|
||||||
|
(None, _) => self.p1 = Some(p),
|
||||||
|
(Some(a), None) => {
|
||||||
|
let span = symbol(s, &self.opts.block)?.span() * self.opts.size * dimscale(s);
|
||||||
|
let len = a.dist(p);
|
||||||
|
if len.is_nan() || len < span {
|
||||||
|
return Err(other(format!("The breakline must be at least as long as the symbol ({span:.4}).")));
|
||||||
|
}
|
||||||
|
self.p2 = Some(p);
|
||||||
|
}
|
||||||
|
(Some(_), Some(_)) => return self.finish(s, Some(p)),
|
||||||
|
},
|
||||||
|
(Ask::Points, Input::Enter) if self.p2.is_some() => return self.finish(s, None),
|
||||||
|
(Ask::Points, Input::Enter) => return Ok(Step::Cancel),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
s.last_used.breakline = self.opts.clone();
|
||||||
|
Ok(Step::Continue)
|
||||||
|
}
|
||||||
|
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
|
||||||
|
match (self.ask, self.p1, self.p2) {
|
||||||
|
(Ask::Points, Some(a), None) => vec![line(a, c)],
|
||||||
|
(Ask::Points, Some(a), Some(b)) => symbol(s, &self.opts.block)
|
||||||
|
.and_then(|sym| breakline_vertices(&sym, a, b, Some(c), self.opts.size * dimscale(s), self.opts.extension))
|
||||||
|
.map(|vs| vec![lwpoly(vs, false)])
|
||||||
|
.unwrap_or_default(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -372,6 +372,118 @@ fn revcloud_command_line() {
|
|||||||
assert_eq!(kinds(&s).len(), 4);
|
assert_eq!(kinds(&s).len(), 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- BREAKLINE ----------------
|
||||||
|
|
||||||
|
fn breakline_points(s: &Session) -> Vec<(f64, f64)> {
|
||||||
|
match last(s) {
|
||||||
|
EntityKind::LwPolyline(p) => {
|
||||||
|
assert!(!p.closed);
|
||||||
|
p.vertices.iter().map(|v| ((v.p.x * 1e9).round() / 1e9, (v.p.y * 1e9).round() / 1e9)).collect()
|
||||||
|
}
|
||||||
|
k => panic!("not a polyline: {k:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn breakline_json_symbol_size_extension_and_location() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
// One polyline: the line to the symbol, the zigzag centred on the midpoint, the rest of the line.
|
||||||
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0]})).unwrap();
|
||||||
|
assert_eq!(s.doc().unwrap().model.len(), 1);
|
||||||
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (4.5, 0.0), (4.75, 0.5), (5.25, -0.5), (5.5, 0.0), (10.0, 0.0)]);
|
||||||
|
// Size scales the symbol, Extension runs the line past both points, `at` is projected onto the line.
|
||||||
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0], "size": 2, "extension": 1, "at": [2, 3]})).unwrap();
|
||||||
|
assert_eq!(breakline_points(&s), vec![(-1.0, 0.0), (1.0, 0.0), (1.5, 1.0), (2.5, -1.0), (3.0, 0.0), (11.0, 0.0)]);
|
||||||
|
// A location past the end keeps the symbol on the line; its end then is the line's end.
|
||||||
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0], "size": 2, "at": [100, 0]})).unwrap();
|
||||||
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (8.0, 0.0), (8.5, 1.0), (9.5, -1.0), (10.0, 0.0)]);
|
||||||
|
// The symbol turns with the line.
|
||||||
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [0, 10]})).unwrap();
|
||||||
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (0.0, 4.5), (-0.5, 4.75), (0.5, 5.25), (0.0, 5.5), (0.0, 10.0)]);
|
||||||
|
// DIMSCALE scales the size.
|
||||||
|
s.doc_mut().unwrap().header.set_f64("DIMSCALE", 4.0);
|
||||||
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0]})).unwrap();
|
||||||
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (3.0, 0.0), (4.0, 2.0), (6.0, -2.0), (7.0, 0.0), (10.0, 0.0)]);
|
||||||
|
|
||||||
|
let n = s.doc().unwrap().model.len();
|
||||||
|
for p in [
|
||||||
|
json!({"p1": [0, 0], "p2": [3, 0]}),
|
||||||
|
json!({"p1": [0, 0], "p2": [0, 0]}),
|
||||||
|
json!({"p1": [0, 0], "p2": [10, 0], "size": 0}),
|
||||||
|
json!({"p1": [0, 0], "p2": [10, 0], "extension": -1}),
|
||||||
|
json!({"p1": [0, 0], "p2": [10, 0], "block": "NOPE"}),
|
||||||
|
json!({"p1": [0, 0], "p2": [1e308, 0], "extension": 1e308}),
|
||||||
|
] {
|
||||||
|
assert!(s.execute("breakline", &p).is_err(), "{p}");
|
||||||
|
}
|
||||||
|
assert_eq!(s.doc().unwrap().model.len(), n);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn breakline_block_symbol_runs_between_its_defpoints() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
s.execute("layer.current", &json!({"name": "Defpoints"})).unwrap();
|
||||||
|
s.execute("point", &json!({"at": [0, 0]})).unwrap();
|
||||||
|
s.execute("point", &json!({"at": [2, 0]})).unwrap();
|
||||||
|
s.execute("layer.current", &json!({"name": "0"})).unwrap();
|
||||||
|
// Drawn in pieces, one of them backwards: they are chained from one point to the other.
|
||||||
|
s.execute("line", &json!({"points": [[2, 0], [1, 1]]})).unwrap();
|
||||||
|
s.execute("line", &json!({"points": [[0, 0], [1, 1]]})).unwrap();
|
||||||
|
let hs: Vec<String> = s.doc().unwrap().model.iter().map(|e| e.handle.hex()).collect();
|
||||||
|
s.execute("block", &json!({"name": "PEAK", "base": [0, 0], "handles": hs, "keep": "delete"})).unwrap();
|
||||||
|
s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0], "block": "PEAK"})).unwrap();
|
||||||
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (4.0, 0.0), (5.0, 1.0), (6.0, 0.0), (10.0, 0.0)]);
|
||||||
|
// Without two Defpoints points the block is refused.
|
||||||
|
s.execute("line", &json!({"points": [[0, 5], [1, 5]]})).unwrap();
|
||||||
|
let l = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||||||
|
s.execute("block", &json!({"name": "BARE", "base": [0, 0], "handles": [l], "keep": "delete"})).unwrap();
|
||||||
|
assert!(s.execute("breakline", &json!({"p1": [0, 0], "p2": [10, 0], "block": "BARE"})).is_err());
|
||||||
|
|
||||||
|
s.cmdline("breakline b PEAK 0,0 10,0").unwrap();
|
||||||
|
s.cmdline("").unwrap();
|
||||||
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||||
|
assert_eq!(breakline_points(&s).len(), 5);
|
||||||
|
s.cmdline("breakline b nope").unwrap();
|
||||||
|
assert!(s.prompt_text().contains("block"), "an unknown block asks again: {}", s.prompt_text());
|
||||||
|
s.cmdline(".").unwrap();
|
||||||
|
s.cmdline("0,0 10,0").unwrap();
|
||||||
|
s.cmdline("").unwrap();
|
||||||
|
assert_eq!(breakline_points(&s).len(), 6, "`.` goes back to the built-in symbol");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn breakline_command_line_prompts_options_and_undo() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
s.start("breakline").unwrap();
|
||||||
|
assert!(s.prompt_text().contains("Specify first point for breakline"), "{}", s.prompt_text());
|
||||||
|
assert!(s.prompt_text().contains("Block") && s.prompt_text().contains("Size") && s.prompt_text().contains("Extension"), "{}", s.prompt_text());
|
||||||
|
s.cmdline("0,0").unwrap();
|
||||||
|
assert!(s.prompt_text().contains("Specify second point for breakline"), "{}", s.prompt_text());
|
||||||
|
// Too short for the symbol: asked again.
|
||||||
|
s.cmdline("0.5,0").unwrap();
|
||||||
|
assert!(s.prompt_text().contains("second point"), "{}", s.prompt_text());
|
||||||
|
s.cmdline("10,0").unwrap();
|
||||||
|
assert!(s.prompt_text().contains("Specify location for break symbol") && s.prompt_text().contains("Midpoint"), "{}", s.prompt_text());
|
||||||
|
assert!(matches!(s.preview(Vec2::new(3.0, 1.0)).first().map(|e| &e.kind), Some(EntityKind::LwPolyline(p)) if p.vertices.len() == 6));
|
||||||
|
s.cmdline("").unwrap();
|
||||||
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||||
|
assert_eq!(breakline_points(&s), vec![(0.0, 0.0), (4.5, 0.0), (4.75, 0.5), (5.25, -0.5), (5.5, 0.0), (10.0, 0.0)]);
|
||||||
|
assert_eq!(s.state().unwrap().undo.len(), 1, "one undo step");
|
||||||
|
|
||||||
|
// Size and Extension at the first prompt, a picked location; remembered for the next time.
|
||||||
|
s.cmdline("breakline s 2 e 1 0,0 10,0 2,3").unwrap();
|
||||||
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||||
|
assert_eq!(breakline_points(&s), vec![(-1.0, 0.0), (1.0, 0.0), (1.5, 1.0), (2.5, -1.0), (3.0, 0.0), (11.0, 0.0)]);
|
||||||
|
assert_eq!(s.state().unwrap().undo.len(), 2);
|
||||||
|
s.cmdline("breakline s").unwrap();
|
||||||
|
assert!(s.prompt_text().contains("2.0000"), "{}", s.prompt_text());
|
||||||
|
s.cmdline("0").unwrap();
|
||||||
|
assert!(s.prompt_text().contains("size"), "zero is refused: {}", s.prompt_text());
|
||||||
|
s.cancel();
|
||||||
|
s.undo().unwrap();
|
||||||
|
assert_eq!(s.doc().unwrap().model.len(), 1, "undo removes the whole breakline");
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------- WIPEOUT ----------------
|
// ---------------- WIPEOUT ----------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -644,6 +756,7 @@ fn new_draw_commands_survive_garbage_input() {
|
|||||||
"revcloud.polygonal",
|
"revcloud.polygonal",
|
||||||
"revcloud.freehand",
|
"revcloud.freehand",
|
||||||
"wipeout",
|
"wipeout",
|
||||||
|
"breakline",
|
||||||
"3dpoly",
|
"3dpoly",
|
||||||
"mline",
|
"mline",
|
||||||
"helix",
|
"helix",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ mod overkill;
|
|||||||
mod pline_opts;
|
mod pline_opts;
|
||||||
pub mod plotters;
|
pub mod plotters;
|
||||||
mod props;
|
mod props;
|
||||||
|
pub use draw2::breakline::BreaklineOptions;
|
||||||
pub use props::MatchProps;
|
pub use props::MatchProps;
|
||||||
mod qselect;
|
mod qselect;
|
||||||
mod selecttools;
|
mod selecttools;
|
||||||
|
|||||||
@@ -554,6 +554,8 @@ pub struct LastUsed {
|
|||||||
pub mleader: MLeaderOptions,
|
pub mleader: MLeaderOptions,
|
||||||
/// MLEADER's block content name (Options > Content type > Block).
|
/// MLEADER's block content name (Options > Content type > Block).
|
||||||
pub mleader_block: String,
|
pub mleader_block: String,
|
||||||
|
/// BREAKLINE's Block, Size and Extension.
|
||||||
|
pub breakline: cmd::BreaklineOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LastUsed {
|
impl Default for LastUsed {
|
||||||
@@ -566,6 +568,7 @@ impl Default for LastUsed {
|
|||||||
matchprop: cmd::MatchProps::default(),
|
matchprop: cmd::MatchProps::default(),
|
||||||
mleader: MLeaderOptions::default(),
|
mleader: MLeaderOptions::default(),
|
||||||
mleader_block: String::new(),
|
mleader_block: String::new(),
|
||||||
|
breakline: cmd::BreaklineOptions::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ menu Modify Edita
|
|||||||
Box Parallelepipedo
|
Box Parallelepipedo
|
||||||
Break Spezza
|
Break Spezza
|
||||||
Break At Point Spezza in un punto
|
Break At Point Spezza in un punto
|
||||||
|
Break-line Symbol Simbolo linea di interruzione
|
||||||
Bring Above Objects Porta sopra gli oggetti
|
Bring Above Objects Porta sopra gli oggetti
|
||||||
Bring Annotations to Front Porta le annotazioni in primo piano
|
Bring Annotations to Front Porta le annotazioni in primo piano
|
||||||
Bring to Front Porta in primo piano
|
Bring to Front Porta in primo piano
|
||||||
@@ -330,6 +331,7 @@ menu Modify Edita
|
|||||||
Export Table Esporta tabella
|
Export Table Esporta tabella
|
||||||
Export to PDF... Esporta in PDF...
|
Export to PDF... Esporta in PDF...
|
||||||
Expression Espressione
|
Expression Espressione
|
||||||
|
Express Tools Strumenti Express
|
||||||
Extend Estendi
|
Extend Estendi
|
||||||
Extension Estensione
|
Extension Estensione
|
||||||
Extents Estensioni
|
Extents Estensioni
|
||||||
|
|||||||
|
@@ -106,6 +106,7 @@
|
|||||||
Box Паралелепіпед
|
Box Паралелепіпед
|
||||||
Break Розірвати
|
Break Розірвати
|
||||||
Break At Point Розірвати в точці
|
Break At Point Розірвати в точці
|
||||||
|
Break-line Symbol Символ лінії розриву
|
||||||
Bring Above Objects Над об'єктами
|
Bring Above Objects Над об'єктами
|
||||||
Bring Annotations to Front Анотації на передній план
|
Bring Annotations to Front Анотації на передній план
|
||||||
Bring to Front На передній план
|
Bring to Front На передній план
|
||||||
@@ -328,6 +329,7 @@
|
|||||||
Export Table Експортувати таблицю
|
Export Table Експортувати таблицю
|
||||||
Export to PDF... Експортувати в PDF...
|
Export to PDF... Експортувати в PDF...
|
||||||
Expression Вираз
|
Expression Вираз
|
||||||
|
Express Tools Інструменти Express
|
||||||
Extend Подовжити
|
Extend Подовжити
|
||||||
Extension Продовження
|
Extension Продовження
|
||||||
Extents За межами об'єктів
|
Extents За межами об'єктів
|
||||||
|
|||||||
|
+2
-1
@@ -134,7 +134,7 @@ Detail: [localization-parity.md](localization-parity.md).
|
|||||||
| # | Gap | Est. |
|
| # | Gap | Est. |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| E1 | AutoLISP beyond the core (#758 landed the language, `command`, entity lists, selection sets, `get…` input, APPLOAD, `(load)`, `c:` commands and `acad.lsp`/`acaddoc.lsp` at startup): Visual LISP ActiveX (`vlax-`, `vla-`), DCL dialogs, reactors, an APPLOAD dialog with a Startup Suite, UCS-aware `trans` | 30–50 |
|
| E1 | AutoLISP beyond the core (#758 landed the language, `command`, entity lists, selection sets, `get…` input, APPLOAD, `(load)`, `c:` commands and `acad.lsp`/`acaddoc.lsp` at startup): Visual LISP ActiveX (`vlax-`, `vla-`), DCL dialogs, reactors, an APPLOAD dialog with a Startup Suite, UCS-aware `trans` | 30–50 |
|
||||||
| E3 | Express Tools equivalents (TXT2MTXT, BREAKLINE, BURST, NCOPY exists, SUPERHATCH, Move/Copy/Rotate) | 30–50 |
|
| E3 | Express Tools equivalents (TXT2MTXT, BURST, SUPERHATCH, Move/Copy/Rotate; NCOPY and BREAKLINE exist, BREAKLINE's Block option takes a block of the drawing, not a `.dwg` file) | 28–47 |
|
||||||
| E4 | Plug-in API (owner decision: Rust/WASM plug-ins rather than ObjectARX/.NET) | 30–50 |
|
| E4 | Plug-in API (owner decision: Rust/WASM plug-ins rather than ObjectARX/.NET) | 30–50 |
|
||||||
| E5 | CUI customisation and alias (`.pgp`) editor | 15–25 |
|
| E5 | CUI customisation and alias (`.pgp`) editor | 15–25 |
|
||||||
|
|
||||||
@@ -153,6 +153,7 @@ Detail: [localization-parity.md](localization-parity.md).
|
|||||||
| 2026-10-13 | minor | Stabilized filled-object highlight dash anchors by canonicalizing boundary traversal order; renderer, CPU pointer-frame and GPU rebuild tests cover the fix (U6) |
|
| 2026-10-13 | minor | Stabilized filled-object highlight dash anchors by canonicalizing boundary traversal order; renderer, CPU pointer-frame and GPU rebuild tests cover the fix (U6) |
|
||||||
| 2026-10-14 | minor | HATCHEDIT adds boundary loops from internal points or closed objects and removes selected loops, in the dialog, command line and JSON command: F13 narrowed |
|
| 2026-10-14 | minor | HATCHEDIT adds boundary loops from internal points or closed objects and removes selected loops, in the dialog, command line and JSON command: F13 narrowed |
|
||||||
| 2026-10-15 | minor | `entities` / MCP `query_entities` accept `selected:true`, combined with existing filters and paging; selection, drawing and undo/redo stay unchanged. No parity estimate change. |
|
| 2026-10-15 | minor | `entities` / MCP `query_entities` accept `selected:true`, combined with existing filters and paging; selection, drawing and undo/redo stay unchanged. No parity estimate change. |
|
||||||
|
| 2026-10-15 | minor | BREAKLINE (Tools ▸ Express Tools ▸ Break-line Symbol): one polyline between two points with a break symbol, Block/Size/Extension options, size times DIMSCALE: E3 narrowed |
|
||||||
| 2026-10-13 | minor | Selection window gesture preference (PICKDRAG 0/2) added to Options ▸ Selection and saved in the profile (U6), with Portuguese and Ukrainian labels |
|
| 2026-10-13 | minor | Selection window gesture preference (PICKDRAG 0/2) added to Options ▸ Selection and saved in the profile (U6), with Portuguese and Ukrainian labels |
|
||||||
| 2026-10-12 | minor | Object transparency (entity, ByLayer, ByBlock) is drawn: on screen (TRANSPARENCYDISPLAY, on by default), in PNG/SVG exports, and in plots when the page setup's "Plot transparency" is on (PDF alpha, PNG blending, SVG opacity; PLOTTRANSPARENCYOVERRIDE): F14 narrowed |
|
| 2026-10-12 | minor | Object transparency (entity, ByLayer, ByBlock) is drawn: on screen (TRANSPARENCYDISPLAY, on by default), in PNG/SVG exports, and in plots when the page setup's "Plot transparency" is on (PDF alpha, PNG blending, SVG opacity; PLOTTRANSPARENCYOVERRIDE): F14 narrowed |
|
||||||
| 2026-10-12 | minor | Hatch and Gradient dialog (HATCH, GRADIENT; HPDLGMODE) and Hatch Edit dialog (HATCHEDIT, right-click, double-click a hatch): F13 narrowed to Preview, Inherit Properties and boundary add/remove in edit mode |
|
| 2026-10-12 | minor | Hatch and Gradient dialog (HATCH, GRADIENT; HPDLGMODE) and Hatch Edit dialog (HATCHEDIT, right-click, double-click a hatch): F13 narrowed to Preview, Inherit Properties and boundary add/remove in edit mode |
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Parity checklist
|
# Parity checklist
|
||||||
|
|
||||||
> **Generated:** 2026-10-12 by `cargo xtask parity`. Do not edit by hand.
|
> **Generated:** 2026-10-15 by `cargo xtask parity`. Do not edit by hand.
|
||||||
|
|
||||||
Menu-breadth parity: a reference menu item counts as covered when a CADCraft command is registered under the same menu label. It measures breadth, not depth; see [target-app-parity.md](target-app-parity.md) for the weighted assessment.
|
Menu-breadth parity: a reference menu item counts as covered when a CADCraft command is registered under the same menu label. It measures breadth, not depth; see [target-app-parity.md](target-app-parity.md) for the weighted assessment.
|
||||||
|
|
||||||
**Menu breadth: 280 / 491 items (57%)** · 409 registered commands.
|
**Menu breadth: 283 / 491 items (58%)** · 427 registered commands.
|
||||||
|
|
||||||
| Menu | Items | Covered | % |
|
| Menu | Items | Covered | % |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -13,7 +13,7 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com
|
|||||||
| View | 75 | 31 | 41% |
|
| View | 75 | 31 | 41% |
|
||||||
| Insert | 11 | 5 | 45% |
|
| Insert | 11 | 5 | 45% |
|
||||||
| Format | 27 | 25 | 93% |
|
| Format | 27 | 25 | 93% |
|
||||||
| Tools | 85 | 57 | 67% |
|
| Tools | 85 | 60 | 71% |
|
||||||
| Draw | 87 | 55 | 63% |
|
| Draw | 87 | 55 | 63% |
|
||||||
| Dimension | 28 | 28 | 100% |
|
| Dimension | 28 | 28 | 100% |
|
||||||
| Modify | 113 | 52 | 46% |
|
| Modify | 113 | 52 | 46% |
|
||||||
@@ -88,9 +88,7 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com
|
|||||||
- Tools > Spelling
|
- Tools > Spelling
|
||||||
- Tools > Inquiry > Volume
|
- Tools > Inquiry > Volume
|
||||||
- Tools > Update Fields
|
- Tools > Update Fields
|
||||||
- Tools > Block Editor
|
|
||||||
- Tools > Edit Xref in Place
|
- Tools > Edit Xref in Place
|
||||||
- Tools > Load Application...
|
|
||||||
- Tools > New UCS > World
|
- Tools > New UCS > World
|
||||||
- Tools > New UCS > Face
|
- Tools > New UCS > Face
|
||||||
- Tools > New UCS > View
|
- Tools > New UCS > View
|
||||||
@@ -106,7 +104,6 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com
|
|||||||
- Tools > Import Settings
|
- Tools > Import Settings
|
||||||
- Tools > Export Settings
|
- Tools > Export Settings
|
||||||
- Tools > Express Tools > Convert Text to Mtext
|
- Tools > Express Tools > Convert Text to Mtext
|
||||||
- Tools > Express Tools > Break-line Symbol
|
|
||||||
- Tools > Express Tools > Explode Attributes to Text
|
- Tools > Express Tools > Explode Attributes to Text
|
||||||
- Tools > Express Tools > Move/Copy/Rotate
|
- Tools > Express Tools > Move/Copy/Rotate
|
||||||
- Tools > Express Tools > Replace Block...
|
- Tools > Express Tools > Replace Block...
|
||||||
|
|||||||
Reference in New Issue
Block a user