Annotative scaling: scale list, CANNOSCALE, OBJECTSCALE, ANNOALLVISIBLE (part of #468) (#486)

Squash of PR #486 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Valentin KleinandClaude Opus 5.5 authored and GitHub committed 2026-10-11 11:42:13 -04:00
1 parent c0a19dfb9e
commit e7bb447d58
23 files changed
+1352 -13

No files matched your search

+239
View File
@@ -0,0 +1,239 @@
//! Annotative scaling.
//!
//! An annotation scale ("1:50") says how many drawing units one paper unit stands for. The
//! drawing keeps a list of them (SCALELISTEDIT) and a current one (`CANNOSCALE`). An annotative
//! object (text, MText, dimension, multileader, hatch, block reference) stores its size as it
//! should measure on paper and the set of scales it supports ([`Common::anno_scales`]); in a view
//! it draws at paper size × that view's annotation scale, and only in views whose scale it
//! supports unless `ANNOALLVISIBLE` is 1. Model space is seen at `CANNOSCALE`, a layout viewport
//! at its viewport scale, and paper space itself at 1:1.
use serde::{Deserialize, Serialize};
use crate::{Common, Drawing, Entity, EntityKind};
/// A named annotation scale: `paper_units` on paper show `drawing_units` of the drawing.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnnoScale {
pub name: String,
pub paper_units: f64,
pub drawing_units: f64,
}
/// The most scales a drawing's list or one object holds (hostile files and parameters).
pub const MAX_SCALES: usize = 1000;
impl AnnoScale {
pub fn new(name: &str, paper_units: f64, drawing_units: f64) -> Self {
AnnoScale { name: name.to_string(), paper_units, drawing_units }
}
/// The 1:1 scale.
pub fn unit() -> Self {
AnnoScale::new("1:1", 1.0, 1.0)
}
/// Both units finite and positive (and their ratio usable).
pub fn is_valid(&self) -> bool {
let ok = |v: f64| v.is_finite() && v > 0.0;
ok(self.paper_units) && ok(self.drawing_units) && ok(self.drawing_units / self.paper_units)
}
/// Drawing units per paper unit (1:50 → 50): what an annotative object's paper size is
/// multiplied by. 1 for an invalid scale.
pub fn factor(&self) -> f64 {
if self.is_valid() { self.drawing_units / self.paper_units } else { 1.0 }
}
/// Whether this scale has the ratio `factor` (drawing units per paper unit).
pub fn has_factor(&self, factor: f64) -> bool {
same_factor(self.factor(), factor)
}
}
/// Two scale factors are the same ratio (relative tolerance).
pub fn same_factor(a: f64, b: f64) -> bool {
(a - b).abs() <= 1e-9 * a.abs().max(b.abs()).max(1e-12)
}
/// The default metric scale list.
pub fn metric_scales() -> Vec<AnnoScale> {
let mut v: Vec<AnnoScale> =
[1.0, 2.0, 4.0, 5.0, 8.0, 10.0, 16.0, 20.0, 30.0, 40.0, 50.0, 100.0].iter().map(|n| AnnoScale::new(&format!("1:{n}"), 1.0, *n)).collect();
v.extend([2.0, 4.0, 8.0, 10.0, 100.0].iter().map(|n| AnnoScale::new(&format!("{n}:1"), *n, 1.0)));
v
}
/// The default imperial scale list: the metric ratios, then the architectural inch-to-foot
/// scales.
pub fn imperial_scales() -> Vec<AnnoScale> {
let mut v = metric_scales();
let arch: [(&str, f64); 15] = [
("1/128\" = 1'-0\"", 1.0 / 128.0),
("1/64\" = 1'-0\"", 1.0 / 64.0),
("1/32\" = 1'-0\"", 1.0 / 32.0),
("1/16\" = 1'-0\"", 1.0 / 16.0),
("3/32\" = 1'-0\"", 3.0 / 32.0),
("1/8\" = 1'-0\"", 1.0 / 8.0),
("3/16\" = 1'-0\"", 3.0 / 16.0),
("1/4\" = 1'-0\"", 0.25),
("3/8\" = 1'-0\"", 0.375),
("1/2\" = 1'-0\"", 0.5),
("3/4\" = 1'-0\"", 0.75),
("1\" = 1'-0\"", 1.0),
("1-1/2\" = 1'-0\"", 1.5),
("3\" = 1'-0\"", 3.0),
("6\" = 1'-0\"", 6.0),
];
v.extend(arch.iter().map(|(n, p)| AnnoScale::new(n, *p, 12.0)));
v.push(AnnoScale::new("1'-0\" = 1'-0\"", 12.0, 12.0));
v
}
/// Kinds that can be annotative.
pub fn can_be_annotative(k: &EntityKind) -> bool {
matches!(
k,
EntityKind::Text(_) | EntityKind::MText(_) | EntityKind::Dimension(_) | EntityKind::MLeader(_) | EntityKind::Hatch(_) | EntityKind::Insert(_)
)
}
/// An annotative object: a kind that can be annotative, with at least one supported scale.
pub fn is_annotative(e: &Entity) -> bool {
!e.common.anno_scales.is_empty() && can_be_annotative(&e.kind)
}
/// Make `e` annotative (supporting `current` when it supports no scale yet) or not. Kinds that
/// can't be annotative are left alone.
pub fn set_annotative(e: &mut Entity, on: bool, current: &AnnoScale) {
if !on {
e.common.anno_scales.clear();
} else if can_be_annotative(&e.kind) && e.common.anno_scales.is_empty() {
e.common.anno_scales.push(current.clone());
}
}
/// How an object draws in a view.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnnoDraw {
/// Not annotative: as stored.
Plain,
/// Annotative, at this factor (drawing units per paper unit).
Scaled(f64),
/// Annotative and not supporting the view's scale, with `ANNOALLVISIBLE` 0.
Hidden,
}
impl Common {
/// Whether these properties list a scale with the ratio `factor`.
pub fn supports_factor(&self, factor: f64) -> bool {
self.anno_scales.iter().any(|s| s.has_factor(factor))
}
/// Whether these properties list the scale named `name`.
pub fn supports_scale(&self, name: &str) -> bool {
self.anno_scales.iter().any(|s| s.name.eq_ignore_ascii_case(name))
}
}
/// How `e` draws in a view whose annotation scale is `view` (drawing units per paper unit). An
/// object that doesn't support the view's scale shows at its first scale when `all_visible`.
pub fn anno_draw(e: &Entity, view: f64, all_visible: bool) -> AnnoDraw {
if !is_annotative(e) {
return AnnoDraw::Plain;
}
if e.common.supports_factor(view) {
return AnnoDraw::Scaled(if view.is_finite() && view > 0.0 { view } else { 1.0 });
}
match e.common.anno_scales.first() {
Some(s) if all_visible => AnnoDraw::Scaled(s.factor()),
_ => AnnoDraw::Hidden,
}
}
/// `e` as it draws at annotation scale `f`: its paper sizes (text height and width, dimension
/// overall scale, multileader arrow, landing and content, hatch pattern scale, block scale and
/// attributes) multiplied by `f`. A dimension is drawn from its definition at that scale, not
/// from a stored geometry block. Other kinds come back unchanged.
pub fn anno_scaled(e: &Entity, f: f64) -> Entity {
let f = if f.is_finite() && f > 0.0 { f } else { 1.0 };
let mut out = e.clone();
match &mut out.kind {
EntityKind::Text(t) => t.height *= f,
EntityKind::MText(t) => {
t.height *= f;
t.width *= f;
}
EntityKind::Dimension(dm) => {
dm.overrides.insert("scale".into(), serde_json::json!(f));
dm.block = None;
}
EntityKind::MLeader(m) => {
let land = m.landing;
m.arrow_size *= f;
m.dogleg *= f;
if let Some(t) = &mut m.text {
t.insert = land + (t.insert - land) * f;
t.height *= f;
t.width *= f;
}
if let Some(ins) = &mut m.block {
ins.insert = land + (ins.insert - land) * f;
ins.scale = ins.scale * f;
}
}
EntityKind::Hatch(h) if !h.solid => h.scale *= f,
EntityKind::Insert(ins) => {
let o = ins.insert;
ins.scale = ins.scale * f;
ins.col_spacing *= f;
ins.row_spacing *= f;
for a in &mut ins.attribs {
a.text.insert = o + (a.text.insert - o) * f;
a.text.align_pt = a.text.align_pt.map(|p| o + (p - o) * f);
a.text.height *= f;
}
}
_ => {}
}
out
}
impl Drawing {
/// The scale named `name` in the drawing's scale list (case-insensitive).
pub fn anno_scale(&self, name: &str) -> Option<&AnnoScale> {
let n = name.trim();
self.scales.iter().find(|s| s.name.eq_ignore_ascii_case(n))
}
/// The current annotation scale (`CANNOSCALE`); 1:1 when it names no scale of the list.
pub fn current_anno_scale(&self) -> AnnoScale {
let name = self.header.str("CANNOSCALE", "1:1");
self.anno_scale(&name).filter(|s| s.is_valid()).cloned().unwrap_or_else(AnnoScale::unit)
}
/// Whether annotative objects that don't support the view's scale show (`ANNOALLVISIBLE`).
pub fn anno_all_visible(&self) -> bool {
self.header.i64("ANNOALLVISIBLE", 1) != 0
}
/// How top-level object `e` draws outside a viewport: in a layout's paper space at 1:1
/// (paper space shows every annotative object), elsewhere (model space, or an object not in
/// the drawing yet) as [`anno_draw`] at the current annotation scale.
pub fn anno_draw(&self, e: &Entity) -> AnnoDraw {
if !is_annotative(e) {
return AnnoDraw::Plain;
}
if !self.model.contains(e.handle) && self.layouts.iter().any(|l| l.entities.contains(e.handle)) {
return AnnoDraw::Scaled(1.0);
}
anno_draw(e, self.current_anno_scale().factor(), self.anno_all_visible())
}
/// Whether new objects of this kind are annotative: text and MText with an annotative text
/// style, dimensions with an annotative dimension style, multileaders with an annotative
/// multileader style, hatches while `HPANNOTATIVE` is 1, references to annotative blocks.
pub fn annotative_by_style(&self, k: &EntityKind) -> bool {
match k {
EntityKind::Text(t) => self.text_style(&t.style).is_some_and(|s| s.annotative),
EntityKind::MText(t) => self.text_style(&t.style).is_some_and(|s| s.annotative),
EntityKind::Dimension(dm) => self.dim_style(&dm.style).is_some_and(|s| s.annotative),
EntityKind::MLeader(m) => self.mleader_styles.iter().find(|s| s.name.eq_ignore_ascii_case(&m.style)).is_some_and(|s| s.annotative),
EntityKind::Hatch(_) => self.header.i64("HPANNOTATIVE", 0) != 0,
EntityKind::Insert(ins) => self.block(&ins.block).is_some_and(|b| b.annotative),
_ => false,
}
}
}
+4
View File
@@ -97,6 +97,9 @@ pub struct Common {
/// Named plot style: `ByLayer`, `ByBlock` or a style of the layout's named (`.stb`) plot
/// style table.
pub plot_style: String,
/// The annotation scales an annotative object supports (empty: not annotative).
#[serde(skip_serializing_if = "Vec::is_empty")]
pub anno_scales: Vec<crate::AnnoScale>,
}
impl Default for Common {
@@ -112,6 +115,7 @@ impl Default for Common {
thickness: 0.0,
extrusion: Vec3::Z,
plot_style: "ByLayer".into(),
anno_scales: Vec::new(),
}
}
}
+8
View File
@@ -49,6 +49,14 @@ fn blank(s: &str) -> bool {
}
pub fn entity_bounds(d: &Drawing, e: &Entity, depth: usize) -> Bounds2 {
// A top-level annotative object measures as it draws at the current annotation scale.
if depth == 0 {
match d.anno_draw(e) {
crate::AnnoDraw::Plain => {}
crate::AnnoDraw::Hidden => return Bounds2::EMPTY,
crate::AnnoDraw::Scaled(f) => return bounds_in(d, &crate::anno_scaled(e, f), 0, &mut HashMap::new()),
}
}
bounds_in(d, e, depth, &mut HashMap::new())
}
+3
View File
@@ -118,6 +118,9 @@ impl Header {
// Snap grid origin and rotation (SNAPANG in degrees).
h.set("SNAPBASE", HVal::Point(Vec3::ZERO));
h.set_f64("SNAPANG", 0.0);
// Annotation scale of model space; annotative objects of other scales show (1) or not.
h.set_str("CANNOSCALE", "1:1");
h.set_i64("ANNOALLVISIBLE", 1);
h
}
+7
View File
@@ -77,6 +77,8 @@ pub struct Drawing {
pub constraints: Vec<Constraint>,
/// User parameters and parametric settings.
pub parametric: Parametric,
/// The annotation scale list (SCALELISTEDIT).
pub scales: Vec<AnnoScale>,
/// Next free handle.
pub handseed: u64,
/// Objects hidden for this session by ISOLATEOBJECTS / HIDEOBJECTS. Never written to a file:
@@ -101,6 +103,7 @@ impl Drawing {
pub fn new_metric() -> Self {
let mut d = Drawing::bare();
d.header = Header::metric();
d.scales = metric_scales();
d.dim_styles.push(DimStyle::iso25());
d.header.set_str("DIMSTYLE", "ISO-25");
d
@@ -128,6 +131,7 @@ impl Drawing {
plot_style_tables: Vec::new(),
constraints: Vec::new(),
parametric: Parametric::default(),
scales: imperial_scales(),
handseed: 0x100,
hidden_objects: BTreeSet::new(),
}
@@ -317,5 +321,8 @@ impl Drawing {
}
}
mod anno;
pub use anno::*;
#[cfg(test)]
mod tests;
+15 -1
View File
@@ -459,11 +459,21 @@ pub struct MLeaderStyle {
pub landing_gap: f64,
pub dogleg: f64,
pub text_style: String,
/// New multileaders of this style are annotative.
pub annotative: bool,
}
impl Default for MLeaderStyle {
fn default() -> Self {
MLeaderStyle { name: "Standard".into(), arrow_size: 0.18, text_height: 0.18, landing_gap: 0.09, dogleg: 0.36, text_style: "Standard".into() }
MLeaderStyle {
name: "Standard".into(),
arrow_size: 0.18,
text_height: 0.18,
landing_gap: 0.09,
dogleg: 0.36,
text_style: "Standard".into(),
annotative: false,
}
}
}
@@ -488,6 +498,9 @@ impl Default for TableStyle {
#[serde(rename_all = "camelCase")]
pub struct Block {
pub name: String,
/// References to this block are annotative.
#[serde(default)]
pub annotative: bool,
pub base: Vec3,
#[serde(skip)]
pub entities: EntityStore,
@@ -511,6 +524,7 @@ impl Block {
pub fn new(name: &str) -> Self {
Block {
name: name.into(),
annotative: false,
base: Vec3::ZERO,
entities: EntityStore::new(),
description: String::new(),
+589
View File
@@ -0,0 +1,589 @@
//! Annotative scaling: the drawing's scale list (SCALELISTEDIT), the scales annotative objects
//! support (OBJECTSCALE, Add/Delete Current Scale, ANNORESET, ANNOUPDATE), which annotative
//! objects show (ANNOALLVISIBLE) and the current annotation scale (CANNOSCALE with
//! ANNOAUTOSCALE).
use cadcraft_doc::{AnnoScale, Drawing, EntityKind, Handle, MAX_SCALES, is_annotative};
use serde_json::{Value, json};
use super::machines::{SelOutcome, SelectPhase, SelectRun};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("scalelistedit", "Scale List...", run_scalelistedit)
.menu(&["Format", "Scale List..."])
.alias(&["-scalelistedit"])
.params(
"{add?: {name, ratio: \"paper:drawing\" (e.g. \"1:50\") | paperUnits + drawingUnits}, delete?: name | [names], reset?: true | \"metric\" | \"imperial\"} → scales, current (no parameters: list only)",
)
.interactive(|_| Ok(Box::new(ScaleListM::default()))),
CommandSpec::new("objectscale", "Add/Delete Scales...", run_objectscale)
.menu(&["Modify", "Annotative Object Scale", "Add/Delete Scales..."])
.alias(&["-objectscale"])
.params("{handles?, add?: name | [names], delete?: name | [names]} (scales of the drawing's list; an object keeps at least one) → changed, objects")
.interactive(|_| Ok(Box::new(ObjectScaleM::default()))),
CommandSpec::new("aiobjectscaleadd", "Add Current Scale", run_add_current)
.menu(&["Modify", "Annotative Object Scale", "Add Current Scale"])
.params("{handles?} (adds the current annotation scale to the annotative objects)")
.interactive(|_| Ok(Box::new(SelectRun::new("aiobjectscaleadd", "AIOBJECTSCALEADD")))),
CommandSpec::new("aiobjectscaleremove", "Delete Current Scale", run_remove_current)
.menu(&["Modify", "Annotative Object Scale", "Delete Current Scale"])
.params("{handles?} (removes the current annotation scale from the annotative objects; an object keeps at least one)")
.interactive(|_| Ok(Box::new(SelectRun::new("aiobjectscaleremove", "AIOBJECTSCALEREMOVE")))),
CommandSpec::new("annoreset", "Synchronize Multiple-scale Positions", run_annoreset)
.menu(&["Modify", "Annotative Object Scale", "Synchronize Multiple-scale Positions"])
.params("{handles?} (every scale of an annotative object shares one position)")
.interactive(|_| Ok(Box::new(SelectRun::new("annoreset", "ANNORESET")))),
CommandSpec::new("annoupdate", "Annotative Update", run_annoupdate)
.params("{handles?} (makes text, dimensions, multileaders and block references annotative or not as their style or block is)")
.interactive(|_| Ok(Box::new(SelectRun::new("annoupdate", "ANNOUPDATE")))),
CommandSpec::new("annovisible.current", "Current Scale Only", |s, _| set_all_visible(s, Some(false)))
.menu(&["View", "Show Annotative Objects", "Current Scale Only"])
.params("{} (ANNOALLVISIBLE 0: annotative objects show only at scales they support)"),
CommandSpec::new("annovisible.all", "All Scales", |s, _| set_all_visible(s, Some(true)))
.menu(&["View", "Show Annotative Objects", "All Scales"])
.params("{} (ANNOALLVISIBLE 1: every annotative object shows)"),
CommandSpec::new("annovisible.toggle", "Annotation Visibility", |s, _| set_all_visible(s, None)).params("{} (toggles ANNOALLVISIBLE)"),
]
}
/// The most objects one command call reports scales for.
const MAX_REPORTED: usize = 1000;
fn scale_json(sc: &AnnoScale) -> Value {
json!({ "name": sc.name, "paperUnits": sc.paper_units, "drawingUnits": sc.drawing_units })
}
fn list_json(d: &Drawing) -> Value {
json!({ "scales": d.scales.iter().map(scale_json).collect::<Vec<_>>(), "current": d.current_anno_scale().name })
}
/// Scale names given as one string or an array of strings.
fn names(cmd: &str, v: Option<&Value>) -> Result<Vec<String>> {
match v {
None | Some(Value::Null) => Ok(Vec::new()),
Some(Value::String(s)) => Ok(vec![s.trim().to_string()]),
Some(Value::Array(a)) if a.len() <= MAX_SCALES => {
a.iter().map(|v| v.as_str().map(|s| s.trim().to_string()).ok_or_else(|| bad(cmd, "scale names must be strings"))).collect()
}
Some(_) => Err(bad(cmd, "expected a scale name or an array of scale names")),
}
}
fn find_scale(d: &Drawing, cmd: &str, name: &str) -> Result<AnnoScale> {
d.anno_scale(name).cloned().ok_or_else(|| bad(cmd, format!("`{name}` is not in the scale list")))
}
/// A scale ratio typed as `paper:drawing` or `paper=drawing`, each side a number or distance
/// (`1:50`, `2:1`, `1/4":1'`).
pub(crate) fn parse_ratio(t: &str) -> Option<(f64, f64)> {
let (a, b) = t.split_once(':').or_else(|| t.split_once('='))?;
let ok = |v: f64| v.is_finite() && v > 0.0;
let a = crate::units::parse_distance(a).filter(|v| ok(*v))?;
let b = crate::units::parse_distance(b).filter(|v| ok(*v))?;
Some((a, b))
}
/// Make `name` the current annotation scale (CANNOSCALE). With ANNOAUTOSCALE on, the annotative
/// objects of model space on layers that are on, thawed and unlocked get the scale too.
pub(crate) fn set_current(s: &mut Session, name: &str) -> Result<()> {
let sc = find_scale(s.doc()?, "setvar", name)?;
let auto = s.settings.annoautoscale;
let d = s.doc_mut()?;
d.header.set_str("CANNOSCALE", &sc.name);
if auto {
let usable = |e: &cadcraft_doc::Entity| d.layer(&e.common.layer).is_none_or(|l| l.visible() && !l.locked);
let hs: Vec<Handle> = d.model.iter().filter(|e| is_annotative(e) && usable(e)).map(|e| e.handle).collect();
for h in hs {
add_scale(d, h, &sc);
}
}
Ok(())
}
/// Add `sc` to annotative object `h`; whether it changed.
fn add_scale(d: &mut Drawing, h: Handle, sc: &AnnoScale) -> bool {
let mut changed = false;
let _ = d.modify_entity(h, |e| {
if is_annotative(e) && !e.common.supports_scale(&sc.name) && e.common.anno_scales.len() < MAX_SCALES {
e.common.anno_scales.push(sc.clone());
changed = true;
}
});
changed
}
/// What removing a scale from an object did.
#[derive(PartialEq)]
enum Removed {
Yes,
/// It is the object's only scale.
Last,
No,
}
fn remove_scale(d: &mut Drawing, h: Handle, name: &str) -> Removed {
let mut r = Removed::No;
let _ = d.modify_entity(h, |e| {
if !is_annotative(e) || !e.common.supports_scale(name) {
return;
}
if e.common.anno_scales.len() < 2 {
r = Removed::Last;
return;
}
e.common.anno_scales.retain(|s| !s.name.eq_ignore_ascii_case(name));
r = Removed::Yes;
});
r
}
/// The annotative objects among the targets (an error when nothing is selected).
fn annotative_targets(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<Handle>, usize)> {
let hs = targets(s, p)?;
if hs.is_empty() {
return Err(bad(cmd, "nothing selected"));
}
let d = s.doc()?;
let total = hs.len();
let anno: Vec<Handle> = hs.into_iter().filter(|h| d.entity(*h).is_some_and(|e| is_annotative(e))).collect();
Ok((anno, total))
}
fn objects_json(d: &Drawing, hs: &[Handle]) -> Value {
json!(
hs.iter()
.take(MAX_REPORTED)
.filter_map(|h| d.entity(*h))
.map(|e| json!({ "handle": e.handle.hex(), "scales": e.common.anno_scales.iter().map(|s| s.name.clone()).collect::<Vec<_>>() }))
.collect::<Vec<_>>()
)
}
fn run_objectscale(s: &mut Session, p: &Value) -> Result<Value> {
const CMD: &str = "objectscale";
let (hs, total) = annotative_targets(s, p, CMD)?;
let d = s.doc()?;
let add = names(CMD, p.get("add"))?.iter().map(|n| find_scale(d, CMD, n)).collect::<Result<Vec<_>>>()?;
let del = names(CMD, p.get("delete"))?;
let d = s.doc_mut()?;
let (mut changed, mut last) = (0usize, 0usize);
for h in &hs {
let mut any = false;
for sc in &add {
any |= add_scale(d, *h, sc);
}
for n in &del {
match remove_scale(d, *h, n) {
Removed::Yes => any = true,
Removed::Last => last += 1,
Removed::No => {}
}
}
changed += usize::from(any);
}
let mut msg = format!("{changed} of {total} object(s) changed");
if hs.len() < total {
msg.push_str(&format!(", {} not annotative", total - hs.len()));
}
if last > 0 {
msg.push_str(&format!(", {last} scale(s) kept as an object's only scale"));
}
Ok(json!({ "changed": changed, "annotative": hs.len(), "objects": objects_json(d, &hs), "message": msg }))
}
fn run_add_current(s: &mut Session, p: &Value) -> Result<Value> {
let cur = s.doc()?.current_anno_scale();
run_objectscale(s, &with_handles(s, p, json!({ "add": cur.name })))
}
fn run_remove_current(s: &mut Session, p: &Value) -> Result<Value> {
let cur = s.doc()?.current_anno_scale();
run_objectscale(s, &with_handles(s, p, json!({ "delete": cur.name })))
}
/// `extra` with the targets of `p` as `handles`.
fn with_handles(s: &Session, p: &Value, mut extra: Value) -> Value {
let hs = targets(s, p).unwrap_or_default();
if let Some(o) = extra.as_object_mut() {
o.insert("handles".into(), json!(hs.iter().map(|h| h.hex()).collect::<Vec<_>>()));
}
extra
}
fn run_annoreset(s: &mut Session, p: &Value) -> Result<Value> {
let (hs, _) = annotative_targets(s, p, "annoreset")?;
// One position serves every scale of an object, so they are always in step.
Ok(json!({ "objects": hs.len(), "message": format!("{} annotative object(s) synchronized", hs.len()) }))
}
fn run_annoupdate(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
if hs.is_empty() {
return Err(bad("annoupdate", "nothing selected"));
}
let d = s.doc()?;
let cur = d.current_anno_scale();
// Hatches have no style: HPANNOTATIVE only sets new ones.
let plan: Vec<(Handle, bool)> = hs
.iter()
.filter_map(|h| d.entity(*h))
.filter(|e| cadcraft_doc::can_be_annotative(&e.kind) && !matches!(e.kind, EntityKind::Hatch(_)))
.map(|e| (e.handle, d.annotative_by_style(&e.kind)))
.filter(|(h, want)| d.entity(*h).is_some_and(|e| is_annotative(e) != *want))
.collect();
let d = s.doc_mut()?;
for (h, want) in &plan {
d.modify_entity(*h, |e| cadcraft_doc::set_annotative(e, *want, &cur))?;
}
Ok(json!({ "updated": plan.len(), "message": format!("{} object(s) updated", plan.len()) }))
}
fn set_all_visible(s: &mut Session, on: Option<bool>) -> Result<Value> {
let d = s.doc_mut()?;
let on = on.unwrap_or(!d.anno_all_visible());
d.header.set_i64("ANNOALLVISIBLE", i64::from(on));
Ok(json!({ "annoallvisible": i64::from(on) }))
}
// ---------------- the scale list ----------------
/// Scales objects use, and the current one: they stay when the list is reset and can't be
/// deleted.
fn in_use(d: &Drawing) -> Vec<AnnoScale> {
let mut v = vec![d.current_anno_scale()];
let ents = d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter()));
for e in ents.filter(|e| is_annotative(e)) {
for sc in &e.common.anno_scales {
if v.len() < MAX_SCALES && !v.iter().any(|u| u.name.eq_ignore_ascii_case(&sc.name)) {
v.push(sc.clone());
}
}
}
v
}
fn add_to_list(d: &mut Drawing, sc: AnnoScale) -> Result<()> {
const CMD: &str = "scalelistedit";
let name = sc.name.trim();
if name.is_empty() || name.chars().count() > 255 {
return Err(bad(CMD, "a scale name has 1 to 255 characters"));
}
if !sc.is_valid() {
return Err(bad(CMD, "paper and drawing units must be positive"));
}
if d.anno_scale(name).is_some() {
return Err(bad(CMD, format!("a scale named `{name}` already exists")));
}
if d.scales.len() >= MAX_SCALES {
return Err(bad(CMD, "the scale list is full"));
}
d.scales.push(AnnoScale { name: name.to_string(), ..sc });
Ok(())
}
fn delete_from_list(d: &mut Drawing, name: &str) -> Result<()> {
const CMD: &str = "scalelistedit";
let sc = find_scale(d, CMD, name)?;
if d.current_anno_scale().name.eq_ignore_ascii_case(&sc.name) {
return Err(bad(CMD, format!("`{}` is the current annotation scale", sc.name)));
}
if in_use(d).iter().any(|u| u.name.eq_ignore_ascii_case(&sc.name)) {
return Err(bad(CMD, format!("`{}` is used by annotative objects", sc.name)));
}
d.scales.retain(|s| !s.name.eq_ignore_ascii_case(&sc.name));
Ok(())
}
/// Replace the list with the default metric or imperial one, keeping scales in use.
fn reset_list(d: &mut Drawing, metric: bool) {
let keep = in_use(d);
let mut list = if metric { cadcraft_doc::metric_scales() } else { cadcraft_doc::imperial_scales() };
for sc in keep {
if !list.iter().any(|l| l.name.eq_ignore_ascii_case(&sc.name)) {
list.push(sc);
}
}
d.scales = list;
}
fn run_scalelistedit(s: &mut Session, p: &Value) -> Result<Value> {
const CMD: &str = "scalelistedit";
let metric = s.doc()?.header.i64("MEASUREMENT", 0) == 1;
let reset = match p.get("reset") {
None | Some(Value::Null) | Some(Value::Bool(false)) => None,
Some(Value::Bool(true)) => Some(metric),
Some(Value::String(t)) if t.eq_ignore_ascii_case("metric") => Some(true),
Some(Value::String(t)) if t.eq_ignore_ascii_case("imperial") => Some(false),
Some(_) => return Err(bad(CMD, "`reset` is true, \"metric\" or \"imperial\"")),
};
let del = names(CMD, p.get("delete"))?;
let add = match p.get("add") {
None | Some(Value::Null) => None,
Some(a) => {
let name = a.get("name").and_then(Value::as_str).ok_or_else(|| bad(CMD, "`add.name` is required"))?;
let (pu, du) = match a.get("ratio").and_then(Value::as_str) {
Some(r) => parse_ratio(r).ok_or_else(|| bad(CMD, format!("`{r}` is not a ratio such as 1:50")))?,
None => (f64_req(CMD, a, "paperUnits")?, f64_req(CMD, a, "drawingUnits")?),
};
Some(AnnoScale::new(name, pu, du))
}
};
let d = s.doc_mut()?;
if let Some(m) = reset {
reset_list(d, m);
}
for n in &del {
delete_from_list(d, n)?;
}
if let Some(sc) = add {
add_to_list(d, sc)?;
}
Ok(list_json(d))
}
fn echo_list(s: &mut Session) {
let lines: Vec<String> = s
.doc()
.map(|d| {
d.scales
.iter()
.map(|sc| format!(" {} ({} paper = {} drawing)", sc.name, fmt_units(sc.paper_units), fmt_units(sc.drawing_units)))
.collect()
})
.unwrap_or_default();
for l in lines {
s.echo(l);
}
}
fn fmt_units(v: f64) -> String {
let r = (v * 1e6).round() / 1e6;
if r.fract() == 0.0 { format!("{r:.0}") } else { format!("{r}") }
}
#[derive(Default)]
enum ListStage {
#[default]
Option,
AddName,
AddRatio(String),
Delete,
Reset,
}
/// -SCALELISTEDIT: "Enter option [?/Add/Delete/Reset/Exit]", repeated until Exit or Enter.
#[derive(Default)]
struct ScaleListM {
stage: ListStage,
}
impl Interactive for ScaleListM {
fn name(&self) -> &'static str {
"SCALELISTEDIT"
}
fn prompt(&self, _s: &Session) -> Prompt {
match &self.stage {
ListStage::Option => Prompt::new("Enter option", Accept::TEXT).kw(&["?", "Add", "Delete", "Reset", "Exit"]).default("Exit"),
ListStage::AddName => Prompt::new("Enter name for new scale", Accept::TEXT),
ListStage::AddRatio(_) => Prompt::new("Enter scale ratio (paper units:drawing units)", Accept::TEXT).default("1:1"),
ListStage::Delete => Prompt::new("Enter name of scale to delete", Accept::TEXT).kw(&["?"]),
ListStage::Reset => Prompt::new("Reset the scale list to the defaults?", Accept::TEXT).kw(&["Yes", "No"]).default("No"),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
let text = match i {
Input::Cancel => return Ok(Step::Cancel),
Input::Enter => String::new(),
Input::Text(t) | Input::Keyword(t) => t.trim().to_string(),
_ => return Ok(Step::Continue),
};
let stage = std::mem::take(&mut self.stage);
let result = match stage {
ListStage::Option => match text.to_ascii_lowercase().as_str() {
"" | "e" | "exit" => return Ok(Step::Done),
"?" => {
echo_list(s);
Ok(())
}
"a" | "add" => {
self.stage = ListStage::AddName;
Ok(())
}
"d" | "delete" => {
self.stage = ListStage::Delete;
Ok(())
}
"r" | "reset" => {
self.stage = ListStage::Reset;
Ok(())
}
_ => {
s.echo("Invalid option keyword.");
Ok(())
}
},
ListStage::AddName if text.is_empty() => Ok(()),
ListStage::AddName => {
if s.doc()?.anno_scale(&text).is_some() {
s.echo(format!("A scale named \"{text}\" already exists."));
} else {
self.stage = ListStage::AddRatio(text);
}
Ok(())
}
ListStage::AddRatio(name) => {
let t = if text.is_empty() { "1:1" } else { text.as_str() };
match parse_ratio(t) {
Some((pu, du)) => add_to_list(s.doc_mut()?, AnnoScale::new(&name, pu, du)),
None => {
s.echo("Enter a ratio such as 1:50.");
self.stage = ListStage::AddRatio(name);
Ok(())
}
}
}
ListStage::Delete => match text.as_str() {
"" => Ok(()),
"?" => {
echo_list(s);
self.stage = ListStage::Delete;
Ok(())
}
n => delete_from_list(s.doc_mut()?, n),
},
ListStage::Reset => {
if matches!(text.to_ascii_lowercase().as_str(), "y" | "yes") {
let metric = s.doc()?.header.i64("MEASUREMENT", 0) == 1;
reset_list(s.doc_mut()?, metric);
}
Ok(())
}
};
if let Err(e) = result {
s.echo(e.to_string());
}
Ok(Step::Continue)
}
}
#[derive(Default, Clone, Copy, PartialEq)]
enum ObjStage {
#[default]
Select,
Option,
Add,
Delete,
}
/// -OBJECTSCALE: select annotative objects, then add or delete scales by name until Enter.
#[derive(Default)]
struct ObjectScaleM {
sel: SelectPhase,
stage: ObjStage,
hs: Vec<Handle>,
}
impl ObjectScaleM {
/// Keep the annotative objects of the selection; false when there are none.
fn take(&mut self, s: &mut Session, picked: Vec<Handle>) -> Result<bool> {
let d = s.doc()?;
self.hs = picked.into_iter().filter(|h| d.entity(*h).is_some_and(|e| is_annotative(e))).collect();
if self.hs.is_empty() {
s.echo("No annotative objects selected.");
return Ok(false);
}
self.stage = ObjStage::Option;
Ok(true)
}
fn apply(&self, s: &mut Session, key: &str, name: &str) {
let mut p = json!({ "handles": self.hs.iter().map(|h| h.hex()).collect::<Vec<_>>() });
if let Some(o) = p.as_object_mut() {
o.insert(key.to_string(), json!(name));
}
match run_objectscale(s, &p) {
Ok(r) => {
if let Some(m) = r.get("message").and_then(Value::as_str) {
s.echo(m.to_string());
}
}
Err(e) => s.echo(e.to_string()),
}
}
fn echo_supported(&self, s: &mut Session) {
let Ok(d) = s.doc() else { return };
let mut names: Vec<String> = Vec::new();
for e in self.hs.iter().filter_map(|h| d.entity(*h)) {
for sc in &e.common.anno_scales {
if names.len() < MAX_SCALES && !names.iter().any(|n| n.eq_ignore_ascii_case(&sc.name)) {
names.push(sc.name.clone());
}
}
}
s.echo(format!("Scales of the selected objects: {}", names.join(", ")));
}
}
impl Interactive for ObjectScaleM {
fn name(&self) -> &'static str {
"OBJECTSCALE"
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
self.sel = SelectPhase::begin(s);
if self.sel.done {
let picked = self.sel.picked.clone();
if !self.take(s, picked)? {
return Ok(Step::Done);
}
}
Ok(Step::Continue)
}
fn prompt(&self, s: &Session) -> Prompt {
let cur = s.doc().map(|d| d.current_anno_scale().name).unwrap_or_else(|_| "1:1".into());
match self.stage {
ObjStage::Select => Prompt::new(if self.sel.removing { "Remove objects" } else { "Select annotative objects" }, Accept::SELECT),
ObjStage::Option => Prompt::new("Enter an option", Accept::TEXT).kw(&["Add", "Delete", "?"]),
ObjStage::Add => Prompt::new("Enter named scale to add", Accept::TEXT).kw(&["?"]).default(cur),
ObjStage::Delete => Prompt::new("Enter named scale to delete", Accept::TEXT).kw(&["?"]).default(cur),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if i == Input::Cancel {
return Ok(Step::Cancel);
}
if self.stage == ObjStage::Select {
return match self.sel.feed(s, &i)? {
SelOutcome::More => Ok(Step::Continue),
SelOutcome::Empty => Ok(Step::Done),
SelOutcome::Done(hs) => Ok(if self.take(s, hs)? { Step::Continue } else { Step::Done }),
};
}
let text = match i {
Input::Enter => String::new(),
Input::Text(t) | Input::Keyword(t) => t.trim().to_string(),
_ => return Ok(Step::Continue),
};
match self.stage {
ObjStage::Option => match text.to_ascii_lowercase().as_str() {
"" => return Ok(Step::Done),
"a" | "add" => self.stage = ObjStage::Add,
"d" | "delete" => self.stage = ObjStage::Delete,
"?" => self.echo_supported(s),
_ => s.echo("Invalid option keyword."),
},
stage @ (ObjStage::Add | ObjStage::Delete) => {
if text == "?" {
echo_list(s);
return Ok(Step::Continue);
}
let name = if text.is_empty() { s.doc()?.current_anno_scale().name } else { text };
self.apply(s, if stage == ObjStage::Add { "add" } else { "delete" }, &name);
self.stage = ObjStage::Option;
}
ObjStage::Select => {}
}
Ok(Step::Continue)
}
}
+2 -1
View File
@@ -466,7 +466,8 @@ fn mleader_kind(s: &Session, pts: &[Vec2], text: &str, landing: bool) -> Option<
let d = s.doc().ok()?;
let cur = d.header.str("CMLEADERSTYLE", "Standard");
let st = d.mleader_styles.iter().find(|m| m.name.eq_ignore_ascii_case(&cur)).or(d.mleader_styles.first()).cloned().unwrap_or_default();
let k = d.header.f64("DIMSCALE", 1.0);
// An annotative style's sizes are paper sizes: the annotation scale applies when drawn.
let k = if st.annotative { 1.0 } else { d.header.f64("DIMSCALE", 1.0) };
let th = st.text_height * k;
let dir = if arrow_pts.first().is_some_and(|a| a.x > landing_pt.x) { -1.0 } else { 1.0 };
let attach = if dir > 0.0 { 4 } else { 6 };
+12 -1
View File
@@ -21,7 +21,7 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("block", "Make...", run_block)
.menu(&["Draw", "Block", "Make..."])
.alias(&["b", "bmake"])
.params("{name, base: [x,y], handles?, keep?: \"convert\"|\"retain\"|\"delete\" (default \"convert\"), description?}")
.params("{name, base: [x,y], handles?, keep?: \"convert\"|\"retain\"|\"delete\" (default \"convert\"), description?, annotative?}")
.interactive(|_| Ok(Box::new(BlockM::default()))),
// -BLOCK erases the selected objects once the block is defined.
CommandSpec::new("-block", "Create Block", run_dash_block)
@@ -189,6 +189,17 @@ fn run_block(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
let keep = str_param(p, "keep").unwrap_or("convert").to_string();
let r = make_block(s, &name, base, &hs, &keep, str_param(p, "description").unwrap_or(""))?;
// An annotative block: its references (the converted selection too) are annotative.
if bool_or(p, "annotative", false) {
let d = s.doc_mut()?;
let cur = d.current_anno_scale();
if let Some(b) = d.blocks.iter_mut().find(|(k, _)| k.eq_ignore_ascii_case(&name)).map(|(_, b)| b) {
Arc::make_mut(b).annotative = true;
}
if let Some(h) = r {
d.modify_entity(h, |e| cadcraft_doc::set_annotative(e, true, &cur))?;
}
}
Ok(json!({ "block": name, "insert": r.map(|h| h.hex()) }))
}
+11 -6
View File
@@ -1414,11 +1414,10 @@ impl Interactive for TextM {
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
let d = s.doc()?;
let msg = format!(
"Current text style: \"{}\" Text height: {:.4} Annotative: No Justify: Left",
d.header.str("TEXTSTYLE", "Standard"),
d.header.f64("TEXTSIZE", 0.2)
);
let style = d.header.str("TEXTSTYLE", "Standard");
let anno = if d.text_style(&style).is_some_and(|st| st.annotative) { "Yes" } else { "No" };
let msg =
format!("Current text style: \"{style}\" Text height: {:.4} Annotative: {anno} Justify: Left", d.header.f64("TEXTSIZE", 0.2));
s.echo(msg);
Ok(Step::Continue)
}
@@ -1487,7 +1486,13 @@ impl Interactive for MTextM {
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
self.opts = super::mtext_opts::MTextOpts::new(s);
s.echo(format!("Current text style: \"{}\" Text height: {:.4} Annotative: No", self.opts.style, self.opts.height));
let anno = s.doc()?.text_style(&self.opts.style).is_some_and(|st| st.annotative);
s.echo(format!(
"Current text style: \"{}\" Text height: {:.4} Annotative: {}",
self.opts.style,
self.opts.height,
if anno { "Yes" } else { "No" }
));
Ok(Step::Continue)
}
fn prompt(&self, _s: &Session) -> Prompt {
+2
View File
@@ -1,6 +1,7 @@
//! The command registry. Ids are AutoCAD command names in lower case; menu paths follow
//! AutoCAD's menu bar so the catalog doubles as the parity metric.
pub(crate) mod annoscale;
mod annotate;
mod array;
mod audit;
@@ -172,6 +173,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(draw::specs());
v.extend(draw2::specs());
v.extend(annotate::specs());
v.extend(annoscale::specs());
v.extend(hatch::specs());
v.extend(overkill::specs());
v.extend(blocks::specs());
+9
View File
@@ -1134,6 +1134,15 @@ fn run_chamfer(s: &mut Session, p: &Value) -> Result<Value> {
// ---------------- explode / array / order / break / join ----------------
pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<Entity>> {
// An annotative object explodes into plain objects as it draws at the current annotation scale.
if cadcraft_doc::is_annotative(e) {
let mut se = match d.anno_draw(e) {
cadcraft_doc::AnnoDraw::Scaled(f) => cadcraft_doc::anno_scaled(e, f),
_ => e.clone(),
};
se.common.anno_scales.clear();
return explode_kind(d, &se);
}
match &e.kind {
EntityKind::LwPolyline(p) => {
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
+15 -1
View File
@@ -54,7 +54,7 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("mleaderstyle", "Multileader Style...", run_mleaderstyle)
.menu(&["Format", "Multileader Style..."])
.alias(&["mls"])
.params("{name, arrowSize?, textHeight?, landingGap?, dogleg?, textStyle?, current?} → styles"),
.params("{name, arrowSize?, textHeight?, landingGap?, dogleg?, textStyle?, annotative?, current?} → styles"),
CommandSpec::new("ddptype", "Point Style...", run_ptype).menu(&["Format", "Point Style..."]).params("{pdmode, pdsize}"),
CommandSpec::new("rename", "Rename...", run_rename)
.menu(&["Format", "Rename..."])
@@ -76,6 +76,12 @@ pub(super) fn entity_props(d: &cadcraft_doc::Drawing, e: &cadcraft_doc::Entity)
"transparency": transparency_value(e.common.transparency),
"visible": e.common.visible,
});
if cadcraft_doc::can_be_annotative(&e.kind)
&& let Some(o) = v.as_object_mut()
{
o.insert("annotative".into(), json!(cadcraft_doc::is_annotative(e)));
o.insert("annotativeScales".into(), json!(e.common.anno_scales.iter().map(|s| s.name.clone()).collect::<Vec<_>>()));
}
let geo = serde_json::to_value(&e.kind).unwrap_or(Value::Null);
if let (Some(o), Some(g)) = (v.as_object_mut(), geo.as_object()) {
o.insert("geometry".into(), Value::Object(g.clone()));
@@ -188,8 +194,13 @@ fn run_set(s: &mut Session, p: &Value) -> Result<Value> {
let dim_style = str_param(p, "dimStyle")
.map(|n| d.dim_style(n).map(|st| st.name.clone()).ok_or_else(|| bad("properties.set", format!("dimension style `{n}` not found"))))
.transpose()?;
let anno_scale = d.current_anno_scale();
for h in &hs {
d.modify_entity(*h, |e| {
// Annotative: Yes supports the current annotation scale; No drops every scale.
if let Some(a) = p.get("annotative").and_then(Value::as_bool) {
cadcraft_doc::set_annotative(e, a, &anno_scale);
}
if let Some(l) = str_param(p, "layer") {
e.common.layer = l.to_string();
}
@@ -831,6 +842,9 @@ fn run_mleaderstyle(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(t) = str_param(p, "textStyle") {
st.text_style = t.to_string();
}
if let Some(a) = p.get("annotative").and_then(Value::as_bool) {
st.annotative = a;
}
}
if bool_or(p, "current", true) {
d.header.set_str("CMLEADERSTYLE", name);
+6
View File
@@ -656,6 +656,12 @@ impl Session {
if let Some(rgb) = true_color {
common.color = cadcraft_color::Color::True(cadcraft_color::Rgb::from_u32(rgb as u32));
}
// Text, dimensions, multileaders, hatches and block references of annotative styles are
// annotative, supporting the current annotation scale.
let d = self.doc()?;
if d.annotative_by_style(&kind) {
common.anno_scales = vec![d.current_anno_scale()];
}
let h = self.doc_mut()?.add(&space, common, kind)?;
Ok(h)
}
+10
View File
@@ -71,6 +71,8 @@ pub struct SpatialIndex {
blocks: BTreeMap<String, Arc<Block>>,
dim_styles: Vec<DimStyle>,
dimscale: f64,
/// Annotation scale factor and ANNOALLVISIBLE: annotative objects measure at that scale.
anno: (u64, bool),
/// (draw index of the chunk's first entity, chunk), in draw order.
chunks: Vec<(u32, Arc<ChunkIx>)>,
/// Chunk serial → draw offset.
@@ -81,6 +83,11 @@ pub struct SpatialIndex {
len: usize,
}
/// What annotative objects' bounds depend on besides the objects themselves.
fn anno_key(d: &Drawing) -> (u64, bool) {
(d.current_anno_scale().factor().to_bits(), d.anno_all_visible())
}
impl SpatialIndex {
/// Build an index over a store (no caching).
pub fn build(d: &Drawing, store: &EntityStore) -> SpatialIndex {
@@ -138,6 +145,7 @@ impl SpatialIndex {
blocks: d.blocks.clone(),
dim_styles: d.dim_styles.clone(),
dimscale: d.header.f64("DIMSCALE", 1.0),
anno: anno_key(d),
chunks,
offsets,
tree,
@@ -151,6 +159,7 @@ impl SpatialIndex {
&& self.blocks.iter().zip(&d.blocks).all(|((ka, a), (kb, b))| ka == kb && Arc::ptr_eq(a, b))
&& self.dimscale.to_bits() == d.header.f64("DIMSCALE", 1.0).to_bits()
&& self.dim_styles == d.dim_styles
&& self.anno == anno_key(d)
}
fn matches(&self, d: &Drawing, store: &EntityStore) -> bool {
@@ -271,6 +280,7 @@ mod tests {
let mut d = Drawing::new_imperial();
let mut blk = Block {
name: "B".into(),
annotative: false,
base: Vec3::default(),
entities: EntityStore::new(),
description: String::new(),
+9
View File
@@ -9,6 +9,7 @@ mod table;
const SESSION_VARS: &[&str] = &[
"OSMODE",
"OSNAPHATCH",
"ANNOAUTOSCALE",
"ORTHOMODE",
"POLARMODE",
"POLARANG",
@@ -51,6 +52,10 @@ pub fn get(s: &Session, name: &str) -> Option<Value> {
let v = match n.as_str() {
"OSMODE" => json!(st.osmode),
"OSNAPHATCH" => json!(i32::from(st.osnaphatch)),
// AutoCAD's values: positive on, negative off (4 = every annotative object).
"ANNOAUTOSCALE" => json!(if st.annoautoscale { 4 } else { -4 }),
// Paper units per drawing unit of the current annotation scale (1:50 → 0.02).
"CANNOSCALEVALUE" => json!(1.0 / s.doc().ok()?.current_anno_scale().factor()),
"ORTHOMODE" => json!(i32::from(st.orthomode)),
"POLARMODE" => json!(st.polar_flags),
"POLARADDANG" => json!(format_angle_list(&st.polaraddang)),
@@ -177,6 +182,10 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {
match n.as_str() {
"OSMODE" => st.osmode = v.as_u64().or_else(|| v.as_str().and_then(|s| s.trim().parse().ok())).ok_or_else(bad)? as u32 & 0x7fff,
"OSNAPHATCH" => st.osnaphatch = as_bool(v).ok_or_else(bad)?,
"ANNOAUTOSCALE" => st.annoautoscale = as_i(v).filter(|i| (-4..=4).contains(i)).ok_or_else(bad)? > 0,
// A scale of the drawing's list; ANNOAUTOSCALE adds it to the annotative objects.
"CANNOSCALE" => crate::cmd::annoscale::set_current(s, v.as_str().ok_or_else(bad)?)?,
"CANNOSCALEVALUE" => return Err(EngineError::BadParams { cmd: "setvar".into(), msg: format!("{n} is read-only") }),
"ORTHOMODE" => st.orthomode = as_bool(v).ok_or_else(bad)?,
"POLARMODE" => st.polar_flags = as_i(v).filter(|b| (0..=15).contains(b)).ok_or_else(bad)? as u32,
"POLARADDANG" => {
+285
View File
@@ -0,0 +1,285 @@
//! Annotative scaling (#468): annotative objects keep their paper size at every annotation
//! scale, show only at scales they support unless ANNOALLVISIBLE is 1, and the scale list,
//! OBJECTSCALE, ANNOAUTOSCALE and ANNOUPDATE manage the scales.
use cadcraft_engine::Session;
use cadcraft_engine::doc::{AnnoDraw, Drawing, Entity, EntityKind, Handle, Space, entity_bounds};
use serde_json::{Value, json};
fn setvar(s: &mut Session, name: &str, value: Value) {
s.execute("setvar", &json!({ "name": name, "value": value })).unwrap();
}
/// A session whose current text style is annotative, with one text 0.25 high on paper.
fn with_text() -> (Session, Handle) {
let mut s = Session::new();
s.execute("style", &json!({ "name": "Notes", "annotative": true, "current": true })).unwrap();
let r = s.execute("text", &json!({ "at": [0, 0], "text": "NOTE", "height": 0.25 })).unwrap();
let h = Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap();
(s, h)
}
fn entity(s: &Session, h: Handle) -> Entity {
(**s.doc().unwrap().entity(h).unwrap()).clone()
}
fn scales(s: &Session, h: Handle) -> Vec<String> {
entity(s, h).common.anno_scales.iter().map(|x| x.name.clone()).collect()
}
fn height(d: &Drawing, h: Handle) -> f64 {
let e = d.entity(h).unwrap();
entity_bounds(d, e, 0).height()
}
/// Height of what the renderer draws for `h` in model space.
fn drawn_height(d: &Drawing, h: Handle) -> Option<f64> {
let list = cadcraft_engine::render::build(d, &Space::Model, &Default::default());
let mut b = cadcraft_engine::geom::Bounds2::EMPTY;
for p in list.prims.iter().filter(|p| p.handle == h) {
for q in list.points(p) {
b.add(*q);
}
}
(!b.is_empty()).then(|| b.height())
}
#[test]
fn text_of_an_annotative_style_supports_the_current_scale() {
let (s, h) = with_text();
assert_eq!(scales(&s, h), ["1:1"]);
let mut s = s;
let p = s.execute("properties", &json!({ "handles": [h.hex()] })).unwrap();
assert_eq!(p["objects"][0]["annotative"], json!(true), "{p}");
assert_eq!(p["objects"][0]["annotativeScales"], json!(["1:1"]), "{p}");
// A plain style makes plain text.
let mut s = Session::new();
let r = s.execute("text", &json!({ "at": [0, 0], "text": "X", "height": 0.25 })).unwrap();
let h = Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap();
assert!(scales(&s, h).is_empty());
}
#[test]
fn annotative_text_draws_at_paper_height_times_the_scale() {
let (mut s, h) = with_text();
let at_1 = drawn_height(s.doc().unwrap(), h).unwrap();
setvar(&mut s, "CANNOSCALE", json!("1:50"));
s.execute("objectscale", &json!({ "handles": [h.hex()], "add": "1:50" })).unwrap();
assert_eq!(scales(&s, h), ["1:1", "1:50"]);
let at_50 = drawn_height(s.doc().unwrap(), h).unwrap();
assert!((at_50 / at_1 - 50.0).abs() < 1e-6, "{at_1} → {at_50}");
// Bounds (zoom, selection) follow the drawn size; the stored height stays the paper height.
assert!((height(s.doc().unwrap(), h) / at_50 - 1.0).abs() < 0.5);
match entity(&s, h).kind {
EntityKind::Text(t) => assert_eq!(t.height, 0.25),
k => panic!("{k:?}"),
}
}
#[test]
fn unsupported_scale_hides_unless_annoallvisible() {
let (mut s, h) = with_text();
setvar(&mut s, "CANNOSCALE", json!("1:20"));
// ANNOALLVISIBLE 1 (the default): shown, at the object's own scale.
assert_eq!(s.doc().unwrap().anno_draw(&entity(&s, h)), AnnoDraw::Scaled(1.0));
assert!(drawn_height(s.doc().unwrap(), h).is_some());
s.execute("annovisible.current", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().header.i64("ANNOALLVISIBLE", 1), 0);
assert_eq!(drawn_height(s.doc().unwrap(), h), None);
assert!(entity_bounds(s.doc().unwrap(), &entity(&s, h), 0).is_empty());
// Back at a supported scale it shows again.
setvar(&mut s, "CANNOSCALE", json!("1:1"));
assert!(drawn_height(s.doc().unwrap(), h).is_some());
s.execute("annovisible.toggle", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().header.i64("ANNOALLVISIBLE", 0), 1);
}
#[test]
fn annoautoscale_adds_the_new_scale() {
let (mut s, h) = with_text();
setvar(&mut s, "ANNOAUTOSCALE", json!(4));
setvar(&mut s, "CANNOSCALE", json!("1:10"));
assert_eq!(scales(&s, h), ["1:1", "1:10"]);
let v = s.execute("getvar", &json!({ "name": "CANNOSCALEVALUE" })).unwrap();
assert!((v["value"].as_f64().unwrap() - 0.1).abs() < 1e-12, "{v}");
assert!(s.execute("setvar", &json!({ "name": "CANNOSCALEVALUE", "value": 1 })).is_err());
setvar(&mut s, "ANNOAUTOSCALE", json!(-4));
setvar(&mut s, "CANNOSCALE", json!("1:20"));
assert_eq!(scales(&s, h), ["1:1", "1:10"]);
// A scale that isn't in the list is refused.
assert!(s.execute("setvar", &json!({ "name": "CANNOSCALE", "value": "1:7" })).is_err());
assert!(s.execute("setvar", &json!({ "name": "CANNOSCALE", "value": 7 })).is_err());
}
#[test]
fn add_and_delete_current_scale_keep_one_scale() {
let (mut s, h) = with_text();
setvar(&mut s, "CANNOSCALE", json!("1:4"));
s.execute("aiobjectscaleadd", &json!({ "handles": [h.hex()] })).unwrap();
assert_eq!(scales(&s, h), ["1:1", "1:4"]);
s.execute("aiobjectscaleremove", &json!({ "handles": [h.hex()] })).unwrap();
assert_eq!(scales(&s, h), ["1:1"]);
// The last scale stays.
setvar(&mut s, "CANNOSCALE", json!("1:1"));
let r = s.execute("aiobjectscaleremove", &json!({ "handles": [h.hex()] })).unwrap();
assert_eq!(scales(&s, h), ["1:1"]);
assert!(r["message"].as_str().unwrap().contains("only scale"), "{r}");
assert!(s.execute("objectscale", &json!({ "handles": [h.hex()], "add": "no such" })).is_err());
}
#[test]
fn objectscale_at_the_command_line() {
let (mut s, h) = with_text();
// The pickfirst selection is used.
s.set_selection(vec![h]);
s.script("-OBJECTSCALE\nA\n1:2\nA\n1:8\nD\n1:1\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(scales(&s, h), ["1:2", "1:8"]);
}
#[test]
fn scale_list_add_delete_reset() {
let (mut s, h) = with_text();
let r = s.execute("scalelistedit", &json!({ "add": { "name": "Site", "ratio": "1:250" } })).unwrap();
assert!(r["scales"].as_array().unwrap().iter().any(|x| x["name"] == "Site" && x["drawingUnits"] == 250.0), "{r}");
assert!(s.execute("scalelistedit", &json!({ "add": { "name": "site", "ratio": "1:3" } })).is_err());
assert!(s.execute("scalelistedit", &json!({ "add": { "name": "Bad", "ratio": "0:3" } })).is_err());
assert!(s.execute("scalelistedit", &json!({ "add": { "name": "Bad", "paperUnits": 1, "drawingUnits": -2 } })).is_err());
// In use by the text, or current: can't be deleted.
s.execute("objectscale", &json!({ "handles": [h.hex()], "add": "Site" })).unwrap();
assert!(s.execute("scalelistedit", &json!({ "delete": "Site" })).is_err());
assert!(s.execute("scalelistedit", &json!({ "delete": "1:1" })).is_err());
s.execute("scalelistedit", &json!({ "delete": "1:100" })).unwrap();
assert!(s.doc().unwrap().anno_scale("1:100").is_none());
// Reset brings the defaults back and keeps scales in use.
let r = s.execute("scalelistedit", &json!({ "reset": "metric" })).unwrap();
let names: Vec<&str> = r["scales"].as_array().unwrap().iter().filter_map(|x| x["name"].as_str()).collect();
assert!(names.contains(&"1:100") && names.contains(&"Site") && !names.iter().any(|n| n.contains('"')), "{names:?}");
let r = s.execute("scalelistedit", &json!({ "reset": "imperial" })).unwrap();
assert!(r["scales"].as_array().unwrap().iter().any(|x| x["name"] == "1/4\" = 1'-0\"" && x["drawingUnits"] == 12.0));
}
#[test]
fn scalelistedit_at_the_command_line() {
let mut s = Session::new();
s.script("-SCALELISTEDIT\nA\nDetail\n5:1\nD\n1:16\nE\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
let d = s.doc().unwrap();
let detail = d.anno_scale("detail").unwrap();
assert_eq!((detail.paper_units, detail.drawing_units), (5.0, 1.0));
assert!(d.anno_scale("1:16").is_none());
// Bad input is reported, not fatal.
s.script("-SCALELISTEDIT\nA\nOdd\nfoo\n1:3\nD\nnothing\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(s.doc().unwrap().anno_scale("Odd").map(|x| x.drawing_units), Some(3.0));
s.script("-SCALELISTEDIT\nR\nY\nX\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert!(s.doc().unwrap().anno_scale("1:16").is_some(), "reset restores the defaults");
assert!(s.doc().unwrap().anno_scale("Odd").is_none(), "an unused scale goes");
}
#[test]
fn annotative_dimension_scales_its_text_and_arrows() {
let mut s = Session::new();
s.execute("dimstyle", &json!({ "name": "Anno", "annotative": true, "current": true })).unwrap();
let r = s.execute("dimlinear", &json!({ "p1": [0, 0], "p2": [100, 0], "at": [50, 20] })).unwrap();
let h = Handle::parse_hex(r["handle"].as_str().or_else(|| r["handles"][0].as_str()).unwrap()).unwrap();
assert_eq!(scales(&s, h), ["1:1"]);
let at_1 = drawn_height(s.doc().unwrap(), h).unwrap();
s.execute("scalelistedit", &json!({ "add": { "name": "1:3", "ratio": "1:3" } })).ok();
setvar(&mut s, "ANNOAUTOSCALE", json!(1));
setvar(&mut s, "CANNOSCALE", json!("1:4"));
let at_4 = drawn_height(s.doc().unwrap(), h).unwrap();
// The dimension stays 20 tall between its points and dimension line; the extension line
// offset and overshoot (DIMEXO, DIMEXE) are 4× their paper size.
assert!(((at_4 - 20.0) / (at_1 - 20.0) - 4.0).abs() < 1e-6, "{at_1} → {at_4}");
}
#[test]
fn annotative_multileader_and_hatch() {
let mut s = Session::new();
s.execute("mleaderstyle", &json!({ "name": "Anno", "annotative": true })).unwrap();
let r = s.execute("mleader", &json!({ "points": [[0, 0], [5, 5]], "text": "A" })).unwrap();
let ml = Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap();
assert_eq!(scales(&s, ml), ["1:1"]);
setvar(&mut s, "HPANNOTATIVE", json!(1));
s.execute("rectang", &json!({ "p1": [20, 0], "p2": [30, 10] })).unwrap();
let r = s.execute("hatch", &json!({ "points": [[25, 5]], "pattern": "ANSI31" })).unwrap();
let hh = r["handle"].as_str().or_else(|| r["handles"][0].as_str()).and_then(Handle::parse_hex).unwrap();
assert_eq!(scales(&s, hh), ["1:1"]);
// At 1:2 the hatch pattern doubles: fewer, longer lines over the same area.
let count = |s: &Session| {
let d = s.doc().unwrap();
let list = cadcraft_engine::render::build(d, &Space::Model, &Default::default());
list.prims.iter().filter(|p| p.handle == hh).count()
};
let n1 = count(&s);
setvar(&mut s, "ANNOAUTOSCALE", json!(1));
setvar(&mut s, "CANNOSCALE", json!("1:2"));
let n2 = count(&s);
assert!(n2 < n1, "{n1} → {n2}");
}
#[test]
fn explode_gives_plain_objects_at_the_current_scale() {
let (mut s, h) = with_text();
s.execute("objectscale", &json!({ "handles": [h.hex()], "add": "1:10" })).unwrap();
setvar(&mut s, "CANNOSCALE", json!("1:10"));
// Text doesn't explode; a block reference of an annotative block does.
s.execute("block", &json!({ "name": "TAG", "base": [0, 0], "handles": [h.hex()], "annotative": true })).unwrap();
let ins = s.doc().unwrap().model.last().unwrap().handle;
assert_eq!(scales(&s, ins), ["1:10"]);
s.execute("explode", &json!({ "handles": [ins.hex()] })).unwrap();
let d = s.doc().unwrap();
let t = d.model.iter().find(|e| matches!(e.kind, EntityKind::Text(_))).unwrap();
match &t.kind {
EntityKind::Text(t) => assert!((t.height - 2.5).abs() < 1e-9, "{}", t.height),
_ => unreachable!(),
}
}
#[test]
fn annoupdate_and_properties_follow_the_style() {
let mut s = Session::new();
s.execute("style", &json!({ "name": "Notes", "current": true })).unwrap();
let r = s.execute("text", &json!({ "at": [0, 0], "text": "N", "height": 0.25 })).unwrap();
let h = Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap();
assert!(scales(&s, h).is_empty());
s.execute("style", &json!({ "name": "Notes", "annotative": true })).unwrap();
s.execute("annoupdate", &json!({ "handles": [h.hex()] })).unwrap();
assert_eq!(scales(&s, h), ["1:1"]);
s.execute("properties.set", &json!({ "handles": [h.hex()], "annotative": false })).unwrap();
assert!(scales(&s, h).is_empty());
s.execute("properties.set", &json!({ "handles": [h.hex()], "annotative": true })).unwrap();
assert_eq!(scales(&s, h), ["1:1"]);
let r = s.execute("annoreset", &json!({ "handles": [h.hex()] })).unwrap();
assert_eq!(r["objects"], 1);
// Lines can't be annotative.
let r = s.execute("line", &json!({ "points": [[0, 0], [1, 1]] })).unwrap();
let l = r["handles"][0].as_str().or_else(|| r["handle"].as_str()).and_then(Handle::parse_hex).unwrap();
s.execute("properties.set", &json!({ "handles": [l.hex()], "annotative": true })).unwrap();
assert!(scales(&s, l).is_empty());
}
#[test]
fn hostile_parameters_are_refused() {
let (mut s, h) = with_text();
for p in [
json!({ "handles": [h.hex()], "add": 5 }),
json!({ "handles": [h.hex()], "delete": [1, 2] }),
json!({ "handles": "x" }),
json!({ "handles": [] }),
] {
assert!(s.execute("objectscale", &p).is_err(), "{p}");
}
for p in [
json!({ "add": { "name": "", "ratio": "1:2" } }),
json!({ "add": { "name": "x", "ratio": "1:inf" } }),
json!({ "add": { "name": "x", "paperUnits": 1e308, "drawingUnits": 1e-308 } }),
json!({ "add": "1:2" }),
json!({ "reset": 3 }),
json!({ "delete": "missing" }),
] {
assert!(s.execute("scalelistedit", &p).is_err(), "{p}");
}
}
+1
View File
@@ -1746,6 +1746,7 @@ impl Objects {
landing_gap: num(42, def.landing_gap, 0.0),
dogleg: num(43, def.dogleg, 0.0),
text_style: text_style.unwrap_or(def.text_style),
annotative: false,
};
match d.mleader_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(name)) {
Some(x) => *x = st,
+9 -2
View File
@@ -62,8 +62,15 @@ fn linetype_text_and_shape_elements_round_trip() {
fn multileader_styles_and_current_style_round_trip() {
let mut d = Drawing::new_imperial();
d.text_styles.push(TextStyle { name: "Slanted".into(), oblique: 0.2, ..TextStyle::default() });
let callout =
MLeaderStyle { name: "Callout".into(), arrow_size: 2.0, text_height: 3.0, landing_gap: 1.0, dogleg: 5.0, text_style: "Slanted".into() };
let callout = MLeaderStyle {
name: "Callout".into(),
arrow_size: 2.0,
text_height: 3.0,
landing_gap: 1.0,
dogleg: 5.0,
text_style: "Slanted".into(),
annotative: false,
};
d.mleader_styles.push(callout);
d.header.set_str("CMLEADERSTYLE", "Callout");
let r = round_trip(&d);
+18
View File
@@ -581,6 +581,24 @@ fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
}
fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) {
// A top-level annotative object draws at the view's annotation scale: a viewport's scale for
// model space seen through it, else the drawing's (CANNOSCALE in model space, 1:1 on paper).
if ctx.depth == 0 && cadcraft_doc::is_annotative(e) {
let mode = if ctx.top == VIEWPORT_CONTENT {
cadcraft_doc::anno_draw(e, 1.0 / ctx.xf.scale_factor().max(1e-12), ctx.d.anno_all_visible())
} else {
ctx.d.anno_draw(e)
};
match mode {
cadcraft_doc::AnnoDraw::Hidden => return,
cadcraft_doc::AnnoDraw::Scaled(f) => {
let mut scaled = cadcraft_doc::anno_scaled(e, f);
scaled.common.anno_scales.clear();
return entity_parts(b, ctx, &scaled);
}
cadcraft_doc::AnnoDraw::Plain => {}
}
}
let (rgb, lw, lt, ltscale, visible) = resolve(ctx, e, b.plotting);
if !visible {
return;
@@ -0,0 +1,86 @@
//! Annotative objects seen through layout viewports (#468): they draw at their paper size in
//! every viewport whose scale they support, and not at all in others when ANNOALLVISIBLE is 0.
use cadcraft_doc::{AnnoScale, Common, Drawing, EntityKind, Space, Text, Viewport};
use cadcraft_geom::{Bounds2, Vec3};
use cadcraft_render::{Options, VIEWPORT_CONTENT, build};
/// A viewport on Layout1, 100 × 100 paper units, showing a model window `100 × scale`.
fn viewport(x: f64, scale: f64) -> Viewport {
serde_json::from_value(serde_json::json!({
"center": [x, 50.0, 0.0],
"width": 100.0,
"height": 100.0,
"viewCenter": [0.0, 0.0],
"viewHeight": 100.0 * scale,
"id": 2,
}))
.unwrap()
}
/// A metric drawing with annotative text 2.5 high on paper supporting 1:50 and 1:100, seen
/// through one viewport at `scale`.
fn drawing(scale: f64, all_visible: bool) -> Drawing {
let mut d = Drawing::new_metric();
d.header.set_i64("ANNOALLVISIBLE", i64::from(all_visible));
let anno = Common { anno_scales: vec![AnnoScale::new("1:50", 1.0, 50.0), AnnoScale::new("1:100", 1.0, 100.0)], ..Default::default() };
let text = Text {
insert: Vec3::ZERO,
align_pt: None,
height: 2.5,
value: "ROOM".into(),
rotation: 0.0,
width_factor: 1.0,
oblique: 0.0,
style: "Standard".into(),
halign: Default::default(),
valign: Default::default(),
};
d.add(&Space::Model, anno, EntityKind::Text(text)).unwrap();
d.add(&Space::Paper("Layout1".into()), Common::default(), EntityKind::Viewport(viewport(50.0, scale))).unwrap();
d
}
/// Bounds of the model-space content drawn in the viewport, in paper units.
fn content(d: &Drawing) -> Bounds2 {
let l = build(d, &Space::Paper("Layout1".into()), &Options::default());
let mut b = Bounds2::EMPTY;
for p in l.prims.iter().filter(|p| p.handle == VIEWPORT_CONTENT) {
for q in l.points(p) {
b.add(*q);
}
}
b
}
#[test]
fn same_paper_height_at_every_supported_scale() {
let at_50 = content(&drawing(50.0, false));
let at_100 = content(&drawing(100.0, false));
assert!(!at_50.is_empty() && !at_100.is_empty());
assert!((at_50.height() / at_100.height() - 1.0).abs() < 1e-9, "{} vs {}", at_50.height(), at_100.height());
assert!((at_50.width() / at_100.width() - 1.0).abs() < 1e-9);
// Glyphs span most of the 2.5 text height on paper.
assert!(at_50.height() > 1.5 && at_50.height() < 3.5, "{}", at_50.height());
}
#[test]
fn unsupported_viewport_scale_hides_or_shows_at_the_objects_scale() {
assert!(content(&drawing(20.0, false)).is_empty());
// ANNOALLVISIBLE 1: drawn at its first scale (1:50), so 50/20 = 2.5× its paper size here.
let shown = content(&drawing(20.0, true));
let at_50 = content(&drawing(50.0, false));
assert!((shown.height() / at_50.height() - 2.5).abs() < 1e-9, "{} vs {}", shown.height(), at_50.height());
}
#[test]
fn model_space_uses_cannoscale() {
let mut d = drawing(50.0, false);
let list = |d: &Drawing| build(d, &Space::Model, &Options::default()).prims.len();
// CANNOSCALE 1:1 isn't supported: hidden.
assert_eq!(list(&d), 0);
d.header.set_str("CANNOSCALE", "1:100");
assert!(list(&d) > 0);
let b = build(&d, &Space::Model, &Options::default()).bounds;
assert!(b.height() > 150.0 && b.height() < 350.0, "{}", b.height());
}
+2 -1
View File
@@ -329,6 +329,7 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {
}
// Right side: toggles, right-aligned.
let s = app.session.settings.clone();
let anno_all = app.session.doc().is_ok_and(|d| d.anno_all_visible());
use cadcraft_engine::snap::mode;
let toggles: Vec<(Icon, bool, &str, &str)> = vec![
(Icon::Grid, s.gridmode, "grid", "Grid (F7)"),
@@ -342,7 +343,7 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {
(Icon::Transparency, s.transparency_display, "transparencydisplay", "Show/Hide Transparency"),
(Icon::DynInput, s.dynmode, "dynmode", "Dynamic Input (F12)"),
(Icon::QuickProps, s.qpmode, "qpmode", "Quick Properties"),
(Icon::Annotation, s.annoallvisible, "ui.noop", "Annotation Visibility"),
(Icon::Annotation, anno_all, "annovisible.toggle", "Annotation Visibility"),
(Icon::Workspace, false, "ui.noop", "Workspace Switching"),
(Icon::Gear, false, "ui.dialog.dsettings", "Drafting Settings"),
];
+10
View File
@@ -1038,6 +1038,16 @@
Import... Імпорт...
Export... Експорт...
Batch Publish... Пакетна публікація...
Scale List... Список масштабів...
Annotative Object Scale Масштаб анотативних об'єктів
Add Current Scale Додати поточний масштаб
Delete Current Scale Видалити поточний масштаб
Add/Delete Scales... Додати/видалити масштаби...
Synchronize Multiple-scale Positions Синхронізувати положення для кількох масштабів
Annotative Update Оновити анотативні об'єкти
Show Annotative Objects Показувати анотативні об'єкти
Current Scale Only Лише поточний масштаб
All Scales Усі масштаби
Drawing Recovery Manager... Диспетчер відновлення креслень...
Tolerance... Допуск...
Add Vertex Додати вершину
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053