mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46: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 crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
pub(crate) mod breakline;
|
||||
pub(crate) mod mline;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -133,6 +134,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
.interactive(|_| Ok(Box::new(CenterM { first: None, mark: false }))),
|
||||
];
|
||||
v.extend(mline::specs());
|
||||
v.extend(breakline::specs());
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------------- 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 ----------------
|
||||
|
||||
#[test]
|
||||
@@ -644,6 +756,7 @@ fn new_draw_commands_survive_garbage_input() {
|
||||
"revcloud.polygonal",
|
||||
"revcloud.freehand",
|
||||
"wipeout",
|
||||
"breakline",
|
||||
"3dpoly",
|
||||
"mline",
|
||||
"helix",
|
||||
|
||||
@@ -31,6 +31,7 @@ mod overkill;
|
||||
mod pline_opts;
|
||||
pub mod plotters;
|
||||
mod props;
|
||||
pub use draw2::breakline::BreaklineOptions;
|
||||
pub use props::MatchProps;
|
||||
mod qselect;
|
||||
mod selecttools;
|
||||
|
||||
@@ -554,6 +554,8 @@ pub struct LastUsed {
|
||||
pub mleader: MLeaderOptions,
|
||||
/// MLEADER's block content name (Options > Content type > Block).
|
||||
pub mleader_block: String,
|
||||
/// BREAKLINE's Block, Size and Extension.
|
||||
pub breakline: cmd::BreaklineOptions,
|
||||
}
|
||||
|
||||
impl Default for LastUsed {
|
||||
@@ -566,6 +568,7 @@ impl Default for LastUsed {
|
||||
matchprop: cmd::MatchProps::default(),
|
||||
mleader: MLeaderOptions::default(),
|
||||
mleader_block: String::new(),
|
||||
breakline: cmd::BreaklineOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ menu Modify Edita
|
||||
Box Parallelepipedo
|
||||
Break Spezza
|
||||
Break At Point Spezza in un punto
|
||||
Break-line Symbol Simbolo linea di interruzione
|
||||
Bring Above Objects Porta sopra gli oggetti
|
||||
Bring Annotations to Front Porta le annotazioni in primo piano
|
||||
Bring to Front Porta in primo piano
|
||||
@@ -330,6 +331,7 @@ menu Modify Edita
|
||||
Export Table Esporta tabella
|
||||
Export to PDF... Esporta in PDF...
|
||||
Expression Espressione
|
||||
Express Tools Strumenti Express
|
||||
Extend Estendi
|
||||
Extension Estensione
|
||||
Extents Estensioni
|
||||
|
||||
|
@@ -106,6 +106,7 @@
|
||||
Box Паралелепіпед
|
||||
Break Розірвати
|
||||
Break At Point Розірвати в точці
|
||||
Break-line Symbol Символ лінії розриву
|
||||
Bring Above Objects Над об'єктами
|
||||
Bring Annotations to Front Анотації на передній план
|
||||
Bring to Front На передній план
|
||||
@@ -328,6 +329,7 @@
|
||||
Export Table Експортувати таблицю
|
||||
Export to PDF... Експортувати в PDF...
|
||||
Expression Вираз
|
||||
Express Tools Інструменти Express
|
||||
Extend Подовжити
|
||||
Extension Продовження
|
||||
Extents За межами об'єктів
|
||||
|
||||
|
+2
-1
@@ -134,7 +134,7 @@ Detail: [localization-parity.md](localization-parity.md).
|
||||
| # | 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 |
|
||||
| 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 |
|
||||
| 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-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 | 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-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 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 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: 280 / 491 items (57%)** · 409 registered commands.
|
||||
**Menu breadth: 283 / 491 items (58%)** · 427 registered commands.
|
||||
|
||||
| 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% |
|
||||
| Insert | 11 | 5 | 45% |
|
||||
| Format | 27 | 25 | 93% |
|
||||
| Tools | 85 | 57 | 67% |
|
||||
| Tools | 85 | 60 | 71% |
|
||||
| Draw | 87 | 55 | 63% |
|
||||
| Dimension | 28 | 28 | 100% |
|
||||
| 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 > Inquiry > Volume
|
||||
- Tools > Update Fields
|
||||
- Tools > Block Editor
|
||||
- Tools > Edit Xref in Place
|
||||
- Tools > Load Application...
|
||||
- Tools > New UCS > World
|
||||
- Tools > New UCS > Face
|
||||
- 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 > Export Settings
|
||||
- Tools > Express Tools > Convert Text to Mtext
|
||||
- Tools > Express Tools > Break-line Symbol
|
||||
- Tools > Express Tools > Explode Attributes to Text
|
||||
- Tools > Express Tools > Move/Copy/Rotate
|
||||
- Tools > Express Tools > Replace Block...
|
||||
|
||||
Reference in New Issue
Block a user