MLEADER's Options answered "not available yet" for Leader type sPline and None and for Content type Block. Spline leaders now draw as smooth curves through their points (DXF LEADER path type 72 = 1); None places only the content at one picked point; Block asks for a block name and how it attaches (Center extents or Insertion point) and inserts it at the end of the landing. MLeader gains `spline` and `block` (serde defaults); DXF stays LEADER + MTEXT/INSERT. PURGE, RENAME, copy/paste, FLATTEN, grips and extents follow the block content. The choices are remembered like the other MLEADER options. docs/gaps.md: top-10 row 5, F1 and F10 narrowed. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a395ec3d2d
commit
87d1e28345
|
|
@ -460,6 +460,26 @@ pub struct MLeader {
|
|||
pub style: String,
|
||||
#[serde(default = "one")]
|
||||
pub arrow_size: f64,
|
||||
/// Leader lines are smooth curves through their points (leader type Spline). An empty
|
||||
/// `leaders` is a multileader without a leader line (leader type None).
|
||||
#[serde(default)]
|
||||
pub spline: bool,
|
||||
/// Block content in place of the text: a block reference placed at the landing.
|
||||
#[serde(default)]
|
||||
pub block: Option<Insert>,
|
||||
}
|
||||
|
||||
impl MLeader {
|
||||
/// Each leader line as drawn: through its points to the landing, smoothed for spline leaders.
|
||||
pub fn leader_paths(&self) -> Vec<Vec<Vec2>> {
|
||||
self.leaders
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let pts: Vec<Vec2> = l.iter().map(|v| v.xy()).chain(std::iter::once(self.landing.xy())).collect();
|
||||
if self.spline && pts.len() > 2 { GSpline::from_fit_points(&pts).tessellate(1e-3) } else { pts }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
/// A hatch boundary loop: closed polyline with bulges.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -831,6 +851,7 @@ impl EntityKind {
|
|||
.leaders
|
||||
.iter()
|
||||
.flat_map(|l| l.windows(2).filter_map(|w| Some(Prim::Seg(Segment::Line(GLine::new(w.first()?.xy(), w.get(1)?.xy()))))))
|
||||
.chain(m.block.iter().map(|b| Prim::Point(b.insert.xy())))
|
||||
.collect(),
|
||||
EntityKind::Image(i) => vec![Prim::Point(i.insert.xy())],
|
||||
EntityKind::Table(t) => vec![Prim::Point(t.insert.xy())],
|
||||
|
|
@ -952,6 +973,14 @@ impl EntityKind {
|
|||
t3(&mut t.insert);
|
||||
t.height *= s;
|
||||
}
|
||||
if let Some(b) = &mut ml.block {
|
||||
// Transformed exactly like a block reference.
|
||||
let mut k = EntityKind::Insert(b.clone());
|
||||
k.transform(m);
|
||||
if let EntityKind::Insert(nb) = k {
|
||||
*b = nb;
|
||||
}
|
||||
}
|
||||
}
|
||||
EntityKind::Hatch(h) => {
|
||||
for l in &mut h.loops {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@ fn bounds_in(d: &Drawing, e: &Entity, depth: usize, blocks: &mut HashMap<String,
|
|||
if let Some(t) = &m.text {
|
||||
b.add(t.insert.xy());
|
||||
}
|
||||
if let Some(ins) = &m.block {
|
||||
let be = Entity { handle: e.handle, common: e.common.clone(), kind: EntityKind::Insert(ins.clone()) };
|
||||
b = b.union(&bounds_in(d, &be, depth, blocks));
|
||||
}
|
||||
b
|
||||
}
|
||||
EntityKind::Table(t) => {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
CommandSpec::new("mleader", "Multileader", run_mleader)
|
||||
.menu(&["Dimension", "Multileader"])
|
||||
.alias(&["mld"])
|
||||
.params("{points: [[arrow], ..., [landing]], text}")
|
||||
.params("{points: [[arrow], ..., [landing]] ([location] with leaderType none), text?, leaderType?: straight|spline|none, block?: name, blockAttach?: center|insertion}")
|
||||
.interactive(|s| Ok(Box::new(MLeaderM::new(s)))),
|
||||
CommandSpec::new("leader", "Leader", run_leader).alias(&["lead"]).params("{points: [[x,y]...], text?}"),
|
||||
CommandSpec::new("dimstyle.update", "Update", run_update)
|
||||
|
|
@ -489,15 +489,20 @@ fn mleader_kind(s: &Session, pts: &[Vec2], text: &str, landing: bool) -> Option<
|
|||
}),
|
||||
style: st.name.clone(),
|
||||
arrow_size: st.arrow_size * k,
|
||||
spline: false,
|
||||
block: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn run_mleader(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let pts = points_param(p, "points").ok_or_else(|| bad("mleader", "`points` (arrow … landing) is required"))?;
|
||||
if pts.len() < 2 {
|
||||
let content = super::mleader_content::json_content(s, p)?;
|
||||
// Leader type None places the content at the one point given.
|
||||
if pts.len() < 2 && !(content.leader == crate::LeaderType::None && pts.len() == 1) {
|
||||
return Err(bad("mleader", "need an arrowhead and a landing point"));
|
||||
}
|
||||
let k = mleader_kind(s, &pts, str_param(p, "text").unwrap_or(""), true).ok_or_else(|| bad("mleader", "bad points"))?;
|
||||
let k = super::mleader_content::apply(s, k, &content)?;
|
||||
Ok(json!({ "handle": s.add_entity(k)?.hex() }))
|
||||
}
|
||||
|
||||
|
|
@ -1467,6 +1472,8 @@ enum LeaderOpt {
|
|||
MaxPoints,
|
||||
FirstAngle,
|
||||
SecondAngle,
|
||||
BlockName,
|
||||
BlockAttach,
|
||||
}
|
||||
|
||||
/// Upper bound for MLEADER Maxpoints.
|
||||
|
|
@ -1486,6 +1493,11 @@ struct MLeaderM {
|
|||
angles: [f64; 2],
|
||||
landing: bool,
|
||||
content: bool,
|
||||
leader_type: crate::LeaderType,
|
||||
/// Block content: the block (`block_name`) instead of text.
|
||||
block: bool,
|
||||
block_name: String,
|
||||
block_center: bool,
|
||||
}
|
||||
|
||||
impl MLeaderM {
|
||||
|
|
@ -1503,12 +1515,37 @@ impl MLeaderM {
|
|||
angles: o.angles,
|
||||
landing: o.landing,
|
||||
content: o.content,
|
||||
leader_type: o.leader_type,
|
||||
// Block content needs a block that still exists.
|
||||
block: o.block && s.doc().is_ok_and(|d| d.block(&s.last_used.mleader_block).is_some()),
|
||||
block_name: s.last_used.mleader_block.clone(),
|
||||
block_center: o.block_center,
|
||||
}
|
||||
}
|
||||
/// Keep the order and Options for the next MLEADER.
|
||||
fn remember(&self, s: &mut Session) {
|
||||
s.last_used.mleader =
|
||||
MLeaderOptions { order: self.order, max_points: self.max_points, angles: self.angles, landing: self.landing, content: self.content };
|
||||
s.last_used.mleader = MLeaderOptions {
|
||||
order: self.order,
|
||||
max_points: self.max_points,
|
||||
angles: self.angles,
|
||||
landing: self.landing,
|
||||
content: self.content,
|
||||
leader_type: self.leader_type,
|
||||
block: self.block,
|
||||
block_center: self.block_center,
|
||||
};
|
||||
s.last_used.mleader_block = self.block_name.clone();
|
||||
}
|
||||
/// The leader type and block content for `mleader_content::apply`.
|
||||
fn content_spec(&self) -> super::mleader_content::Content {
|
||||
super::mleader_content::Content {
|
||||
leader: self.leader_type,
|
||||
block: (self.content && self.block).then(|| (self.block_name.clone(), self.block_center)),
|
||||
}
|
||||
}
|
||||
/// Leader type None: the one point placed is the content location.
|
||||
fn no_leader(&self) -> bool {
|
||||
self.leader_type == crate::LeaderType::None
|
||||
}
|
||||
/// The leader from arrowhead to landing, with `extra` placed next.
|
||||
fn leader(&self, extra: Option<Vec2>) -> Vec<Vec2> {
|
||||
|
|
@ -1531,11 +1568,19 @@ impl MLeaderM {
|
|||
}
|
||||
/// The text is asked now: right after the content location (Content first) or after the leader.
|
||||
fn wants_text(&self) -> bool {
|
||||
self.content && self.text.is_none() && (self.leader_done || (self.order == LeaderOrder::Content && self.pts.len() == 1))
|
||||
self.content
|
||||
&& !self.block
|
||||
&& self.text.is_none()
|
||||
&& (self.leader_done || (self.order == LeaderOrder::Content && self.pts.len() == 1 && !self.no_leader()))
|
||||
}
|
||||
fn finish(&self, s: &mut Session) -> Result<Step> {
|
||||
if self.no_leader() && !self.content {
|
||||
s.echo("A multileader needs a leader line or content; nothing was drawn.");
|
||||
return Ok(Step::Cancel);
|
||||
}
|
||||
let text = self.text.clone().unwrap_or_default();
|
||||
let k = mleader_kind(s, &self.leader(None), &text, self.landing).ok_or_else(|| EngineError::Other("bad leader".into()))?;
|
||||
let k = super::mleader_content::apply(s, k, &self.content_spec())?;
|
||||
s.add_entity(k)?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
|
|
@ -1551,6 +1596,10 @@ impl MLeaderM {
|
|||
LeaderOpt::MaxPoints => Prompt::new("Enter the maximum points for leader line", Accept::NUMBER),
|
||||
LeaderOpt::FirstAngle => Prompt::new("Enter first angle constraint", Accept::NUMBER),
|
||||
LeaderOpt::SecondAngle => Prompt::new("Enter second angle constraint", Accept::NUMBER),
|
||||
LeaderOpt::BlockName => Prompt::new("Enter block name", kw),
|
||||
LeaderOpt::BlockAttach => {
|
||||
Prompt::new("Specify block attachment", kw).kw(&["Center extents", "Insertion point"]).default("Center extents")
|
||||
}
|
||||
}
|
||||
}
|
||||
/// One answer at the Options prompts; Enter keeps the current value.
|
||||
|
|
@ -1568,11 +1617,11 @@ impl MLeaderM {
|
|||
},
|
||||
(Menu, Input::Enter) => None,
|
||||
(LeaderType, Input::Keyword(k)) => {
|
||||
match k.as_str() {
|
||||
"sPline" => s.echo("Spline leaders are not available yet; the leader stays straight."),
|
||||
"None" => s.echo("Multileaders without a leader line are not available yet."),
|
||||
_ => {}
|
||||
}
|
||||
self.leader_type = match k.as_str() {
|
||||
"sPline" => crate::LeaderType::Spline,
|
||||
"None" => crate::LeaderType::None,
|
||||
_ => crate::LeaderType::Straight,
|
||||
};
|
||||
Some(Menu)
|
||||
}
|
||||
(Landing, Input::Keyword(k)) => {
|
||||
|
|
@ -1580,11 +1629,34 @@ impl MLeaderM {
|
|||
Some(Menu)
|
||||
}
|
||||
(ContentType, Input::Keyword(k)) => {
|
||||
match k.as_str() {
|
||||
"Block" => s.echo("Block content is not available yet; the content stays multiline text."),
|
||||
"None" => self.content = false,
|
||||
_ => self.content = true,
|
||||
self.content = k != "None";
|
||||
self.block = false;
|
||||
// Block content asks which block, then how it attaches.
|
||||
if k == "Block" { Some(BlockName) } else { Some(Menu) }
|
||||
}
|
||||
(BlockName, Input::Text(t)) => match super::mleader_content::block_name(s, &t) {
|
||||
Ok(n) => {
|
||||
self.block_name = n;
|
||||
self.block = true;
|
||||
Some(BlockAttach)
|
||||
}
|
||||
Err(e) => {
|
||||
s.echo(e.to_string());
|
||||
Some(BlockName)
|
||||
}
|
||||
},
|
||||
(BlockName, Input::Enter) => {
|
||||
// Enter keeps the block chosen before, if there is one.
|
||||
if super::mleader_content::block_name(s, &self.block_name).is_ok() {
|
||||
self.block = true;
|
||||
Some(BlockAttach)
|
||||
} else {
|
||||
s.echo("Requires the name of a block defined in the drawing.");
|
||||
Some(BlockName)
|
||||
}
|
||||
}
|
||||
(BlockAttach, Input::Keyword(k)) => {
|
||||
self.block_center = k == "Center extents";
|
||||
Some(Menu)
|
||||
}
|
||||
(MaxPoints, Input::Text(t)) => match t.trim().parse::<usize>() {
|
||||
|
|
@ -1607,7 +1679,7 @@ impl MLeaderM {
|
|||
Some(o)
|
||||
}
|
||||
},
|
||||
(Menu | LeaderType | Landing | ContentType, Input::Text(_)) => {
|
||||
(Menu | LeaderType | Landing | ContentType | BlockAttach, Input::Text(_)) => {
|
||||
s.echo("Invalid option keyword.");
|
||||
Some(o)
|
||||
}
|
||||
|
|
@ -1628,6 +1700,10 @@ impl Interactive for MLeaderM {
|
|||
if self.wants_text() {
|
||||
return Prompt::new("Enter leader text", Accept::TEXT);
|
||||
}
|
||||
if self.no_leader() && self.pts.is_empty() {
|
||||
let what = if self.content && self.block { "Specify location of the block" } else { "Specify location of the text" };
|
||||
return Prompt::new(what, Accept::POINT).kw(&["Options"]).default("Options");
|
||||
}
|
||||
let last = self.pts.len() + 1 >= self.max_points;
|
||||
match (self.pts.last(), self.order) {
|
||||
(None, LeaderOrder::Arrowhead) => Prompt::new("Specify leader arrowhead location", Accept::POINT)
|
||||
|
|
@ -1658,7 +1734,7 @@ impl Interactive for MLeaderM {
|
|||
Input::Point(p) => {
|
||||
let p = self.constrained(p);
|
||||
self.pts.push(p);
|
||||
if self.pts.len() >= self.max_points {
|
||||
if self.pts.len() >= self.max_points || self.no_leader() {
|
||||
self.leader_done = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1684,7 +1760,10 @@ impl Interactive for MLeaderM {
|
|||
return Vec::new();
|
||||
}
|
||||
let text = self.text.as_deref().unwrap_or("");
|
||||
mleader_kind(s, &self.leader(Some(self.constrained(c))), text, self.landing).into_iter().collect()
|
||||
mleader_kind(s, &self.leader(Some(self.constrained(c))), text, self.landing)
|
||||
.and_then(|k| super::mleader_content::apply(s, k, &self.content_spec()).ok())
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ fn used_blocks(d: &Drawing) -> HashSet<String> {
|
|||
match &e.kind {
|
||||
EntityKind::Insert(i) => frontier.push(i.block.clone()),
|
||||
EntityKind::Dimension(dm) => frontier.extend(dm.block.clone()),
|
||||
EntityKind::MLeader(m) => frontier.extend(m.block.as_ref().map(|b| b.block.clone())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +128,7 @@ fn used_blocks(d: &Drawing) -> HashSet<String> {
|
|||
match &e.kind {
|
||||
EntityKind::Insert(i) => frontier.push(i.block.clone()),
|
||||
EntityKind::Dimension(dm) => frontier.extend(dm.block.clone()),
|
||||
EntityKind::MLeader(m) => frontier.extend(m.block.as_ref().map(|b| b.block.clone())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ impl Used {
|
|||
EntityKind::MLeader(m) => {
|
||||
self.mleader_styles.insert(up(&m.style));
|
||||
self.text_styles.extend(m.text.iter().map(|t| up(&t.style)));
|
||||
blocks.extend(m.block.as_ref().map(|b| b.block.clone()));
|
||||
}
|
||||
EntityKind::Table(t) => {
|
||||
self.table_styles.insert(up(&t.style));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
//! MLEADER's leader types (straight, spline, none) and block content: applied to the multileader
|
||||
//! `mleader_kind` builds, for the prompt machine and the JSON form.
|
||||
|
||||
use cadcraft_doc::{Entity, EntityKind, Handle, Insert};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{bad, str_param};
|
||||
use crate::{EngineError, LeaderType, Result, Session};
|
||||
|
||||
/// How the multileader differs from a straight leader with text.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub(super) struct Content {
|
||||
pub leader: LeaderType,
|
||||
/// Block content: the block name, and whether its extents centre (rather than its insertion
|
||||
/// point) attaches to the end of the landing.
|
||||
pub block: Option<(String, bool)>,
|
||||
}
|
||||
|
||||
/// A block MLEADER can use as content: defined, and not an anonymous (`*`) block.
|
||||
pub(super) fn block_name(s: &Session, name: &str) -> Result<String> {
|
||||
let name = name.trim();
|
||||
match s.doc()?.block(name) {
|
||||
Some(b) if !b.name.starts_with('*') => Ok(b.name.clone()),
|
||||
_ => Err(EngineError::Other(format!("Block \"{name}\" not found."))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the leader type and block content to a multileader from `mleader_kind`.
|
||||
pub(super) fn apply(s: &Session, k: EntityKind, c: &Content) -> Result<EntityKind> {
|
||||
let EntityKind::MLeader(mut m) = k else { return Ok(k) };
|
||||
m.spline = c.leader == LeaderType::Spline;
|
||||
let land = m.landing.xy();
|
||||
// The side the content sits on, as `mleader_kind` chose it for the text.
|
||||
let dir = if m.leaders.first().and_then(|l| l.first()).is_some_and(|a| a.x > land.x) { -1.0 } else { 1.0 };
|
||||
if c.leader == LeaderType::None {
|
||||
// Content only, at the picked location (text from its top left corner).
|
||||
m.leaders.clear();
|
||||
m.dogleg = 0.0;
|
||||
if let Some(t) = &mut m.text {
|
||||
t.insert = m.landing;
|
||||
t.attach = 1;
|
||||
}
|
||||
}
|
||||
if let Some((name, center)) = &c.block {
|
||||
let anchor = land + Vec2::new(m.dogleg * dir, 0.0);
|
||||
let mut ins = Insert {
|
||||
block: name.clone(),
|
||||
insert: Vec3::new(anchor.x, anchor.y, m.landing.z),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: Vec::new(),
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
};
|
||||
if *center {
|
||||
// Move the block so the centre of its extents lands on the anchor.
|
||||
let d = s.doc()?;
|
||||
let probe = Entity { handle: Handle(0), common: Default::default(), kind: EntityKind::Insert(ins.clone()) };
|
||||
let b = cadcraft_doc::entity_bounds(d, &probe, 0);
|
||||
if !b.is_empty() {
|
||||
let c = b.center();
|
||||
if c.is_finite() {
|
||||
ins.insert = Vec3::new(ins.insert.x + anchor.x - c.x, ins.insert.y + anchor.y - c.y, ins.insert.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
m.text = None;
|
||||
m.block = Some(ins);
|
||||
}
|
||||
Ok(EntityKind::MLeader(m))
|
||||
}
|
||||
|
||||
/// `leaderType` ("straight" | "spline" | "none"), `block` (a block name) and `blockAttach`
|
||||
/// ("center" | "insertion") from the JSON form.
|
||||
pub(super) fn json_content(s: &Session, p: &Value) -> Result<Content> {
|
||||
let leader = match str_param(p, "leaderType").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("straight") => LeaderType::Straight,
|
||||
Some("spline") => LeaderType::Spline,
|
||||
Some("none") => LeaderType::None,
|
||||
Some(_) => return Err(bad("mleader", "`leaderType` must be \"straight\", \"spline\" or \"none\"")),
|
||||
};
|
||||
let block = match str_param(p, "block") {
|
||||
None => None,
|
||||
Some(n) => {
|
||||
let center = match str_param(p, "blockAttach").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("center") => true,
|
||||
Some("insertion") => false,
|
||||
Some(_) => return Err(bad("mleader", "`blockAttach` must be \"center\" or \"insertion\"")),
|
||||
};
|
||||
Some((block_name(s, n).map_err(|e| bad("mleader", e.to_string()))?, center))
|
||||
}
|
||||
};
|
||||
Ok(Content { leader, block })
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ mod hatch;
|
|||
mod inquiry;
|
||||
mod layer;
|
||||
mod layout;
|
||||
mod mleader_content;
|
||||
mod modify;
|
||||
mod modify2;
|
||||
mod mtext_opts;
|
||||
|
|
|
|||
|
|
@ -1428,6 +1428,7 @@ pub(crate) fn flatten_kind(k: &mut EntityKind) {
|
|||
EntityKind::MLeader(m) => {
|
||||
m.leaders.iter_mut().for_each(|l| l.iter_mut().for_each(z));
|
||||
z(&mut m.landing);
|
||||
m.block.iter_mut().for_each(|b| z(&mut b.insert));
|
||||
}
|
||||
EntityKind::Hatch(h) => h.elevation = 0.0,
|
||||
EntityKind::Solid(so) | EntityKind::Trace(so) => so.corners.iter_mut().for_each(z),
|
||||
|
|
|
|||
|
|
@ -54,11 +54,15 @@ pub(super) fn block(s: &mut Session, from: &str, to: &str) -> Result<()> {
|
|||
let mut b = d.blocks.remove(&key).ok_or_else(|| bad("rename", "no such block"))?;
|
||||
Arc::make_mut(&mut b).name = to.clone();
|
||||
d.blocks.insert(to.clone(), b);
|
||||
let uses = |k: &EntityKind| matches!(k, EntityKind::Insert(i) if i.block.eq_ignore_ascii_case(&key));
|
||||
let fix = |k: &mut EntityKind| {
|
||||
if let EntityKind::Insert(i) = k {
|
||||
i.block = to.clone();
|
||||
}
|
||||
let uses = |k: &EntityKind| match k {
|
||||
EntityKind::Insert(i) => i.block.eq_ignore_ascii_case(&key),
|
||||
EntityKind::MLeader(m) => m.block.as_ref().is_some_and(|b| b.block.eq_ignore_ascii_case(&key)),
|
||||
_ => false,
|
||||
};
|
||||
let fix = |k: &mut EntityKind| match k {
|
||||
EntityKind::Insert(i) => i.block = to.clone(),
|
||||
EntityKind::MLeader(m) => m.block.iter_mut().for_each(|b| b.block = to.clone()),
|
||||
_ => {}
|
||||
};
|
||||
repoint(d, &uses, &fix)?;
|
||||
if d.header.str("INSNAME", "").eq_ignore_ascii_case(&key) {
|
||||
|
|
|
|||
|
|
@ -238,13 +238,17 @@ pub fn stretch_grip(kind: &EntityKind, index: usize, to: Vec2) -> Option<EntityK
|
|||
}
|
||||
}
|
||||
if !done {
|
||||
// The landing grip moves the landing and its text.
|
||||
// The landing grip moves the landing and its content.
|
||||
let ld = m.landing.xy() + delta;
|
||||
set_xy(&mut m.landing, ld);
|
||||
if let Some(t) = &mut m.text {
|
||||
let q = t.insert.xy() + delta;
|
||||
set_xy(&mut t.insert, q);
|
||||
}
|
||||
if let Some(b) = &mut m.block {
|
||||
let q = b.insert.xy() + delta;
|
||||
set_xy(&mut b.insert, q);
|
||||
}
|
||||
}
|
||||
}
|
||||
EntityKind::Solid(s) | EntityKind::Trace(s) => set_xy(s.corners.get_mut(index)?, to),
|
||||
|
|
|
|||
|
|
@ -387,11 +387,20 @@ pub struct LastUsed {
|
|||
pub scale_factor: f64,
|
||||
/// MLEADER placement order and Options.
|
||||
pub mleader: MLeaderOptions,
|
||||
/// MLEADER's block content name (Options > Content type > Block).
|
||||
pub mleader_block: String,
|
||||
}
|
||||
|
||||
impl Default for LastUsed {
|
||||
fn default() -> Self {
|
||||
LastUsed { hatch_select: false, polygon_circumscribed: false, rotate_angle: 0.0, scale_factor: 1.0, mleader: MLeaderOptions::default() }
|
||||
LastUsed {
|
||||
hatch_select: false,
|
||||
polygon_circumscribed: false,
|
||||
rotate_angle: 0.0,
|
||||
scale_factor: 1.0,
|
||||
mleader: MLeaderOptions::default(),
|
||||
mleader_block: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -416,11 +425,35 @@ pub struct MLeaderOptions {
|
|||
pub landing: bool,
|
||||
/// Ask for multiline text content (Content type None turns it off).
|
||||
pub content: bool,
|
||||
/// Leader lines: straight, spline, or none (content only).
|
||||
pub leader_type: LeaderType,
|
||||
/// The content is a block (`LastUsed::mleader_block`) instead of multiline text.
|
||||
pub block: bool,
|
||||
/// The block's extents centre (not its insertion point) sits at the end of the landing.
|
||||
pub block_center: bool,
|
||||
}
|
||||
|
||||
/// MLEADER's leader line type (Options > Leader type).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum LeaderType {
|
||||
#[default]
|
||||
Straight,
|
||||
Spline,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for MLeaderOptions {
|
||||
fn default() -> Self {
|
||||
MLeaderOptions { order: LeaderOrder::Arrowhead, max_points: 2, angles: [0.0; 2], landing: true, content: true }
|
||||
MLeaderOptions {
|
||||
order: LeaderOrder::Arrowhead,
|
||||
max_points: 2,
|
||||
angles: [0.0; 2],
|
||||
landing: true,
|
||||
content: true,
|
||||
leader_type: LeaderType::Straight,
|
||||
block: false,
|
||||
block_center: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -131,9 +131,10 @@ fn mleader_options_take_effect() {
|
|||
let r = Vec2::new(10.0, 8.0).len();
|
||||
assert!(near(m.landing.xy(), Vec2::new(r, r) / 2f64.sqrt()), "{:?}", m.landing);
|
||||
|
||||
// Unmodelled choices say so and keep the following input.
|
||||
// Leader type sPline keeps the following input and draws a spline leader.
|
||||
s.script("mleader o l p x 0,0 4,4 hi\n").unwrap();
|
||||
assert!(s.log.iter().any(|l| l.contains("not available yet")), "{:?}", s.log);
|
||||
assert_eq!(last_mleader(&s).text.unwrap().contents, "hi");
|
||||
let m = last_mleader(&s);
|
||||
assert!(m.spline);
|
||||
assert_eq!(m.text.unwrap().contents, "hi");
|
||||
assert!(s.running.is_none());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
//! MLEADER leader types (straight, spline, none) and block content, at the command line and in
|
||||
//! the JSON form (#408).
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{EntityKind, MLeader};
|
||||
use cadcraft_engine::geom::Vec2;
|
||||
use serde_json::json;
|
||||
|
||||
fn last_mleader(s: &Session) -> MLeader {
|
||||
match &s.doc().unwrap().model.last().unwrap().kind {
|
||||
EntityKind::MLeader(m) => m.clone(),
|
||||
other => panic!("not a multileader: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn logged(s: &Session, text: &str) -> bool {
|
||||
s.log.iter().any(|l| l.contains(text))
|
||||
}
|
||||
|
||||
/// A session with block "TAG": a 2 x 2 square around its base point (0, 0).
|
||||
fn with_block() -> Session {
|
||||
let mut s = Session::new();
|
||||
let sq = s.execute("pline", &json!({"vertices": [[-1, -1], [1, -1], [1, 1], [-1, 1]], "closed": true})).unwrap()["handle"].clone();
|
||||
s.execute("block", &json!({"name": "TAG", "base": [-1, -1], "handles": [sq], "keep": "delete"})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spline_leader_curves_through_its_points() {
|
||||
let mut s = Session::new();
|
||||
s.script("mleader o l p m 3 x 0,0 5,5 10,0 note\n").unwrap();
|
||||
let m = last_mleader(&s);
|
||||
assert!(m.spline && m.leaders[0].len() == 2);
|
||||
let path = &m.leader_paths()[0];
|
||||
assert!(path.len() > 3, "tessellated: {path:?}");
|
||||
assert!(path.first().unwrap().near(Vec2::ZERO, 1e-9) && path.last().unwrap().near(Vec2::new(10.0, 0.0), 1e-9));
|
||||
// The straight leader runs along y = x up to the middle point; the curve rounds the corner above it.
|
||||
assert!(path.iter().any(|p| p.x > 2.0 && p.x < 4.0 && p.y > p.x + 0.1), "not a smooth curve: {path:?}");
|
||||
// Remembered, and Straight switches back.
|
||||
s.script("mleader 0,0 5,5 10,0 again\n").unwrap();
|
||||
assert!(last_mleader(&s).spline);
|
||||
s.script("mleader o l s x 0,0 5,5 10,0 straight\n").unwrap();
|
||||
assert!(!last_mleader(&s).spline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_leader_places_only_the_content() {
|
||||
let mut s = Session::new();
|
||||
s.script("mleader o l n x 3,4 Only text\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let m = last_mleader(&s);
|
||||
assert!(m.leaders.is_empty() && m.dogleg == 0.0);
|
||||
let t = m.text.unwrap();
|
||||
assert_eq!(t.contents, "Only text");
|
||||
assert!(t.insert.xy().near(Vec2::new(3.0, 4.0), 1e-9) && t.attach == 1);
|
||||
// No leader and no content: nothing to draw.
|
||||
s.script("mleader o c n x 1,1\n").unwrap();
|
||||
assert!(logged(&s, "nothing was drawn"));
|
||||
assert_eq!(s.doc().unwrap().model.iter().count(), 1);
|
||||
assert!(s.running.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_content_inserts_the_block_at_the_landing() {
|
||||
let mut s = with_block();
|
||||
// Unknown names are reported and asked again; Center extents puts the block's centre at the
|
||||
// end of the landing.
|
||||
s.script("mleader o c b nosuch\n").unwrap();
|
||||
assert!(logged(&s, "Block \"nosuch\" not found."));
|
||||
s.script("tag c x 0,0 10,5\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let m = last_mleader(&s);
|
||||
assert!(m.text.is_none());
|
||||
let b = m.block.clone().expect("block content");
|
||||
assert_eq!(b.block, "TAG");
|
||||
let end = m.landing.xy() + Vec2::new(m.dogleg, 0.0);
|
||||
// Block base (-1,-1) maps to the insertion point; the square's centre is base + (1, 1).
|
||||
assert!((b.insert.xy() + Vec2::new(1.0, 1.0)).near(end, 1e-9), "{:?} vs {end:?}", b.insert);
|
||||
// Insertion point attachment, remembered for the next MLEADER together with the block.
|
||||
s.script("mleader o c b tag i x 0,0 10,5\n").unwrap();
|
||||
let m = last_mleader(&s);
|
||||
assert!(m.block.unwrap().insert.xy().near(m.landing.xy() + Vec2::new(m.dogleg, 0.0), 1e-9));
|
||||
s.script("mleader 20,0 30,5\n").unwrap();
|
||||
assert!(last_mleader(&s).block.is_some(), "block content remembered");
|
||||
// Block content without a leader line.
|
||||
s.script("mleader o l n x 50,50\n").unwrap();
|
||||
let m = last_mleader(&s);
|
||||
assert!(m.leaders.is_empty() && m.block.unwrap().insert.xy().near(Vec2::new(50.0, 50.0), 1e-9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_content_keeps_the_block_alive() {
|
||||
let mut s = with_block();
|
||||
let h = s.execute("mleader", &json!({"points": [[0, 0], [10, 5]], "block": "tag", "blockAttach": "insertion"})).unwrap()["handle"].clone();
|
||||
// PURGE keeps a block used only as multileader content.
|
||||
s.execute("purge", &json!({"type": "blocks"})).ok();
|
||||
assert!(s.doc().unwrap().block("TAG").is_some(), "purged a block in use");
|
||||
// RENAME follows into the multileader.
|
||||
s.execute("rename", &json!({"table": "block", "from": "TAG", "to": "NOTE"})).unwrap();
|
||||
assert_eq!(last_mleader(&s).block.unwrap().block, "NOTE");
|
||||
// MOVE carries the block along.
|
||||
let before = last_mleader(&s).block.unwrap().insert.xy();
|
||||
s.execute("move", &json!({"handles": [h], "from": [0, 0], "to": [5, 5]})).unwrap();
|
||||
assert!(last_mleader(&s).block.unwrap().insert.xy().near(before + Vec2::new(5.0, 5.0), 1e-9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_form_takes_leader_type_and_block() {
|
||||
let mut s = with_block();
|
||||
s.execute("mleader", &json!({"points": [[0, 0], [5, 5], [10, 0]], "text": "s", "leaderType": "spline"})).unwrap();
|
||||
assert!(last_mleader(&s).spline);
|
||||
s.execute("mleader", &json!({"points": [[7, 7]], "text": "alone", "leaderType": "none"})).unwrap();
|
||||
let m = last_mleader(&s);
|
||||
assert!(m.leaders.is_empty() && m.text.unwrap().insert.xy().near(Vec2::new(7.0, 7.0), 1e-9));
|
||||
for bad in [
|
||||
json!({"points": [[0, 0], [5, 5]], "leaderType": "wavy"}),
|
||||
json!({"points": [[0, 0], [5, 5]], "block": "missing"}),
|
||||
json!({"points": [[0, 0], [5, 5]], "block": "tag", "blockAttach": "corner"}),
|
||||
json!({"points": [[0, 0]]}),
|
||||
] {
|
||||
assert!(s.execute("mleader", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
|
@ -745,7 +745,7 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
|
|||
let le = Entity {
|
||||
handle: Handle(u64::from_str_radix(&w.h(), 16).unwrap_or(0)),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Leader(cadcraft_doc::Leader { vertices, arrow: true, spline: false, style: m.style.clone() }),
|
||||
kind: EntityKind::Leader(cadcraft_doc::Leader { vertices, arrow: true, spline: m.spline, style: m.style.clone() }),
|
||||
};
|
||||
entity(w, d, &le, owner, paper, cx);
|
||||
}
|
||||
|
|
@ -757,6 +757,15 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
|
|||
};
|
||||
entity(w, d, &te, owner, paper, cx);
|
||||
}
|
||||
// Block content: a plain INSERT of the block.
|
||||
if let Some(ins) = &m.block {
|
||||
let ie = Entity {
|
||||
handle: Handle(u64::from_str_radix(&w.h(), 16).unwrap_or(0)),
|
||||
common: e.common.clone(),
|
||||
kind: EntityKind::Insert(ins.clone()),
|
||||
};
|
||||
entity(w, d, &ie, owner, paper, cx);
|
||||
}
|
||||
}
|
||||
EntityKind::Leader(l) => {
|
||||
w.s(0, "LEADER");
|
||||
|
|
|
|||
|
|
@ -1359,6 +1359,8 @@ fn mleader_is_written_as_leader_and_mtext() {
|
|||
text: Some(text),
|
||||
style: "Standard".into(),
|
||||
arrow_size: 2.5,
|
||||
spline: false,
|
||||
block: None,
|
||||
};
|
||||
d.add(&Space::Model, Default::default(), EntityKind::MLeader(m)).unwrap();
|
||||
let back = roundtrip(&d);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
//! A multileader's spline leader and block content are written as LEADER (path type spline,
|
||||
//! group 72) and INSERT (#408); MULTILEADER objects themselves are not written.
|
||||
|
||||
use cadcraft_doc::{Block, Common, Drawing, Entity, EntityKind, Handle, Insert, Line, MLeader, Space};
|
||||
use cadcraft_geom::Vec3;
|
||||
|
||||
#[test]
|
||||
fn spline_leader_and_block_content_are_written() {
|
||||
let mut d = Drawing::new_metric();
|
||||
let mut blk = Block::new("TAG");
|
||||
blk.entities.push(Entity::new(Handle(0x100), EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 0.0), b: Vec3::new(1.0, 1.0, 0.0) })));
|
||||
d.blocks.insert(blk.name.clone(), std::sync::Arc::new(blk));
|
||||
let m = MLeader {
|
||||
leaders: vec![vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(5.0, 5.0, 0.0)]],
|
||||
landing: Vec3::new(10.0, 0.0, 0.0),
|
||||
dogleg: 1.0,
|
||||
text: None,
|
||||
style: "Standard".into(),
|
||||
arrow_size: 1.0,
|
||||
spline: true,
|
||||
block: Some(Insert {
|
||||
block: "TAG".into(),
|
||||
insert: Vec3::new(11.0, 0.0, 0.0),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: Vec::new(),
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
}),
|
||||
};
|
||||
d.add(&Space::Model, Common::default(), EntityKind::MLeader(m)).unwrap();
|
||||
let bytes = cadcraft_io::write(&d, "a.dxf").unwrap();
|
||||
let back = cadcraft_io::read(&bytes, "a.dxf").unwrap();
|
||||
assert!(back.model.iter().any(|e| matches!(&e.kind, EntityKind::Leader(l) if l.spline && l.vertices.len() == 3)));
|
||||
assert!(back.model.iter().any(|e| matches!(&e.kind, EntityKind::Insert(i) if i.block == "TAG" && i.insert == Vec3::new(11.0, 0.0, 0.0))));
|
||||
}
|
||||
|
|
@ -674,8 +674,8 @@ fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
|||
}
|
||||
}
|
||||
EntityKind::MLeader(m) => {
|
||||
for l in &m.leaders {
|
||||
let pts: Vec<Vec2> = l.iter().map(|v| v.xy()).chain(std::iter::once(m.landing.xy())).collect();
|
||||
// Straight or spline leader lines; block content draws like a block reference.
|
||||
for pts in m.leader_paths() {
|
||||
b.polyline(ctx, rgb, lw, &pts);
|
||||
if let (Some(a), Some(n)) = (pts.first(), pts.get(1)) {
|
||||
b.tris(ctx, rgb, &dim::arrow(*a, (*a - *n).normalized(), m.arrow_size));
|
||||
|
|
@ -687,6 +687,14 @@ fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
|||
b.polyline(ctx, rgb, lw, &[land, land + Vec2::new(m.dogleg * dir, 0.0)]);
|
||||
mtext(b, ctx, t, rgb, lw);
|
||||
}
|
||||
if let Some(ins) = &m.block {
|
||||
if m.dogleg > 0.0 && !m.leaders.is_empty() {
|
||||
let land = m.landing.xy();
|
||||
let dir = if ins.insert.x >= land.x { 1.0 } else { -1.0 };
|
||||
b.polyline(ctx, rgb, lw, &[land, land + Vec2::new(m.dogleg * dir, 0.0)]);
|
||||
}
|
||||
insert(b, ctx, e, ins, rgb);
|
||||
}
|
||||
}
|
||||
EntityKind::Point(p) => {
|
||||
// Relative sizes follow the view: undo the block / viewport scale.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Where CADCraft falls short of AutoCAD
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (object snap tracking, Extension, Parallel and Apparent Intersection landed, #379: top-10 row 3 and U1 narrowed) · **Target:** Autodesk AutoCAD 2027
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (SPLINE, MTEXT and MLEADER prompt options landed, #408: top-10 row 5, F1 and F10 narrowed) · **Target:** Autodesk AutoCAD 2027
|
||||
|
||||
Every known shortfall, one row each, ranked within each section by user impact. This is the work
|
||||
list: agents pick from here (top of a section first), and remove or update a row in the same PR
|
||||
|
|
@ -17,7 +17,7 @@ do them first.
|
|||
| 2 | **[alpha blocker]** **No real-file test corpus and no check in AutoCAD** | All 36 file tests are synthetic; nobody has verified a CADCraft DWG/DXF opens cleanly in AutoCAD | 10–15 + human | [file formats](file-format-parity.md#tests) |
|
||||
| 3 | **Tracking leftovers** (object snap tracking, Extension, Parallel and Apparent Intersection landed, #379): polar angles relative to the last segment, PolarSnap, temporary tracking point | Small now; ranks lower at the next review | 3–5 | [ui](ui-parity.md#tracking-polar-ortho-grid-and-snap--70) |
|
||||
| 4 | **[alpha blocker]** **DWG always saves as R2000; DXF only R2000 ASCII; no code pages** | Can't exchange at 2018 format; pre-2007 CJK/Cyrillic files are garbled | 10–15 | [file formats](file-format-parity.md) |
|
||||
| 5 | **[alpha blocker]** **Stubbed prompt options** in SPLINE, MTEXT, MLEADER | Scripts and muscle memory hit "not available yet" in the most-used commands | 15–25 | [features](#features) |
|
||||
| 5 | **[alpha blocker]** **Stubbed prompt options** left in PEDIT Edit vertex, SPLINEDIT and MTEXT columns | Scripts and muscle memory hit "not available yet" (the most-used commands are done) | 3–6 | [features](#features) |
|
||||
| 6 | **Ellipse and spline geometry is tessellated** (intersections, offsets, closest points; polyline offset ignores bulges) | Coordinates are subtly wrong; offsets of ellipses come out as polylines | 30–50 | [geometry](geometry-parity.md) |
|
||||
| 7 | **[alpha blocker]** **No xrefs, block editor, dynamic blocks or groups** | Most production drawings use them | 80–120 | [features](#features) |
|
||||
| 8 | **No autosave, crash recovery, AUDIT or RECOVER** | Work is lost if anything goes wrong; damaged files can't be repaired | 10–15 | [features](#features) |
|
||||
|
|
@ -28,7 +28,7 @@ do them first.
|
|||
|
||||
| # | Gap | Evidence | User impact | Est. |
|
||||
|---|---|---|---|---|
|
||||
| F1 | **[alpha blocker]** Prompt options print "not available yet": SPLINE Method, Knots, Object, Tangency, toLerance; MTEXT Height/Justify/Line spacing/Rotation/Style/Width/Columns; MLEADER options (TRIM/EXTEND, PLINE arc mode and Length, XLINE done (#397); OFFSET, ROTATE and FILLET options done earlier) | `cmd/draw.rs` (`SplineM`), `modify2.rs:594,783,806` | High: the commands people use most | 15–25 |
|
||||
| F1 | **[alpha blocker]** Prompt options print "not available yet": PEDIT Edit vertex Break/Regen/Tangent, some SPLINEDIT options; MTEXT Dynamic/Static columns (no column model). SPLINE, MTEXT and MLEADER options done (#408): closed splines keep but ignore a fit tolerance, SPLINE Object fits the polyline's vertices (no spline frame is kept) (TRIM/EXTEND, PLINE arc mode and Length, XLINE done (#397); OFFSET, ROTATE and FILLET options done earlier) | `modify2.rs:594,783,806`, `cmd/mtext_opts.rs` | Medium: the most-used commands are done | 3–6 |
|
||||
| F2 | **[alpha blocker]** No xrefs (XATTACH, XREF palette, REFEDIT, BIND, XCLIP, overlay, paths); DXF xref blocks read with an empty path | `dxf_read.rs:923` | High: multi-file projects | 30–45 |
|
||||
| F3 | **[alpha blocker]** (block editor and REFEDIT; dynamic-block authoring is beta) No block editor (BEDIT) or dynamic blocks (parameters, actions, visibility, lookup); dynamic blocks from other files lose their behaviour | issue #185 offers help | High: standard libraries are dynamic | 50–75 |
|
||||
| F4 | No autosave (SAVETIME), crash recovery files or drawing recovery manager (AUDIT and RECOVER landed with #406; AUDITCTL `.adt` logs and a RECOVER mode in the file readers are not there) | no SAVETIME anywhere | High: lost work | 6–10 |
|
||||
|
|
@ -37,7 +37,7 @@ do them first.
|
|||
| F7 | Dimensions: no DIMSTYLE manager dialog; 43 of ~80 DIMSTYLE variables (DIMTOFL, DIMATFIT, DIMTMOVE, DIMLTYPE, DIMLWD, DIMFXL, DIMTOLJ, DIMALTU …); no smart DIM; no DIMJOGGED, DIMJOGLINE, DIMBREAK, oblique DIMEDIT, inspection; QDIM continuous only (SETVAR DIM* now become style overrides, #129) | `doc/src/tables.rs:223-266`, `annotate.rs:64,364` | High for detailers | 25–35 |
|
||||
| F8 | No TOLERANCE (GD&T feature control frames) | menu "Dimension > Tolerance..." uncovered | Medium (mechanical) | 4–6 |
|
||||
| F9 | MTEXT editor is a dialog box, not in place; no columns, bullets, numbering, tabs, indents; no fields (FIELD, UPDATEFIELD); no spell check (SPELL) | `ui-egui/dialogs.rs:188-240` | Medium-high | 30–45 |
|
||||
| F10 | MLEADER: options ignored, no block content, no MLEADEREDIT/ALIGN/COLLECT; MLEADERSTYLE has 5 fields | `annotate.rs:1195-1230`, `tables.rs:417` | Medium | 10–15 |
|
||||
| F10 | MLEADER: no MLEADEREDIT/ALIGN/COLLECT; MLEADERSTYLE has 5 fields (block content and spline/no-leader types are per command, not in the style; block content has no attributes) | `annotate.rs`, `cmd/mleader_content.rs`, `tables.rs:417` | Medium | 8–12 |
|
||||
| F11 | Tables: no cell styles, formulas, data links, table breaking | `cmd/table.rs` | Medium | 10–15 |
|
||||
| F12 | Annotative scaling: the flag is stored, nothing scales; no scale list, no "Annotative Object Scale" commands | 4 uncovered menu items | Medium-high in layouts | 10–15 |
|
||||
| F13 | Hatch: 23 patterns (AutoCAD ~85; ours must stay original); gradients render as a flat colour; associativity not updated when the boundary moves; origin always 0,0; island style always normal; no gap tolerance, separate hatches, hatch dialog, MPolygon | `doc/src/library.rs`, `render/lib.rs:430-437`, `cmd/hatch.rs:198,201` | Medium | 15–25 |
|
||||
|
|
@ -158,6 +158,7 @@ Detail: [localization-parity.md](localization-parity.md).
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | SPLINE Knots/Tangency/toLerance/Object, MTEXT Height/Justify/Line spacing/Rotation/Style/Width/Columns, MLEADER spline and no-leader types and block content landed (#408): top-10 row 5, F1 and F10 narrowed |
|
||||
| 2026-10-11 | minor | Object snap tracking, Extension, Parallel and true Apparent Intersection landed (#379): top-10 row 3 and U1 narrowed to the leftovers |
|
||||
| 2026-10-10 | minor | Merged main: closed or narrowed rows for the Ukrainian catalog, UNITS dialog, shortcut display, dynamic input pointer boxes, press-drag windows, SETVAR DIM*, WIPEOUT and gradients, fmt |
|
||||
| 2026-10-10 | minor | Alpha blockers marked (rows failing the core-workflow gate) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue