mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Draw order: DRAWORDER Above/Under objects, TEXTTOFRONT, HATCHTOBACK, SORTENTSTABLE read (part of #439) (#446)
Squash of PR #446 by @vangel76. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9427b77209
commit
9b136d6c19
8 files changed
+736
-34
No files matched your search
@@ -189,6 +189,70 @@ impl EntityStore {
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
/// Move `h` directly above (`above`) or under `anchor` in the draw order. False (and nothing
|
||||
/// moves) when either is absent or they are the same object.
|
||||
pub fn move_next_to(&mut self, h: Handle, anchor: Handle, above: bool) -> bool {
|
||||
if h == anchor || !self.contains(anchor) {
|
||||
return false;
|
||||
}
|
||||
let Some(e) = self.remove(h) else { return false };
|
||||
let at = self.chunk_id(anchor).and_then(|id| Some((id, self.chunk_pos(id)?)));
|
||||
let Some((id, pos)) = at else {
|
||||
// Unreachable in practice (the anchor was present); keep the object on top.
|
||||
self.push(Arc::unwrap_or_clone(e));
|
||||
return false;
|
||||
};
|
||||
let Some(c) = self.chunks.get_mut(pos) else {
|
||||
// Unreachable in practice too; never lose the object.
|
||||
self.push(Arc::unwrap_or_clone(e));
|
||||
return false;
|
||||
};
|
||||
let chunk = Arc::make_mut(c);
|
||||
let i = chunk.items.iter().position(|x| x.handle == anchor).unwrap_or(chunk.items.len());
|
||||
let i = if above { (i + 1).min(chunk.items.len()) } else { i };
|
||||
chunk.items.insert(i, e);
|
||||
let split = chunk.items.len() > 2 * CHUNK_MAX;
|
||||
self.set_index(h, Some(id));
|
||||
self.len += 1;
|
||||
if split {
|
||||
self.split_chunk(pos);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// Split an oversized chunk in two, keeping the order.
|
||||
fn split_chunk(&mut self, pos: usize) {
|
||||
let mut fresh = self.new_chunk();
|
||||
let Some(c) = self.chunks.get_mut(pos) else { return };
|
||||
let chunk = Arc::make_mut(c);
|
||||
let tail = chunk.items.split_off(chunk.items.len() / 2);
|
||||
let new = Arc::make_mut(&mut fresh);
|
||||
new.items = tail;
|
||||
let (id, moved): (u32, Vec<Handle>) = (new.id, new.items.iter().map(|e| e.handle).collect());
|
||||
self.chunks.insert(pos + 1, fresh);
|
||||
for h in moved {
|
||||
self.set_index(h, Some(id));
|
||||
}
|
||||
}
|
||||
/// Reorder the whole store by a key (stable: equal keys keep their order).
|
||||
pub fn sort_by_key<K: Ord>(&mut self, mut key: impl FnMut(&Entity) -> K) {
|
||||
let mut items: Vec<Arc<Entity>> = self.iter().cloned().collect();
|
||||
items.sort_by_cached_key(|e| key(e));
|
||||
*self = EntityStore::default();
|
||||
for e in items {
|
||||
let needs_new = self.chunks.last().is_none_or(|c| c.items.len() >= CHUNK_MAX);
|
||||
if needs_new {
|
||||
let c = self.new_chunk();
|
||||
self.chunks.push(c);
|
||||
}
|
||||
let h = e.handle;
|
||||
let Some(last) = self.chunks.last_mut() else { return };
|
||||
let chunk = Arc::make_mut(last);
|
||||
chunk.items.push(e);
|
||||
let id = chunk.id;
|
||||
self.set_index(h, Some(id));
|
||||
self.len += 1;
|
||||
}
|
||||
}
|
||||
/// True when `other` shares every chunk with `self` (same draw order, same entities). Holding
|
||||
/// a clone of a store makes this exact: any edit to the original copies the touched chunk.
|
||||
pub fn same_as(&self, other: &EntityStore) -> bool {
|
||||
@@ -256,4 +320,48 @@ mod tests {
|
||||
assert_eq!(s.iter().next().map(|e| e.handle), Some(Handle(5)));
|
||||
assert_eq!(s.len(), 5);
|
||||
}
|
||||
|
||||
fn order(s: &EntityStore) -> Vec<u64> {
|
||||
s.iter().map(|e| e.handle.0).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_next_to_and_sort() {
|
||||
let mut s = EntityStore::new();
|
||||
for h in 1..=5 {
|
||||
s.push(ent(h));
|
||||
}
|
||||
assert!(s.move_next_to(Handle(1), Handle(3), true));
|
||||
assert_eq!(order(&s), [2, 3, 1, 4, 5]);
|
||||
assert!(s.move_next_to(Handle(5), Handle(2), false));
|
||||
assert_eq!(order(&s), [5, 2, 3, 1, 4]);
|
||||
assert!(!s.move_next_to(Handle(5), Handle(5), true));
|
||||
assert!(!s.move_next_to(Handle(5), Handle(99), true));
|
||||
assert!(!s.move_next_to(Handle(99), Handle(5), true));
|
||||
assert_eq!(order(&s), [5, 2, 3, 1, 4]);
|
||||
assert_eq!(s.len(), 5);
|
||||
s.sort_by_key(|e| e.handle.0 % 2);
|
||||
assert_eq!(order(&s), [2, 4, 5, 3, 1]);
|
||||
assert!(s.get(Handle(3)).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_next_to_splits_big_chunks() {
|
||||
let mut s = EntityStore::new();
|
||||
for h in 1..=3000 {
|
||||
s.push(ent(h));
|
||||
}
|
||||
// Pile everything into the first chunk, under handle 1.
|
||||
for h in 1500..=3000 {
|
||||
assert!(s.move_next_to(Handle(h), Handle(1), true));
|
||||
}
|
||||
assert_eq!(s.len(), 3000);
|
||||
assert!(s.chunks.iter().all(|c| c.items.len() <= 2 * CHUNK_MAX));
|
||||
let o = order(&s);
|
||||
assert_eq!(o.first(), Some(&1));
|
||||
assert_eq!(o.get(1), Some(&3000));
|
||||
for h in 1..=3000 {
|
||||
assert_eq!(s.get(Handle(h)).map(|e| e.handle.0), Some(h));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
//! Draw order: DRAWORDER (Above objects / Under objects / Front / Back), TEXTTOFRONT and
|
||||
//! HATCHTOBACK. The order is the order of a space's entity store, which DXF files keep as the
|
||||
//! order of their entity records (`cadcraft-io` also applies a SORTENTSTABLE when it reads one).
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use cadcraft_doc::{EntityKind, Handle};
|
||||
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("draworder", "Draw Order", run_draworder)
|
||||
.alias(&["dr"])
|
||||
.params("{handles?, order?: \"front\"|\"back\"|\"above\"|\"under\" (default back), reference?: [hex] (above/under)} → {moved}")
|
||||
.interactive(|_| Ok(Box::new(DrawOrderM::new(None)))),
|
||||
CommandSpec::new("draworder.front", "Bring to Front", run_front)
|
||||
.menu(&["Tools", "Draw Order", "Bring to Front"])
|
||||
.params("{handles?}")
|
||||
.interactive(|_| Ok(Box::new(SelectRun::new("draworder.front", "DRAWORDER")))),
|
||||
CommandSpec::new("draworder.back", "Send to Back", run_back)
|
||||
.menu(&["Tools", "Draw Order", "Send to Back"])
|
||||
.params("{handles?}")
|
||||
.interactive(|_| Ok(Box::new(SelectRun::new("draworder.back", "DRAWORDER")))),
|
||||
CommandSpec::new("draworder.above", "Bring Above Objects", run_above)
|
||||
.menu(&["Tools", "Draw Order", "Bring Above Objects"])
|
||||
.params("{handles?, reference: [hex]}")
|
||||
.interactive(|_| Ok(Box::new(DrawOrderM::new(Some(Order::Above))))),
|
||||
CommandSpec::new("draworder.under", "Send Under Objects", run_under)
|
||||
.menu(&["Tools", "Draw Order", "Send Under Objects"])
|
||||
.params("{handles?, reference: [hex]}")
|
||||
.interactive(|_| Ok(Box::new(DrawOrderM::new(Some(Order::Under))))),
|
||||
CommandSpec::new("texttofront", "Bring Annotations to Front", run_texttofront)
|
||||
.params("{objects?: \"text\"|\"dimensions\"|\"leaders\"|\"all\" (default all)} → {moved}")
|
||||
.interactive(|_| Ok(Box::new(TextToFrontM))),
|
||||
CommandSpec::new("texttofront.text", "Text Objects Only", |s, _| annotations_to_front(s, Annot::Text))
|
||||
.menu(&["Tools", "Draw Order", "Bring Annotations to Front", "Text Objects Only"])
|
||||
.params("{} → {moved}"),
|
||||
CommandSpec::new("texttofront.dimensions", "Dimension Objects Only", |s, _| annotations_to_front(s, Annot::Dimensions))
|
||||
.menu(&["Tools", "Draw Order", "Bring Annotations to Front", "Dimension Objects Only"])
|
||||
.params("{} → {moved}"),
|
||||
CommandSpec::new("texttofront.leaders", "Leaders Only", |s, _| annotations_to_front(s, Annot::Leaders))
|
||||
.menu(&["Tools", "Draw Order", "Bring Annotations to Front", "Leaders Only"])
|
||||
.params("{} → {moved}"),
|
||||
CommandSpec::new("texttofront.all", "All Annotation Objects", |s, _| annotations_to_front(s, Annot::All))
|
||||
.menu(&["Tools", "Draw Order", "Bring Annotations to Front", "All Annotation Objects"])
|
||||
.params("{} → {moved}"),
|
||||
CommandSpec::new("hatchtoback", "Send Hatches to Back", run_hatchtoback)
|
||||
.menu(&["Tools", "Draw Order", "Send Hatches to Back"])
|
||||
.params("{} → {moved}"),
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Order {
|
||||
Front,
|
||||
Back,
|
||||
Above,
|
||||
Under,
|
||||
}
|
||||
|
||||
/// Move `hs` in the current space's draw order, keeping their order among themselves. Above and
|
||||
/// Under place them right over the topmost reference object or right under the lowest one.
|
||||
/// Returns how many objects moved.
|
||||
fn reorder(s: &mut Session, hs: &[Handle], order: Order, refs: &[Handle]) -> Result<usize> {
|
||||
let space = s.space();
|
||||
let d = s.doc_mut()?;
|
||||
let Some(st) = d.space_mut(&space) else { return Ok(0) };
|
||||
let index: HashMap<Handle, usize> = st.handles().into_iter().enumerate().map(|(i, h)| (h, i)).collect();
|
||||
let mut moving: Vec<(usize, Handle)> = hs.iter().filter_map(|h| index.get(h).map(|i| (*i, *h))).collect();
|
||||
moving.sort_unstable();
|
||||
moving.dedup();
|
||||
let set: HashSet<Handle> = moving.iter().map(|m| m.1).collect();
|
||||
let refs: Vec<(usize, Handle)> = refs.iter().filter(|h| !set.contains(h)).filter_map(|h| index.get(h).map(|i| (*i, *h))).collect();
|
||||
match order {
|
||||
Order::Front => {
|
||||
for (_, h) in &moving {
|
||||
st.bring_to_front(*h);
|
||||
}
|
||||
}
|
||||
Order::Back => {
|
||||
for (_, h) in moving.iter().rev() {
|
||||
st.send_to_back(*h);
|
||||
}
|
||||
}
|
||||
Order::Above => {
|
||||
let Some(&(_, mut cur)) = refs.iter().max() else { return Err(EngineError::Other("no reference objects".into())) };
|
||||
for (_, h) in &moving {
|
||||
st.move_next_to(*h, cur, true);
|
||||
cur = *h;
|
||||
}
|
||||
}
|
||||
Order::Under => {
|
||||
let Some(&(_, anchor)) = refs.iter().min() else { return Err(EngineError::Other("no reference objects".into())) };
|
||||
for (_, h) in &moving {
|
||||
st.move_next_to(*h, anchor, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(moving.len())
|
||||
}
|
||||
|
||||
fn parse_order(p: &Value) -> Result<Order> {
|
||||
match p.get("order") {
|
||||
None | Some(Value::Null) => Ok(Order::Back),
|
||||
Some(v) => match v.as_str().map(|t| t.trim().to_ascii_lowercase()).as_deref() {
|
||||
Some("front") => Ok(Order::Front),
|
||||
Some("back") => Ok(Order::Back),
|
||||
Some("above") => Ok(Order::Above),
|
||||
Some("under") => Ok(Order::Under),
|
||||
_ => Err(bad("draworder", format!("`order` must be \"front\", \"back\", \"above\" or \"under\", got {v}"))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn reference(s: &Session, cmd: &str, p: &Value) -> Result<Vec<Handle>> {
|
||||
match p.get("reference") {
|
||||
None | Some(Value::Null) => Err(bad(cmd, "`reference` (object handles) is required")),
|
||||
Some(v) => targets(s, &json!({ "handles": v })),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_draworder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let order = parse_order(p)?;
|
||||
let hs = targets(s, p)?;
|
||||
let refs = if matches!(order, Order::Above | Order::Under) { reference(s, "draworder", p)? } else { Vec::new() };
|
||||
let n = reorder(s, &hs, order, &refs)?;
|
||||
Ok(json!({ "moved": n }))
|
||||
}
|
||||
|
||||
fn run_front(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
reorder(s, &hs, Order::Front, &[])?;
|
||||
ok()
|
||||
}
|
||||
|
||||
fn run_back(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
reorder(s, &hs, Order::Back, &[])?;
|
||||
ok()
|
||||
}
|
||||
|
||||
fn run_above(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
let refs = reference(s, "draworder.above", p)?;
|
||||
let n = reorder(s, &hs, Order::Above, &refs)?;
|
||||
Ok(json!({ "moved": n }))
|
||||
}
|
||||
|
||||
fn run_under(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
let refs = reference(s, "draworder.under", p)?;
|
||||
let n = reorder(s, &hs, Order::Under, &refs)?;
|
||||
Ok(json!({ "moved": n }))
|
||||
}
|
||||
|
||||
/// TEXTTOFRONT's object classes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Annot {
|
||||
Text,
|
||||
Dimensions,
|
||||
Leaders,
|
||||
All,
|
||||
}
|
||||
|
||||
impl Annot {
|
||||
fn matches(self, k: &EntityKind) -> bool {
|
||||
let text = matches!(k, EntityKind::Text(_) | EntityKind::MText(_));
|
||||
let dim = matches!(k, EntityKind::Dimension(_));
|
||||
let leader = matches!(k, EntityKind::Leader(_) | EntityKind::MLeader(_));
|
||||
match self {
|
||||
Annot::Text => text,
|
||||
Annot::Dimensions => dim,
|
||||
Annot::Leaders => leader,
|
||||
Annot::All => text || dim || leader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every object of the current space that `keep` accepts, in draw order.
|
||||
fn of_kind(s: &Session, keep: impl Fn(&EntityKind) -> bool) -> Result<Vec<Handle>> {
|
||||
let d = s.doc()?;
|
||||
Ok(d.space(&s.space()).map(|st| st.iter().filter(|e| keep(&e.kind)).map(|e| e.handle).collect()).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn annotations_to_front(s: &mut Session, which: Annot) -> Result<Value> {
|
||||
let hs = of_kind(s, |k| which.matches(k))?;
|
||||
let n = reorder(s, &hs, Order::Front, &[])?;
|
||||
Ok(json!({ "moved": n, "message": format!("{n} object(s) brought to front.") }))
|
||||
}
|
||||
|
||||
fn run_texttofront(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let which = match p.get("objects") {
|
||||
None | Some(Value::Null) => Annot::All,
|
||||
Some(v) => match v.as_str().map(|t| t.trim().to_ascii_lowercase()).as_deref() {
|
||||
Some("text") => Annot::Text,
|
||||
Some("dimensions") => Annot::Dimensions,
|
||||
Some("leaders") => Annot::Leaders,
|
||||
Some("all") => Annot::All,
|
||||
_ => return Err(bad("texttofront", format!("`objects` must be \"text\", \"dimensions\", \"leaders\" or \"all\", got {v}"))),
|
||||
},
|
||||
};
|
||||
annotations_to_front(s, which)
|
||||
}
|
||||
|
||||
fn run_hatchtoback(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
let hs = of_kind(s, |k| matches!(k, EntityKind::Hatch(_)))?;
|
||||
let n = reorder(s, &hs, Order::Back, &[])?;
|
||||
Ok(json!({ "moved": n, "message": format!("{n} hatch object(s) sent to back.") }))
|
||||
}
|
||||
|
||||
/// TEXTTOFRONT typed: "Bring to front [Text/Dimensions/Leaders/All] <All>:".
|
||||
struct TextToFrontM;
|
||||
|
||||
impl Interactive for TextToFrontM {
|
||||
fn name(&self) -> &'static str {
|
||||
"TEXTTOFRONT"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
Prompt::new("Bring to front", Accept::TEXT).kw(&["Text", "Dimensions", "Leaders", "All"]).default("All")
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
let which = match &i {
|
||||
Input::Enter => Annot::All,
|
||||
Input::Keyword(k) | Input::Text(k) => match k.trim().to_ascii_lowercase().as_str() {
|
||||
"text" | "t" => Annot::Text,
|
||||
"dimensions" | "d" => Annot::Dimensions,
|
||||
"leaders" | "l" => Annot::Leaders,
|
||||
"all" | "a" => Annot::All,
|
||||
_ => {
|
||||
s.echo("Invalid option keyword.");
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
},
|
||||
_ => return Ok(Step::Continue),
|
||||
};
|
||||
let r = annotations_to_front(s, which)?;
|
||||
if let Some(m) = r.get("message").and_then(Value::as_str) {
|
||||
s.echo(m.to_string());
|
||||
}
|
||||
Ok(Step::Done)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Stage {
|
||||
Select,
|
||||
Option,
|
||||
Reference,
|
||||
}
|
||||
|
||||
/// DRAWORDER typed: "Select objects:", then "Enter object ordering option [Above objects/Under
|
||||
/// objects/Front/Back] <Back>:", and for Above/Under "Select reference objects:". The Bring Above
|
||||
/// / Send Under menu items start with the option chosen.
|
||||
struct DrawOrderM {
|
||||
stage: Stage,
|
||||
preset: Option<Order>,
|
||||
order: Order,
|
||||
sel: SelectPhase,
|
||||
objs: Vec<Handle>,
|
||||
}
|
||||
|
||||
impl DrawOrderM {
|
||||
fn new(preset: Option<Order>) -> Self {
|
||||
DrawOrderM { stage: Stage::Select, preset, order: preset.unwrap_or(Order::Back), sel: SelectPhase::default(), objs: Vec::new() }
|
||||
}
|
||||
/// The objects are chosen: ask for the option, or go on with the preset one.
|
||||
fn selected(&mut self, s: &mut Session, hs: Vec<Handle>) -> Result<Step> {
|
||||
self.objs = hs;
|
||||
match self.preset {
|
||||
Some(o) => self.chosen(s, o),
|
||||
None => {
|
||||
self.stage = Stage::Option;
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn chosen(&mut self, s: &mut Session, o: Order) -> Result<Step> {
|
||||
self.order = o;
|
||||
match o {
|
||||
Order::Front | Order::Back => self.finish(s, &[]),
|
||||
Order::Above | Order::Under => {
|
||||
self.stage = Stage::Reference;
|
||||
self.sel = SelectPhase::default();
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn finish(&mut self, s: &mut Session, refs: &[Handle]) -> Result<Step> {
|
||||
let objs = std::mem::take(&mut self.objs);
|
||||
match reorder(s, &objs, self.order, refs) {
|
||||
Ok(_) => {}
|
||||
Err(e) => s.echo(e.to_string()),
|
||||
}
|
||||
s.set_selection(Vec::new());
|
||||
Ok(Step::Done)
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for DrawOrderM {
|
||||
fn name(&self) -> &'static str {
|
||||
"DRAWORDER"
|
||||
}
|
||||
fn begin(&mut self, s: &mut Session) -> Result<Step> {
|
||||
self.sel = SelectPhase::begin(s);
|
||||
if self.sel.done {
|
||||
let hs = self.sel.picked.clone();
|
||||
return self.selected(s, hs);
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
match self.stage {
|
||||
Stage::Select => self.sel.prompt(),
|
||||
Stage::Option => {
|
||||
Prompt::new("Enter object ordering option", Accept::TEXT).kw(&["Above objects", "Under objects", "Front", "Back"]).default("Back")
|
||||
}
|
||||
Stage::Reference if self.sel.removing => self.sel.prompt(),
|
||||
Stage::Reference => Prompt::new("Select reference objects", Accept::SELECT),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
match self.stage {
|
||||
Stage::Select => match self.sel.feed(s, &i)? {
|
||||
SelOutcome::More => Ok(Step::Continue),
|
||||
SelOutcome::Empty => Ok(Step::Done),
|
||||
SelOutcome::Done(hs) => {
|
||||
s.set_selection(Vec::new());
|
||||
self.selected(s, hs)
|
||||
}
|
||||
},
|
||||
Stage::Option => {
|
||||
let o = match &i {
|
||||
Input::Enter => Order::Back,
|
||||
Input::Keyword(k) | Input::Text(k) => match k.trim().to_ascii_lowercase().as_str() {
|
||||
"above objects" | "a" | "above" => Order::Above,
|
||||
"under objects" | "u" | "under" => Order::Under,
|
||||
"front" | "f" => Order::Front,
|
||||
"back" | "b" => Order::Back,
|
||||
_ => {
|
||||
s.echo("Invalid option keyword.");
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
},
|
||||
_ => return Ok(Step::Continue),
|
||||
};
|
||||
self.chosen(s, o)
|
||||
}
|
||||
Stage::Reference => match self.sel.feed(s, &i)? {
|
||||
SelOutcome::More => Ok(Step::Continue),
|
||||
SelOutcome::Empty => {
|
||||
s.set_selection(Vec::new());
|
||||
Ok(Step::Done)
|
||||
}
|
||||
SelOutcome::Done(refs) => self.finish(s, &refs),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub mod clipboard;
|
||||
pub mod constraints;
|
||||
mod draw;
|
||||
mod draw2;
|
||||
mod draworder;
|
||||
mod edit;
|
||||
mod exchange;
|
||||
pub mod file;
|
||||
@@ -172,6 +173,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(hatch::specs());
|
||||
v.extend(blocks::specs());
|
||||
v.extend(modify::specs());
|
||||
v.extend(draworder::specs());
|
||||
v.extend(array::specs());
|
||||
v.extend(modify2::specs());
|
||||
v.extend(groups::specs());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Modify commands: ERASE, MOVE, COPY, ROTATE, SCALE, MIRROR, OFFSET, TRIM, EXTEND, FILLET,
|
||||
//! CHAMFER, EXPLODE, STRETCH, DRAWORDER, BREAK, JOIN (arrays: `array`).
|
||||
//! CHAMFER, EXPLODE, STRETCH, BREAK, JOIN (arrays: `array`; draw order: `draworder`).
|
||||
|
||||
use cadcraft_doc::{Entity, EntityKind, Handle, Prim};
|
||||
use cadcraft_geom::{
|
||||
@@ -8,7 +8,7 @@ use cadcraft_geom::{
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::helpers::*;
|
||||
use super::machines::{SelOutcome, SelectPhase, SelectRun, number};
|
||||
use super::machines::{SelOutcome, SelectPhase, number};
|
||||
use super::trimextend::{self, TrimM};
|
||||
use super::*;
|
||||
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
||||
@@ -82,14 +82,6 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
.alias(&["x"])
|
||||
.params("{handles?}")
|
||||
.interactive(|_| Ok(Box::new(SelectThen::new(Op::Explode)))),
|
||||
CommandSpec::new("draworder.front", "Bring to Front", run_front)
|
||||
.menu(&["Tools", "Draw Order", "Bring to Front"])
|
||||
.params("{handles?}")
|
||||
.interactive(|_| Ok(Box::new(SelectRun::new("draworder.front", "DRAWORDER")))),
|
||||
CommandSpec::new("draworder.back", "Send to Back", run_back)
|
||||
.menu(&["Tools", "Draw Order", "Send to Back"])
|
||||
.params("{handles?}")
|
||||
.interactive(|_| Ok(Box::new(SelectRun::new("draworder.back", "DRAWORDER")))),
|
||||
CommandSpec::new("break", "Break", run_break)
|
||||
.menu(&["Modify", "Break"])
|
||||
.alias(&["br"])
|
||||
@@ -1290,30 +1282,6 @@ fn run_explode(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
fn run_front(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
let space = s.space();
|
||||
let d = s.doc_mut()?;
|
||||
if let Some(st) = d.space_mut(&space) {
|
||||
for h in hs {
|
||||
st.bring_to_front(h);
|
||||
}
|
||||
}
|
||||
ok()
|
||||
}
|
||||
|
||||
fn run_back(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
let space = s.space();
|
||||
let d = s.doc_mut()?;
|
||||
if let Some(st) = d.space_mut(&space) {
|
||||
for h in hs.into_iter().rev() {
|
||||
st.send_to_back(h);
|
||||
}
|
||||
}
|
||||
ok()
|
||||
}
|
||||
|
||||
/// Break `h` between two points (or at one point when p1 == p2).
|
||||
pub(crate) fn break_entity(s: &mut Session, h: Handle, p1: Vec2, p2: Vec2) -> Result<Vec<Handle>> {
|
||||
let e = s.doc()?.entity(h).map(|e| (**e).clone()).ok_or_else(|| EngineError::Other("no such object".into()))?;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Draw order (#439): DRAWORDER's Above objects / Under objects / Front / Back, typed and as JSON,
|
||||
//! TEXTTOFRONT and HATCHTOBACK.
|
||||
|
||||
use cadcraft_engine::doc::{EntityKind, Handle};
|
||||
use cadcraft_engine::{Input, Session};
|
||||
use serde_json::json;
|
||||
|
||||
/// A session with five lines; their handles in draw order.
|
||||
fn five_lines() -> (Session, Vec<Handle>) {
|
||||
let mut s = Session::new();
|
||||
for y in 0..5 {
|
||||
s.execute("line", &json!({ "points": [[0, y], [10, y]] })).unwrap();
|
||||
}
|
||||
let hs = order(&s);
|
||||
(s, hs)
|
||||
}
|
||||
|
||||
fn order(s: &Session) -> Vec<Handle> {
|
||||
s.doc().unwrap().model.iter().map(|e| e.handle).collect()
|
||||
}
|
||||
|
||||
fn hex(hs: &[Handle]) -> Vec<String> {
|
||||
hs.iter().map(|h| h.hex()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draworder_json_keeps_relative_order() {
|
||||
let (mut s, h) = five_lines();
|
||||
// Front/back keep the moved objects' order among themselves, whatever order they are given in.
|
||||
s.execute("draworder", &json!({ "handles": hex(&[h[2], h[0]]), "order": "front" })).unwrap();
|
||||
assert_eq!(order(&s), [h[1], h[3], h[4], h[0], h[2]]);
|
||||
s.execute("draworder", &json!({ "handles": hex(&[h[2], h[0]]) })).unwrap();
|
||||
assert_eq!(order(&s), [h[0], h[2], h[1], h[3], h[4]], "back is the default");
|
||||
// Above: right over the topmost reference object.
|
||||
let r = s.execute("draworder", &json!({ "handles": hex(&[h[0], h[2]]), "order": "above", "reference": hex(&[h[1], h[3]]) })).unwrap();
|
||||
assert_eq!(r["moved"], 2);
|
||||
assert_eq!(order(&s), [h[1], h[3], h[0], h[2], h[4]]);
|
||||
// Under: right under the lowest reference object; a moved object among the references is skipped.
|
||||
s.execute("draworder.under", &json!({ "handles": hex(&[h[4]]), "reference": hex(&[h[3], h[0], h[4]]) })).unwrap();
|
||||
assert_eq!(order(&s), [h[1], h[4], h[3], h[0], h[2]]);
|
||||
s.execute("draworder.above", &json!({ "handles": hex(&[h[1]]), "reference": hex(&[h[2]]) })).unwrap();
|
||||
assert_eq!(order(&s), [h[4], h[3], h[0], h[2], h[1]]);
|
||||
// Bad options, missing references and unknown handles are refused without changes.
|
||||
let before = order(&s);
|
||||
assert!(s.execute("draworder", &json!({ "handles": hex(&[h[0]]), "order": "sideways" })).is_err());
|
||||
assert!(s.execute("draworder", &json!({ "handles": hex(&[h[0]]), "order": 3 })).is_err());
|
||||
assert!(s.execute("draworder", &json!({ "handles": hex(&[h[0]]), "order": "above" })).is_err());
|
||||
assert!(s.execute("draworder.above", &json!({ "handles": hex(&[h[0]]), "reference": "1A" })).is_err());
|
||||
assert!(s.execute("draworder.under", &json!({ "handles": hex(&[h[0]]), "reference": ["FFFFF"] })).is_err());
|
||||
assert!(s.execute("draworder.under", &json!({ "handles": hex(&[h[0]]), "reference": hex(&[h[0]]) })).is_err());
|
||||
assert_eq!(order(&s), before);
|
||||
// The old menu commands still work.
|
||||
s.execute("draworder.front", &json!({ "handles": hex(&[h[4]]) })).unwrap();
|
||||
assert_eq!(order(&s).last(), Some(&h[4]));
|
||||
s.execute("draworder.back", &json!({ "handles": hex(&[h[4]]) })).unwrap();
|
||||
assert_eq!(order(&s).first(), Some(&h[4]));
|
||||
assert!(s.running.is_none(), "JSON calls never prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draworder_typed_options_and_undo() {
|
||||
let (mut s, h) = five_lines();
|
||||
let undo = s.state().unwrap().undo.len();
|
||||
s.cmdline("draworder").unwrap();
|
||||
assert!(s.prompt_text().contains("Select objects"), "{}", s.prompt_text());
|
||||
s.input(Input::Pick(vec![h[0]])).unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.prompt_text().contains("[Above objects/Under objects/Front/Back] <Back>"), "{}", s.prompt_text());
|
||||
s.cmdline("a").unwrap();
|
||||
assert!(s.prompt_text().contains("Select reference objects"), "{}", s.prompt_text());
|
||||
s.input(Input::Pick(vec![h[2]])).unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(order(&s), [h[1], h[2], h[0], h[3], h[4]]);
|
||||
assert_eq!(s.state().unwrap().undo.len(), undo + 1, "one undo step");
|
||||
assert!(s.selection().is_empty(), "picked objects don't stay selected");
|
||||
// Enter takes the default, Back.
|
||||
s.cmdline("dr").unwrap();
|
||||
s.input(Input::Pick(vec![h[4]])).unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(order(&s).first(), Some(&h[4]));
|
||||
// Front by keyword; with a pickfirst selection the option is asked at once.
|
||||
s.set_selection(vec![h[4]]);
|
||||
s.cmdline("draworder").unwrap();
|
||||
assert!(s.prompt_text().contains("ordering option"), "{}", s.prompt_text());
|
||||
s.cmdline("f").unwrap();
|
||||
assert_eq!(order(&s).last(), Some(&h[4]));
|
||||
// The Send Under Objects menu item goes from the objects straight to the references.
|
||||
s.start("draworder.under").unwrap();
|
||||
s.input(Input::Pick(vec![h[4]])).unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.prompt_text().contains("Select reference objects"), "{}", s.prompt_text());
|
||||
s.input(Input::Pick(vec![h[1]])).unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert_eq!(order(&s).first(), Some(&h[4]));
|
||||
// An empty reference set ends without changes.
|
||||
let before = order(&s);
|
||||
s.cmdline("draworder").unwrap();
|
||||
s.input(Input::Pick(vec![h[0]])).unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
s.cmdline("u").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(order(&s), before);
|
||||
s.execute("u", &json!({})).unwrap();
|
||||
s.execute("u", &json!({})).unwrap();
|
||||
s.execute("u", &json!({})).unwrap();
|
||||
s.execute("u", &json!({})).unwrap();
|
||||
assert_eq!(order(&s), h, "undo restores the order");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn texttofront_and_hatchtoback() {
|
||||
let mut s = Session::new();
|
||||
s.execute("text", &json!({ "at": [0, 0], "height": 2, "text": "A" })).unwrap();
|
||||
s.execute("dimlinear", &json!({ "p1": [0, 0], "p2": [10, 0], "at": [5, 5] })).unwrap();
|
||||
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
|
||||
s.execute("hatch", &json!({ "points": [[5, 5]], "pattern": "SOLID" })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[0, 0], [10, 10]] })).unwrap();
|
||||
let kinds = |s: &Session| s.doc().unwrap().model.iter().map(|e| e.kind.type_name()).collect::<Vec<_>>();
|
||||
// HATCH already puts its hatch at the back; move it to the front first.
|
||||
let hatch = s.doc().unwrap().model.iter().find(|e| matches!(e.kind, EntityKind::Hatch(_))).unwrap().handle;
|
||||
s.execute("draworder.front", &json!({ "handles": [hatch.hex()] })).unwrap();
|
||||
assert_eq!(kinds(&s).last(), Some(&"Hatch"));
|
||||
let r = s.execute("hatchtoback", &json!({})).unwrap();
|
||||
assert_eq!(r["moved"], 1);
|
||||
assert_eq!(kinds(&s).first(), Some(&"Hatch"));
|
||||
// Text only, then all annotation.
|
||||
let r = s.execute("texttofront", &json!({ "objects": "text" })).unwrap();
|
||||
assert_eq!(r["moved"], 1);
|
||||
assert_eq!(kinds(&s).last(), Some(&"Text"));
|
||||
s.execute("texttofront.dimensions", &json!({})).unwrap();
|
||||
assert!(kinds(&s).last().is_some_and(|k| k.contains("Dimension")), "{:?}", kinds(&s));
|
||||
assert!(s.execute("texttofront", &json!({ "objects": "everything" })).is_err());
|
||||
// Typed: the option prompt, Enter = All.
|
||||
s.cmdline("texttofront").unwrap();
|
||||
assert!(s.prompt_text().contains("[Text/Dimensions/Leaders/All] <All>"), "{}", s.prompt_text());
|
||||
s.cmdline("t").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert_eq!(kinds(&s).last(), Some(&"Text"));
|
||||
assert!(s.log.iter().any(|l| l.contains("1 object(s) brought to front")), "{:?}", s.log);
|
||||
s.cmdline("texttofront").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.log.iter().any(|l| l.contains("2 object(s) brought to front")), "{:?}", s.log);
|
||||
}
|
||||
@@ -1499,6 +1499,7 @@ pub fn read(bytes: &[u8]) -> Result<Drawing> {
|
||||
d.model.push(e);
|
||||
}
|
||||
}
|
||||
apply_sortents(&mut d, &objs.sortents, &rx.brs, &paper_block_for, &first_layout);
|
||||
objs.apply(&mut d, &rx);
|
||||
crate::dxf_image::resolve(&secs, &mut d);
|
||||
// Keep handles unique against the header's seed.
|
||||
@@ -1512,6 +1513,57 @@ pub fn read(bytes: &[u8]) -> Result<Drawing> {
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// A SORTENTSTABLE (DXF Reference, OBJECTS section): after its AcDbSortentsTable marker, the
|
||||
/// owning block record (330), then pairs of an entity handle (331) and the sort handle (5) it is
|
||||
/// drawn by.
|
||||
fn sortents_table(tags: &[Tag]) -> Option<(String, HashMap<Handle, u64>)> {
|
||||
let start = tags.iter().position(|t| t.code == 100 && t.str() == "AcDbSortentsTable")?;
|
||||
let tail = tags.get(start + 1..)?;
|
||||
let owner = tail.iter().find(|t| t.code == 330)?.str().trim().to_ascii_uppercase();
|
||||
let mut map = HashMap::new();
|
||||
let mut ent: Option<Handle> = None;
|
||||
for t in tail {
|
||||
match t.code {
|
||||
331 => ent = Handle::parse_hex(&t.str()),
|
||||
5 => {
|
||||
if let (Some(e), Some(k)) = (ent.take(), Handle::parse_hex(&t.str()))
|
||||
&& map.len() < MAX_OBJECTS
|
||||
{
|
||||
map.insert(e, k.0);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(!map.is_empty()).then_some((owner, map))
|
||||
}
|
||||
|
||||
/// Draw each space with a SORTENTSTABLE in sort-handle order: an entity listed in the table by
|
||||
/// its sort handle, any other by its own handle.
|
||||
fn apply_sortents(
|
||||
d: &mut Drawing,
|
||||
tables: &[(String, HashMap<Handle, u64>)],
|
||||
brs: &HashMap<String, String>,
|
||||
paper_for: &HashMap<String, String>,
|
||||
first_layout: &str,
|
||||
) {
|
||||
for (owner, map) in tables {
|
||||
let Some(name) = brs.get(owner) else { continue };
|
||||
let up = name.to_ascii_uppercase();
|
||||
let key = |e: &Entity| map.get(&e.handle).copied().unwrap_or(e.handle.0);
|
||||
if up == "*MODEL_SPACE" {
|
||||
d.model.sort_by_key(key);
|
||||
} else if up.starts_with("*PAPER_SPACE") {
|
||||
let lname = paper_for.get(&up).map(String::as_str).unwrap_or(first_layout);
|
||||
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
|
||||
l.entities.sort_by_key(key);
|
||||
}
|
||||
} else if let Some(b) = d.blocks.get_mut(name.as_str()) {
|
||||
std::sync::Arc::make_mut(b).entities.sort_by_key(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-graphical objects applied once every entity is placed.
|
||||
#[derive(Default)]
|
||||
struct Objects {
|
||||
@@ -1529,6 +1581,8 @@ struct Objects {
|
||||
groups: Vec<(String, Vec<Tag>)>,
|
||||
/// Saved paper-space views of layouts: (layout name, view).
|
||||
layout_views: Vec<(String, (Vec2, f64))>,
|
||||
/// SORTENTSTABLE objects: (owning block record handle, entity handle → sort handle).
|
||||
sortents: Vec<(String, HashMap<Handle, u64>)>,
|
||||
}
|
||||
|
||||
const MAX_OBJECTS: usize = 1_000_000;
|
||||
@@ -1560,6 +1614,11 @@ impl Objects {
|
||||
"DIMASSOC" if self.dimassocs.len() < MAX_OBJECTS => self.dimassocs.push(tags.to_vec()),
|
||||
"MLEADERSTYLE" if self.mleader_styles.len() < MAX_OBJECTS => self.mleader_styles.push((h, tags.to_vec())),
|
||||
"GROUP" if self.groups.len() < MAX_OBJECTS => self.groups.push((h, tags.to_vec())),
|
||||
"SORTENTSTABLE" if self.sortents.len() < MAX_OBJECTS => {
|
||||
if let Some(t) = sortents_table(tags) {
|
||||
self.sortents.push(t);
|
||||
}
|
||||
}
|
||||
"LAYOUT" if self.layout_views.len() < MAX_OBJECTS => {
|
||||
let start = tags.iter().position(|x| x.code == 100 && x.str() == "AcDbLayout").unwrap_or(0);
|
||||
if let (Some(name), Some(view)) = (T(tags.get(start..).unwrap_or(&[])).s(1), layout_view(tags)) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Draw order from DXF files (#439): a SORTENTSTABLE reorders its space by sort handle, and the
|
||||
//! order CADCraft writes (the order of the entity records) reads back unchanged.
|
||||
|
||||
use cadcraft_doc::*;
|
||||
|
||||
fn line(h: &str, y: f64) -> String {
|
||||
format!("0\nLINE\n5\n{h}\n330\n1F\n8\n0\n10\n0\n20\n{y}\n30\n0\n11\n10\n21\n{y}\n31\n0\n")
|
||||
}
|
||||
|
||||
fn dxf(objects: &str) -> String {
|
||||
let mut s = String::from("0\nSECTION\n2\nTABLES\n0\nTABLE\n2\nBLOCK_RECORD\n0\nBLOCK_RECORD\n5\n1F\n2\n*Model_Space\n0\nENDTAB\n0\nENDSEC\n");
|
||||
s.push_str("0\nSECTION\n2\nENTITIES\n");
|
||||
s.push_str(&line("A1", 0.0));
|
||||
s.push_str(&line("A2", 1.0));
|
||||
s.push_str(&line("A3", 2.0));
|
||||
s.push_str("0\nENDSEC\n0\nSECTION\n2\nOBJECTS\n");
|
||||
s.push_str(objects);
|
||||
s.push_str("0\nENDSEC\n0\nEOF\n");
|
||||
s
|
||||
}
|
||||
|
||||
fn order(d: &Drawing) -> Vec<String> {
|
||||
d.model.iter().map(|e| e.handle.hex()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sortentstable_sets_the_draw_order() {
|
||||
// A1 is drawn by sort handle A4 (over A2 and A3); A3 by 10 (under everything).
|
||||
let table = "0\nSORTENTSTABLE\n5\nB0\n330\nB1\n100\nAcDbSortentsTable\n330\n1F\n331\nA1\n5\nA4\n331\nA3\n5\n10\n";
|
||||
let d = cadcraft_io::read_dxf(dxf(table).as_bytes()).unwrap();
|
||||
assert_eq!(order(&d), ["A3", "A2", "A1"]);
|
||||
// Written back in draw order, without a table, the order survives.
|
||||
let back = cadcraft_io::read_dxf(cadcraft_io::write_dxf(&d).as_bytes()).unwrap();
|
||||
assert_eq!(order(&back), ["A3", "A2", "A1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broken_sortentstables_are_ignored() {
|
||||
for table in [
|
||||
"0\nSORTENTSTABLE\n5\nB0\n331\nA1\n5\nA4\n",
|
||||
"0\nSORTENTSTABLE\n100\nAcDbSortentsTable\n330\nFFFF\n331\nA1\n5\nA4\n",
|
||||
"0\nSORTENTSTABLE\n100\nAcDbSortentsTable\n330\n1F\n331\nzz\n5\nA4\n331\nA2\n5\n\n5\nA9\n",
|
||||
"0\nSORTENTSTABLE\n100\nAcDbSortentsTable\n",
|
||||
] {
|
||||
let d = cadcraft_io::read_dxf(dxf(table).as_bytes()).unwrap();
|
||||
assert_eq!(order(&d), ["A1", "A2", "A3"], "{table:?}");
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
Align Text Вирівняти текст
|
||||
Aligned Паралельний
|
||||
All Усі
|
||||
All Annotation Objects Усі об'єкти анотацій
|
||||
All Used Layers Усі використані шари
|
||||
All files Усі файли
|
||||
Angle Кут
|
||||
@@ -79,6 +80,8 @@
|
||||
Box Паралелепіпед
|
||||
Break Розірвати
|
||||
Break At Point Розірвати в точці
|
||||
Bring Above Objects Над об'єктами
|
||||
Bring Annotations to Front Анотації на передній план
|
||||
Bring to Front На передній план
|
||||
ByBlock За блоком
|
||||
ByColor За кольором
|
||||
@@ -173,6 +176,7 @@
|
||||
Diameter Діаметр
|
||||
Dim style Розмірний стиль
|
||||
Dimension Розміри
|
||||
Dimension Objects Only Лише розміри
|
||||
Dimension Space Відстань між розмірами
|
||||
Dimension Style Override Перевизначення розмірного стилю
|
||||
Dimension Style... Розмірний стиль...
|
||||
@@ -388,6 +392,7 @@
|
||||
Layout Аркуш
|
||||
Layout list Список аркушів
|
||||
Leader Виноска
|
||||
Leaders Only Лише виноски
|
||||
Left Ліворуч
|
||||
Length Довжина
|
||||
Length X Довжина X
|
||||
@@ -542,6 +547,9 @@
|
||||
New STB Нова STB
|
||||
New style Новий стиль
|
||||
Reset Panel Layout Скинути розташування панелей
|
||||
Send Hatches to Back Штрихування на задній план
|
||||
Send Under Objects Під об'єкти
|
||||
Text Objects Only Лише текстові об'єкти
|
||||
Use for this layout Використати для цього аркуша
|
||||
Display plot styles Показувати стилі друку
|
||||
No plot style tables. Немає таблиць стилів друку.
|
||||
|
||||
|
Reference in new issue
Block a user