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:
Marco Arnulfo
2026-10-19 02:55:41 -04:00
committed by GitHub
co-authored by Brandon Thomas Claude Opus 5.5
parent 006f71f650
commit 7adf8d89cd
9 changed files with 429 additions and 7 deletions
+2
View File
@@ -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
} }
+301
View File
@@ -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(),
}
}
}
+113
View File
@@ -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",
+1
View File
@@ -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;
+3
View File
@@ -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(),
} }
} }
} }
+2
View File
@@ -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
1 # CADCraft Italian interface catalog — original translations by Emanuele Bottelli (AI-assisted, Claude).
109
110
111
112
113
114
115
331
332
333
334
335
336
337
+2
View File
@@ -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 За межами об'єктів
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
106
107
108
109
110
111
112
329
330
331
332
333
334
335
+2 -1
View File
@@ -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 |
+3 -6
View File
@@ -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...