mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-01 06:46:50 -06:00
LAYOUT: SAveas saves a layout as a drawing template
LAYOUT SAveas printed "LAYOUT SAveas is not available yet." It now asks
for the layout (the current one by default) and the template file
(<layout>.dwt beside the drawing by default), and writes the layout as a
template: its page setup and paper space objects, viewports included, no
model space objects and no other layout. Named objects nothing in it uses
are left out, as PURGE All removes them. Layers the layout's viewports
freeze or recolor are kept, with those settings. The open drawing is not
changed.
The JSON form is layout.saveas {name?, path}, also reached as LAYOUT
{option: saveas}. A path without an extension gets .dwt. The command
also writes .dwg and .dxf paths and refuses other file types.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! Layouts, paper-space viewports, page setup and plotting: LAYOUT (new/delete/rename/copy),
|
||||
//! Layouts, paper-space viewports, page setup and plotting: LAYOUT (new/delete/rename/copy/saveas),
|
||||
//! MVIEW / VPORTS, viewport scale/lock/layer settings, PAGESETUP, PLOT and EXPORTPDF.
|
||||
//!
|
||||
//! Plotting goes through the host's [`super::file::IoHooks::plot`] hook so the engine stays
|
||||
@@ -22,7 +22,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("layout", "Layout", run_layout)
|
||||
.alias(&["lo"])
|
||||
.params("{option: new|copy|delete|rename|set|list, name?, to?} (typed LAYOUT / -LAYOUT: option prompts)")
|
||||
.params("{option: new|copy|delete|rename|saveas|set|list, name?, to?, path? (saveas)} (typed LAYOUT / -LAYOUT: option prompts)")
|
||||
.interactive(|_| Ok(Box::new(prompts::LayoutM::default()))),
|
||||
CommandSpec::new("layout.new", "New Layout", run_new)
|
||||
.menu(&["Insert", "Layout", "New Layout"])
|
||||
@@ -32,6 +32,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
CommandSpec::new("layout.rename", "Rename Layout", run_rename).params("{from?: current layout, to}"),
|
||||
CommandSpec::new("layout.copy", "Copy Layout", run_copy).params("{from, to?}"),
|
||||
CommandSpec::new("layout.list", "List Layouts", run_list).params("→ [{name, tabOrder, page, viewports}]").noundo(),
|
||||
CommandSpec::new("layout.saveas", "Save Layout as Template", saveas::run_saveas)
|
||||
.params("{name?: layout (default: the current layout, else the first tab), path: .dwt (or .dwg, .dxf; no extension adds .dwt)} → {name, path, bytes, notSaved?}")
|
||||
.noundo(),
|
||||
CommandSpec::new("mview", "Viewports", run_mview)
|
||||
.alias(&["mv"])
|
||||
.params(
|
||||
@@ -334,6 +337,9 @@ fn run_list(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
))
|
||||
}
|
||||
|
||||
/// LAYOUT SAveas: a layout written as a drawing template.
|
||||
mod saveas;
|
||||
|
||||
/// `-LAYOUT`-style dispatcher.
|
||||
fn run_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let opt = str_param(p, "option").unwrap_or("list").to_ascii_lowercase();
|
||||
@@ -362,8 +368,9 @@ fn run_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let set = find_command("layout.set").ok_or_else(|| bad("layout", "layout.set is missing"))?;
|
||||
(set.run)(s, p)
|
||||
}
|
||||
"saveas" | "sa" => saveas::run_saveas(s, p),
|
||||
"list" | "?" => run_list(s, p),
|
||||
_ => Err(bad("layout", "option must be new, copy, delete, rename, set or list")),
|
||||
_ => Err(bad("layout", "option must be new, copy, delete, rename, saveas, set or list")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ enum Stage {
|
||||
RenameFrom,
|
||||
RenameTo(String),
|
||||
Set,
|
||||
SaveasLayout,
|
||||
SaveasFile(String),
|
||||
List,
|
||||
}
|
||||
|
||||
@@ -34,7 +36,7 @@ pub struct LayoutM {
|
||||
}
|
||||
|
||||
/// The layout the name prompts default to: the current one, else the first tab.
|
||||
fn current_layout(s: &Session) -> Option<String> {
|
||||
pub(super) fn current_layout(s: &Session) -> Option<String> {
|
||||
if let Space::Paper(n) = s.layout_space() {
|
||||
return Some(n);
|
||||
}
|
||||
@@ -70,9 +72,10 @@ impl LayoutM {
|
||||
"New" => Stage::New,
|
||||
"Rename" => Stage::RenameFrom,
|
||||
"Set" => Stage::Set,
|
||||
"SAveas" => Stage::SaveasLayout,
|
||||
"?" => Stage::List,
|
||||
other => {
|
||||
// Template, SAveas: report and end, leaving the next input to the command line.
|
||||
// Template: report and end, leaving the next input to the command line.
|
||||
s.echo(format!("LAYOUT {other} is not available yet."));
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
@@ -131,6 +134,8 @@ impl Interactive for LayoutM {
|
||||
let tab = s.layout_space().tab_name().to_string();
|
||||
Prompt::new("Enter layout to make current", Accept::TEXT).default(tab)
|
||||
}
|
||||
Stage::SaveasLayout => with_cur(Prompt::new("Enter layout to save to template", Accept::TEXT)),
|
||||
Stage::SaveasFile(layout) => Prompt::new("Enter template file name", Accept::TEXT).default(super::saveas::default_path(s, layout)),
|
||||
Stage::List => Prompt::new("Enter layout name(s) to list", Accept::TEXT).default("*"),
|
||||
}
|
||||
}
|
||||
@@ -199,6 +204,19 @@ impl Interactive for LayoutM {
|
||||
(set.run)(s, &json!({ "name": n })).map_err(retry)?;
|
||||
Ok(Step::Done)
|
||||
}
|
||||
Stage::SaveasLayout => {
|
||||
let n = need(name.or_else(|| current_layout(s)))?;
|
||||
let n = super::layout_name("layout.saveas", s.doc()?, &n).map_err(retry)?;
|
||||
self.stage = Stage::SaveasFile(n);
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
Stage::SaveasFile(layout) => {
|
||||
let path = name.unwrap_or_else(|| super::saveas::default_path(s, &layout));
|
||||
let r = super::saveas::run_saveas(s, &json!({ "name": layout, "path": path })).map_err(retry)?;
|
||||
let path = r.get("path").and_then(Value::as_str).unwrap_or_default();
|
||||
s.echo(format!("Layout \"{layout}\" saved to template \"{path}\"."));
|
||||
Ok(Step::Done)
|
||||
}
|
||||
Stage::List => {
|
||||
Self::list(s, name.as_deref().unwrap_or("*"))?;
|
||||
Ok(Step::Done)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
//! LAYOUT SAveas: a layout written to a drawing template file (`.dwt`, or a `.dwg` or `.dxf`).
|
||||
//!
|
||||
//! The template holds the layout's page setup and paper space objects, viewports included, and
|
||||
//! no model space objects and no other layout. Named objects the layout does not use (blocks,
|
||||
//! layers, linetypes, text, dimension, multileader, multiline and table styles, empty groups)
|
||||
//! are left out, as PURGE All removes them; layers the layout's viewports freeze or recolor stay.
|
||||
//! The open drawing is not changed.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, Entity, EntityKind, EntityStore, Point};
|
||||
use cadcraft_geom::Vec3;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::super::file::{drops_unknown, io, not_saved, not_saved_message};
|
||||
use super::super::{bad, str_param};
|
||||
use super::layout_name;
|
||||
use super::prompts::current_layout;
|
||||
use crate::{Result, Session};
|
||||
|
||||
/// File types a layout template is written as; a path without an extension gets `.dwt`.
|
||||
const TEMPLATE_TYPES: [&str; 3] = ["dwt", "dwg", "dxf"];
|
||||
|
||||
/// The most PURGE All passes run on a template; they stop at the first pass that purges nothing.
|
||||
const MAX_PURGE_PASSES: usize = 64;
|
||||
|
||||
/// The file a SAveas typed at the command line suggests: `<layout>.dwt` beside the drawing.
|
||||
pub(super) fn default_path(s: &Session, layout: &str) -> String {
|
||||
let file = format!("{layout}.dwt");
|
||||
match s.state().ok().and_then(|st| st.path.clone()) {
|
||||
Some(p) => std::path::Path::new(&p).with_file_name(&file).to_string_lossy().into_owned(),
|
||||
None => file,
|
||||
}
|
||||
}
|
||||
|
||||
/// `path` with `.dwt` added when it has no extension; an error for any other file type.
|
||||
fn template_path(path: &str) -> Result<String> {
|
||||
let path = path.trim();
|
||||
if path.is_empty() {
|
||||
return Err(bad("layout.saveas", "`path` is empty"));
|
||||
}
|
||||
match std::path::Path::new(path).extension() {
|
||||
None => Ok(format!("{path}.dwt")),
|
||||
Some(e) if TEMPLATE_TYPES.iter().any(|t| e.eq_ignore_ascii_case(t)) => Ok(path.to_string()),
|
||||
Some(_) => Err(bad("layout.saveas", "a layout template is saved as a .dwt, .dwg or .dxf file")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The drawing a template of `layout` holds: `d` with that layout alone, an empty model space,
|
||||
/// and without the named objects nothing left in it uses.
|
||||
fn template_of(d: &Drawing, layout: &str) -> Result<Drawing> {
|
||||
let mut t = d.clone();
|
||||
t.layouts.retain(|l| l.name == layout);
|
||||
t.active_layout = Some(layout.to_string());
|
||||
t.model = EntityStore::new();
|
||||
t.hidden_objects.clear();
|
||||
let gone: Vec<u32> = t.constraints.iter().filter(|c| c.refs.iter().any(|r| t.entity(r.handle).is_none())).map(|c| c.id).collect();
|
||||
t.constraints.retain(|c| !gone.contains(&c.id));
|
||||
// Layers the layout's viewports freeze or recolor stay, with those settings. PURGE counts
|
||||
// only objects, so each such layer gets a point in model space while PURGE runs.
|
||||
let named: BTreeSet<String> = t
|
||||
.layouts
|
||||
.iter()
|
||||
.flat_map(|l| l.entities.iter())
|
||||
.filter_map(|e| match &e.kind {
|
||||
EntityKind::Viewport(v) => Some(v.frozen_layers.iter().cloned().chain(v.layer_colors.iter().map(|(n, _)| n.clone())).collect::<Vec<_>>()),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
for layer in named {
|
||||
let handle = t.new_handle();
|
||||
t.model.push(Entity { handle, common: Common { layer, ..Common::default() }, kind: EntityKind::Point(Point { p: Vec3::ZERO, angle: 0.0 }) });
|
||||
}
|
||||
// PURGE runs on a session of its own, so the open drawing keeps everything.
|
||||
let mut scratch = Session::empty();
|
||||
scratch.open_drawing(t, layout, None);
|
||||
for _ in 0..MAX_PURGE_PASSES {
|
||||
let r = super::super::blocks::purge::run(&mut scratch, &json!({ "type": "all" }))?;
|
||||
let purged = r.get("purged").and_then(Value::as_object).is_some_and(|o| o.values().any(|v| v.as_array().is_some_and(|a| !a.is_empty())));
|
||||
if !purged {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let st = scratch.docs.pop().ok_or_else(|| bad("layout.saveas", "the template could not be made"))?;
|
||||
let mut t = std::sync::Arc::unwrap_or_clone(st.doc);
|
||||
t.model = EntityStore::new();
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// `layout.saveas {name?, path}`: write a layout as a drawing template.
|
||||
pub(super) fn run_saveas(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = template_path(str_param(p, "path").ok_or_else(|| bad("layout.saveas", "`path` is required"))?)?;
|
||||
if s.doc()?.refedit.is_some() {
|
||||
return Err(bad("layout.saveas", "A block reference is being edited in place: finish with REFCLOSE (Save or Discard) before saving."));
|
||||
}
|
||||
let name = match str_param(p, "name") {
|
||||
Some(n) => n.to_string(),
|
||||
None => current_layout(s).ok_or_else(|| bad("layout.saveas", "the drawing has no layout"))?,
|
||||
};
|
||||
// A file is the drawing outside the Block Editor.
|
||||
let d = s.state()?.drawing();
|
||||
let name = layout_name("layout.saveas", d, &name)?;
|
||||
let t = template_of(d, &name)?;
|
||||
let hooks = io().ok_or_else(|| bad("layout.saveas", "file formats are not available in this build"))?;
|
||||
let bytes = (hooks.write)(&t, &path).map_err(|e| bad("layout.saveas", e))?;
|
||||
store(&path, &bytes)?;
|
||||
let mut r = json!({ "name": name, "path": path, "bytes": bytes.len() });
|
||||
let lost = not_saved(&t);
|
||||
if drops_unknown(&path)
|
||||
&& let Some(msg) = not_saved_message(&lost)
|
||||
{
|
||||
s.echo(msg);
|
||||
if let Some(o) = r.as_object_mut() {
|
||||
o.insert("notSaved".into(), json!(lost));
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Write the template: natively to `path` through a temporary file; on the web as a download.
|
||||
fn store(path: &str, bytes: &[u8]) -> Result<()> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
// Exclusively create a fresh temporary file and rename it into place so a planted
|
||||
// `<path>.cadcraft-tmp` symlink cannot redirect the write (CWE-59).
|
||||
crate::atomic_write::write_atomic(std::path::Path::new(path), bytes).map_err(|e| bad("layout.saveas", format!("{path}: {e}")))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
super::super::exchange::deliver_file("layout.saveas", path, bytes, super::super::file::deliver())
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ fn typed_layout_options_prompt_for_names() {
|
||||
s.script("LAYOUT\n\nModel\n").unwrap();
|
||||
assert_eq!(s.layout_space(), Space::Model);
|
||||
|
||||
// ? lists; Template and SAveas report and end without taking the next input.
|
||||
// ? lists; Template reports and ends without taking the next input.
|
||||
let n = s.log.len();
|
||||
s.cmdline("LAYOUT ? Sheet*").unwrap();
|
||||
assert!(s.log[n..].iter().any(|l| l == "Layout: Sheet A (2)"), "{:?}", &s.log[n..]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Drawing templates (#552): SAVEAS to a .dwt, NEW/QNEW from a template file, and the default
|
||||
//! template (QNEWTEMPLATE). The io hooks here keep drawings in memory: a "file" holds the number
|
||||
//! template (QNEWTEMPLATE), and a layout saved as a template (LAYOUT SAveas). The io hooks here keep drawings in memory: a "file" holds the number
|
||||
//! of the drawing written, so the test sees exactly what the engine hands the file formats.
|
||||
|
||||
use std::sync::Mutex;
|
||||
@@ -105,3 +105,95 @@ fn a_template_that_cannot_be_read_makes_no_drawing() {
|
||||
assert!(s.execute("new", &json!({ "template": 42 })).is_err());
|
||||
assert_eq!(s.docs.len(), docs);
|
||||
}
|
||||
|
||||
/// The drawing the io hooks were last handed for a file whose name ends with `name`.
|
||||
fn written(name: &str) -> Drawing {
|
||||
FILES.lock().unwrap().iter().rev().find(|(n, _)| n.ends_with(name)).map(|f| f.1.clone()).unwrap()
|
||||
}
|
||||
|
||||
fn layout_names(d: &Drawing) -> Vec<String> {
|
||||
d.layouts.iter().map(|l| l.name.clone()).collect()
|
||||
}
|
||||
|
||||
/// LAYOUT SAveas: the template holds the layout alone, with the named objects it uses, and the
|
||||
/// drawing it came from keeps everything.
|
||||
#[test]
|
||||
fn layout_saveas_writes_the_layout_and_the_named_objects_it_uses() {
|
||||
hooks();
|
||||
let mut s = Session::new();
|
||||
for name in ["TITLE", "PARTS-LAYER", "UNUSED", "WALLS", "NOTES"] {
|
||||
s.execute("layer.new", &json!({ "name": name })).unwrap();
|
||||
}
|
||||
s.execute("layer.current", &json!({ "name": "PARTS-LAYER" })).unwrap();
|
||||
let c = s.execute("circle", &json!({ "center": [5, 5], "radius": 2 })).unwrap()["handle"].clone();
|
||||
s.execute("block", &json!({ "name": "PARTS", "base": [0, 0], "handles": [c], "keep": "retain" })).unwrap();
|
||||
// WALLS has model space objects only, and NOTES none; the sheet's viewport freezes one and recolors the other.
|
||||
s.execute("layer.current", &json!({ "name": "WALLS" })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[0, 0], [0, 10]] })).unwrap();
|
||||
let vp = s.execute("layout.new", &json!({ "name": "Sheet" })).unwrap()["viewport"].clone();
|
||||
s.execute("viewport.set", &json!({ "handle": vp, "freeze": ["WALLS"], "colors": { "NOTES": 1 } })).unwrap();
|
||||
s.execute("layout.set", &json!({ "name": "Sheet" })).unwrap();
|
||||
s.execute("layer.current", &json!({ "name": "TITLE" })).unwrap();
|
||||
s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap();
|
||||
s.execute("layer.current", &json!({ "name": "0" })).unwrap();
|
||||
let sheet_objects = s.doc().unwrap().layout("Sheet").unwrap().entities.iter().count();
|
||||
|
||||
let p = path("sheet.dwt");
|
||||
let r = s.execute("layout.saveas", &json!({ "path": p })).unwrap();
|
||||
assert_eq!(r["name"], json!("Sheet"), "the current layout by default");
|
||||
assert_eq!(r["path"], json!(p));
|
||||
let t = written("sheet.dwt");
|
||||
assert_eq!(layout_names(&t), ["Sheet"]);
|
||||
assert_eq!(t.active_layout.as_deref(), Some("Sheet"));
|
||||
assert_eq!(t.model.iter().count(), 0, "no model space objects");
|
||||
assert_eq!(t.layout("Sheet").unwrap().entities.iter().count(), sheet_objects, "the layout's viewport and line");
|
||||
assert!(t.layer("TITLE").is_some() && t.layer("0").is_some());
|
||||
assert!(t.layer("UNUSED").is_none() && t.layer("PARTS-LAYER").is_none(), "unused layers are left out");
|
||||
assert!(t.block("PARTS").is_none(), "unused blocks are left out");
|
||||
// Layers the viewport freezes or recolors stay, and so do those settings.
|
||||
assert!(t.layer("WALLS").is_some() && t.layer("NOTES").is_some());
|
||||
let sheet_vp = t.layout("Sheet").unwrap().entities.iter().find_map(|e| match &e.kind {
|
||||
cadcraft_engine::doc::EntityKind::Viewport(v) if v.id != 1 => Some(v.clone()),
|
||||
_ => None,
|
||||
});
|
||||
let sheet_vp = sheet_vp.unwrap();
|
||||
assert_eq!(sheet_vp.frozen_layers, ["WALLS"]);
|
||||
assert_eq!(sheet_vp.layer_colors.len(), 1);
|
||||
|
||||
// The drawing is not changed.
|
||||
let d = s.doc().unwrap();
|
||||
assert_eq!(layout_names(d), ["Layout1", "Layout2", "Sheet"]);
|
||||
assert_eq!(d.model.iter().count(), 2);
|
||||
assert!(d.layer("UNUSED").is_some() && d.block("PARTS").is_some());
|
||||
|
||||
// A drawing started from the template has the layout.
|
||||
s.execute("new", &json!({ "template": p })).unwrap();
|
||||
assert_eq!(layout_names(s.doc().unwrap()), ["Sheet"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_layout_saveas_asks_for_the_layout_and_the_template_file() {
|
||||
hooks();
|
||||
let mut s = Session::new();
|
||||
s.cmdline("LAYOUT").unwrap();
|
||||
s.cmdline("sa").unwrap();
|
||||
assert_eq!(s.current_prompt().unwrap().display(), "Enter layout to save to template <Layout1>:");
|
||||
s.cmdline("Layout2").unwrap();
|
||||
assert_eq!(s.current_prompt().unwrap().display(), "Enter template file name <Layout2.dwt>:");
|
||||
// No extension: the template is a .dwt.
|
||||
let p = path("typed");
|
||||
s.cmdline(&p).unwrap();
|
||||
assert!(s.current_prompt().is_none());
|
||||
let saved = format!("{p}.dwt");
|
||||
assert!(s.log.iter().any(|l| *l == format!("Layout \"Layout2\" saved to template \"{saved}\".")), "{:?}", s.log);
|
||||
assert_eq!(layout_names(&written("typed.dwt")), ["Layout2"]);
|
||||
|
||||
// The JSON form of LAYOUT, other file types, a missing path and an unknown layout.
|
||||
let dxf = path("sheet1.dxf");
|
||||
let r = s.execute("layout", &json!({ "option": "saveas", "name": "layout1", "path": dxf })).unwrap();
|
||||
assert_eq!(r["name"], json!("Layout1"));
|
||||
assert_eq!(layout_names(&written("sheet1.dxf")), ["Layout1"]);
|
||||
assert!(s.execute("layout.saveas", &json!({ "path": path("sheet.png") })).is_err());
|
||||
assert!(s.execute("layout.saveas", &json!({})).is_err());
|
||||
assert!(s.execute("layout.saveas", &json!({ "name": "Nope", "path": path("nope.dwt") })).is_err());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (DIMASSOC objects for angular, diametric and radial dimensions) · **Target:** Autodesk AutoCAD 2027
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LAYOUT SAveas writes a layout as a DWT template) · **Target:** Autodesk AutoCAD 2027
|
||||
|
||||
Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work
|
||||
items are in [gaps.md](gaps.md#file-formats); the weighted score feeds
|
||||
@@ -108,7 +108,7 @@ on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG
|
||||
|
||||
| Format | AutoCAD 2027 | CADCraft | Hours to parity (est.) |
|
||||
|---|---|---|---|
|
||||
| DWT templates | open, save | NEW/QNEW from `.dwt` and `.dxf` templates, SAVEAS `.dwt` (#552) | 0–1 |
|
||||
| DWT templates | open, save | NEW/QNEW from `.dwt` and `.dxf` templates, SAVEAS `.dwt` (#552); LAYOUT SAveas writes one layout and the named objects it uses as a `.dwt` (`layout.saveas`) | 0–1 |
|
||||
| DWS standards | open, save, CHECKSTANDARDS | no | 6–10 |
|
||||
| PDF output (PLOT, EXPORTPDF) | multi-sheet, layers, searchable text, hyperlinks | vector PDF with lineweights, real text in embedded TrueType subsets, layers (OCG), several sheets per file and hyperlinks (#567, #571); object transparency as PDF alpha when "Plot transparency" is on (the option isn't saved in DXF/DWG: no public plot layout flag) | 1–2 |
|
||||
| PDF import (PDFIMPORT) and PDF underlays (PDFATTACH) | yes | no | 15–25 |
|
||||
@@ -140,6 +140,7 @@ our output in AutoCAD black-box.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-25 | minor | LAYOUT SAveas (`layout.saveas`) writes a layout as a drawing template: its page setup and paper space objects, no model space objects or other layouts, and only the named objects they use |
|
||||
| 2026-10-22 | minor | DXF/DWG save: angular, diametric and radial dimensions get standard DIMASSOC objects (one centre reference to the circle for radial/diametric, the definition points for angular), so AutoCAD follows their geometry; the standard objects of those kinds are read back too, without CADCraft's xdata (#1651) |
|
||||
| 2026-10-19 | minor | DXF/DWG save: angular dimension block text honors DIMDSEP, including per-dimension overrides (#1646) |
|
||||
| 2026-10-19 | minor | A file's own arrowhead blocks with standard names (AutoCAD's `_Dot`, `_DotSmall`), named only by a dimension style, are kept and saved as they were instead of being replaced by CADCraft's solids; PURGE keeps arrowhead blocks dimension styles name |
|
||||
|
||||
Reference in New Issue
Block a user