Squash of PR #431 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9e3379882f
commit
b3a73626b2
|
|
@ -616,6 +616,7 @@ dependencies = [
|
|||
"cadcraft-color",
|
||||
"cadcraft-constraints",
|
||||
"cadcraft-doc",
|
||||
"cadcraft-dxf",
|
||||
"cadcraft-fonts",
|
||||
"cadcraft-geom",
|
||||
"cadcraft-render",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ scriptable CLI, a web build, Linux and FreeBSD builds, and a free licence.
|
|||
**Why pre-alpha:** four of AutoCAD's five core workflows fail the [alpha gate](docs/roadmap.md#alpha-gate).
|
||||
Precise drafting lacks object snap tracking and has stubbed TRIM/EXTEND/OFFSET options; blocks
|
||||
can't be edited (no block editor, REFEDIT or xrefs); plots ignore plot styles; and a DWG saved by
|
||||
CADCraft drops everything it doesn't model, is always R2000 and has never been checked in
|
||||
CADCraft drops everything it doesn't model and has never been checked in
|
||||
AutoCAD. Dimensioning passes. Ready for real work (≈ 38%) is also under the ~40% alpha bar. Beta
|
||||
is ≈ 600–930 h away.
|
||||
|
||||
|
|
|
|||
|
|
@ -132,6 +132,9 @@ fn install_io() {
|
|||
write: |d, name| cadcraft_io::write(d, name).map_err(|e| e.to_string()),
|
||||
plot: Some(|d, space, opts| cadcraft_io::plot(d, space, opts).map_err(|e| e.to_string())),
|
||||
});
|
||||
cadcraft_engine::cmd::file::set_write_as(|d, name, o| {
|
||||
cadcraft_io::write_with(d, name, &cadcraft_io::SaveOptions { version: o.version, binary: o.binary }).map_err(|e| e.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn open(s: &mut Session, path: &str) -> Result<(), String> {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ mod web {
|
|||
write: |d, name| cadcraft_io::write(d, name).map_err(|e| e.to_string()),
|
||||
plot: Some(|d, space, opts| cadcraft_io::plot(d, space, opts).map_err(|e| e.to_string())),
|
||||
});
|
||||
cadcraft_engine::cmd::file::set_write_as(|d, name, o| {
|
||||
cadcraft_io::write_with(d, name, &cadcraft_io::SaveOptions { version: o.version, binary: o.binary }).map_err(|e| e.to_string())
|
||||
});
|
||||
// No file system here: saved files reach the user as browser downloads.
|
||||
cadcraft_engine::cmd::file::set_deliver(crate::download::download);
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,9 @@ pub fn install_io() {
|
|||
write: |d, name| cadcraft_io::write(d, name).map_err(|e| e.to_string()),
|
||||
plot: Some(|d, space, opts| cadcraft_io::plot(d, space, opts).map_err(|e| e.to_string())),
|
||||
});
|
||||
cadcraft_engine::cmd::file::set_write_as(|d, name, o| {
|
||||
cadcraft_io::write_with(d, name, &cadcraft_io::SaveOptions { version: o.version, binary: o.binary }).map_err(|e| e.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn services() -> Services {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ workspace = true
|
|||
cadcraft-geom = { workspace = true }
|
||||
cadcraft-color = { workspace = true }
|
||||
cadcraft-doc = { workspace = true }
|
||||
cadcraft-dxf = { workspace = true }
|
||||
cadcraft-constraints = { workspace = true }
|
||||
cadcraft-fonts = { workspace = true }
|
||||
cadcraft-render = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use cadcraft_doc::Drawing;
|
||||
pub use cadcraft_dxf::Version;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
|
|
@ -45,6 +46,77 @@ pub fn deliver() -> Option<DeliverHook> {
|
|||
DELIVER.get().copied()
|
||||
}
|
||||
|
||||
/// How a drawing file is written: its version (`None`: the drawing's own, `$ACADVER`) and
|
||||
/// binary instead of ASCII DXF.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct SaveAs {
|
||||
pub version: Option<Version>,
|
||||
pub binary: bool,
|
||||
}
|
||||
|
||||
/// Write a drawing with [`SaveAs`] options (installed by hosts whose writer supports them).
|
||||
pub type WriteAsHook = fn(&Drawing, &str, &SaveAs) -> std::result::Result<Vec<u8>, String>;
|
||||
|
||||
static WRITE_AS: OnceLock<WriteAsHook> = OnceLock::new();
|
||||
|
||||
/// Install the host's writer for [`SaveAs`] options; saves that ask for none use [`IoHooks::write`].
|
||||
pub fn set_write_as(h: WriteAsHook) {
|
||||
let _ = WRITE_AS.set(h);
|
||||
}
|
||||
pub fn write_as() -> Option<WriteAsHook> {
|
||||
WRITE_AS.get().copied()
|
||||
}
|
||||
|
||||
/// The versions a drawing can be saved in, for messages ("2000, 2004, …, 2018").
|
||||
fn version_list() -> String {
|
||||
Version::ALL.iter().map(|v| v.year()).collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
|
||||
/// The `version` and `binary` parameters of a save: a version is a year (2018 or "2018"), "R2018"
|
||||
/// or an `$ACADVER` code ("AC1032").
|
||||
fn save_options(cmd: &str, p: &Value) -> Result<SaveAs> {
|
||||
let version = match p.get("version") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => {
|
||||
let text = match v {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let name: String = text.chars().take(40).collect();
|
||||
Some(Version::parse(&text).ok_or_else(|| bad(cmd, format!("unknown version `{name}` (use {})", version_list())))?)
|
||||
}
|
||||
};
|
||||
let binary = match p.get("binary") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(_) => return Err(bad(cmd, "`binary` must be true or false")),
|
||||
};
|
||||
Ok(SaveAs { version, binary })
|
||||
}
|
||||
|
||||
/// Whether `path` names a DXF file (or has no extension, which saves as DXF).
|
||||
fn is_dxf(path: &str) -> bool {
|
||||
let ext = std::path::Path::new(path).extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default();
|
||||
matches!(ext.as_str(), "dxf" | "")
|
||||
}
|
||||
|
||||
/// The drawing written to `path` with `opts`: through [`IoHooks::write`] when no option is set,
|
||||
/// else through the [`write_as`] hook.
|
||||
fn write_bytes(s: &Session, cmd: &str, path: &str, opts: &SaveAs) -> Result<Vec<u8>> {
|
||||
let hooks = io().ok_or_else(|| bad(cmd, "file formats are not available in this build"))?;
|
||||
if opts.binary && !is_dxf(path) {
|
||||
return Err(bad(cmd, "binary applies to DXF files only"));
|
||||
}
|
||||
let r = if *opts == SaveAs::default() {
|
||||
(hooks.write)(s.doc()?, path)
|
||||
} else {
|
||||
let w = write_as().ok_or_else(|| bad(cmd, "saving at a chosen version or as binary DXF is not available in this build"))?;
|
||||
w(s.doc()?, path, opts)
|
||||
};
|
||||
r.map_err(|e| bad(cmd, e))
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("new", "New Drawing...", run_new)
|
||||
|
|
@ -66,10 +138,12 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
CommandSpec::new("saveas", "Save As...", run_saveas_reporting)
|
||||
.menu(&["File", "Save As..."])
|
||||
.key("Cmd+Shift+S")
|
||||
.params("{path, format?: dxf|dwg}")
|
||||
.params("{path, version?: 2000|2004|2007|2010|2013|2018, binary?: bool}")
|
||||
.noundo(),
|
||||
CommandSpec::new("document.switch", "Switch Drawing", run_switch).params("{index}").enabled(always).noundo(),
|
||||
CommandSpec::new("document.bytes", "Drawing as bytes", run_bytes).params("{format?: dxf} → {data: base64}").noundo(),
|
||||
CommandSpec::new("document.bytes", "Drawing as bytes", run_bytes)
|
||||
.params("{format?: dxf, version?: 2000…2018, binary?: bool} → {data: base64}")
|
||||
.noundo(),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +159,12 @@ fn file_name(path: &str) -> String {
|
|||
pub(crate) fn open_bytes(s: &mut Session, bytes: &[u8], name: &str, path: Option<String>) -> Result<usize> {
|
||||
let hooks = io().ok_or_else(|| bad("open", "file formats are not available in this build"))?;
|
||||
let d = (hooks.read)(bytes, name).map_err(|e| bad("open", e))?;
|
||||
Ok(s.open_drawing(d, &file_name(name), path))
|
||||
let i = s.open_drawing(d, &file_name(name), path);
|
||||
if let Some(st) = s.docs.get_mut(i) {
|
||||
// QSAVE keeps a binary DXF binary.
|
||||
st.binary_dxf = cadcraft_dxf::is_binary(bytes);
|
||||
}
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
fn run_open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
@ -133,9 +212,26 @@ fn run_closeall(s: &mut Session, _p: &Value) -> Result<Value> {
|
|||
}
|
||||
|
||||
pub(crate) fn save_to(s: &mut Session, path: &str) -> Result<usize> {
|
||||
let hooks = io().ok_or_else(|| bad("save", "file formats are not available in this build"))?;
|
||||
let bytes = (hooks.write)(s.doc()?, path).map_err(|e| bad("save", e))?;
|
||||
commit_save(s, path, &bytes, store)
|
||||
let opts = SaveAs { version: None, binary: s.state()?.binary_dxf && is_dxf(path) };
|
||||
save_as(s, "save", path, &opts)
|
||||
}
|
||||
|
||||
/// Save to `path` with `opts`. A drawing file then keeps the version and binary choice: the
|
||||
/// version goes into the drawing's `$ACADVER`, so later saves (QSAVE) write it again.
|
||||
fn save_as(s: &mut Session, cmd: &str, path: &str, opts: &SaveAs) -> Result<usize> {
|
||||
let bytes = write_bytes(s, cmd, path, opts)?;
|
||||
let n = commit_save(s, path, &bytes, store)?;
|
||||
if is_drawing_file(path) {
|
||||
let st = s.state_mut()?;
|
||||
st.binary_dxf = opts.binary;
|
||||
if let Some(v) = opts.version
|
||||
&& st.doc.header.get("ACADVER").and_then(cadcraft_doc::HVal::as_str) != Some(v.code())
|
||||
{
|
||||
std::sync::Arc::make_mut(&mut st.doc).header.set_str("ACADVER", v.code());
|
||||
st.saved = st.doc.clone();
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Put saved bytes where the user gets them: natively the file at `path` (written atomically); on
|
||||
|
|
@ -194,8 +290,22 @@ fn run_save(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
fn run_saveas(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = str_param(p, "path").ok_or_else(|| bad("saveas", "`path` is required"))?.to_string();
|
||||
let n = save_to(s, &path)?;
|
||||
Ok(saved(&path, n))
|
||||
let opts = save_options("saveas", p)?;
|
||||
let n = save_as(s, "saveas", &path, &opts)?;
|
||||
let mut r = saved(&path, n);
|
||||
if is_drawing_file(&path) {
|
||||
r["version"] = json!(opts.version.unwrap_or_else(|| drawing_version(s, &path)).year());
|
||||
r["binary"] = json!(opts.binary);
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The version a drawing is saved in at `path` when none is asked for: its `$ACADVER` (2018 by
|
||||
/// default), and for DWG at least 2004 (the io layer's rule: R2000 DWG has no true colours).
|
||||
fn drawing_version(s: &Session, path: &str) -> Version {
|
||||
let v =
|
||||
s.doc().ok().and_then(|d| d.header.get("ACADVER").and_then(cadcraft_doc::HVal::as_str).and_then(Version::for_acadver)).unwrap_or_default();
|
||||
if is_dxf(path) { v } else { v.max(Version::R2004) }
|
||||
}
|
||||
|
||||
fn run_switch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
@ -209,10 +319,10 @@ fn run_switch(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
|
||||
fn run_bytes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hooks = io().ok_or_else(|| bad("document.bytes", "file formats are not available in this build"))?;
|
||||
let fmt = str_param(p, "format").unwrap_or("dxf");
|
||||
let opts = save_options("document.bytes", p)?;
|
||||
let lost = not_saved(s.doc()?);
|
||||
let bytes = (hooks.write)(s.doc()?, &format!("drawing.{fmt}")).map_err(|e| bad("document.bytes", e))?;
|
||||
let bytes = write_bytes(s, "document.bytes", &format!("drawing.{fmt}"), &opts)?;
|
||||
let mut r = json!({ "data": base64_encode(&bytes), "bytes": bytes.len() });
|
||||
if is_drawing_file(&format!("drawing.{fmt}")) {
|
||||
report_not_saved(s, &mut r, &lost);
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ pub struct DocState {
|
|||
pub views: Vec<(Space, View)>,
|
||||
pub view_history: Vec<View>,
|
||||
pub uid: u64,
|
||||
/// The drawing is saved as binary DXF (it was opened from one, or last saved as one).
|
||||
pub binary_dxf: bool,
|
||||
}
|
||||
|
||||
static NEXT_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
|
@ -122,6 +124,7 @@ impl DocState {
|
|||
views: Vec::new(),
|
||||
view_history: Vec::new(),
|
||||
uid: NEXT_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
binary_dxf: false,
|
||||
}
|
||||
}
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
//! SAVEAS at a chosen version and as binary DXF (#421): the options reach the host's writer, the
|
||||
//! drawing keeps them for later saves, and bad values are errors.
|
||||
|
||||
use cadcraft_doc::{Drawing, HVal};
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::cmd::file::{IoHooks, SaveAs, base64_decode, base64_encode, set_io, set_write_as};
|
||||
use serde_json::json;
|
||||
|
||||
/// The plain writer (no options).
|
||||
fn plain(_: &Drawing, _: &str) -> Result<Vec<u8>, String> {
|
||||
Ok(b"plain".to_vec())
|
||||
}
|
||||
|
||||
/// The writer with options: the bytes name the options it got.
|
||||
fn with_options(_: &Drawing, _: &str, o: &SaveAs) -> Result<Vec<u8>, String> {
|
||||
Ok(format!("{}|{}", o.version.map(|v| v.year()).unwrap_or("-"), o.binary).into_bytes())
|
||||
}
|
||||
|
||||
fn install() {
|
||||
set_io(IoHooks { read: |_, _| Ok(Drawing::new_metric()), write: plain, plot: None });
|
||||
set_write_as(with_options);
|
||||
}
|
||||
|
||||
fn acadver(s: &Session) -> Option<String> {
|
||||
s.state().unwrap().doc.header.get("ACADVER").and_then(HVal::as_str).map(str::to_string)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saveas_version_and_binary_reach_the_writer_and_stay_with_the_drawing() {
|
||||
install();
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-save-version-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = |name: &str| dir.join(name).to_string_lossy().to_string();
|
||||
let read = |name: &str| String::from_utf8(std::fs::read(dir.join(name)).unwrap()).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.cmdline("circle 0,0 5").unwrap();
|
||||
assert_eq!(acadver(&s).as_deref(), Some("AC1032"), "a new drawing is 2018");
|
||||
|
||||
let r = s.execute("saveas", &json!({ "path": path("a.dxf"), "version": "2010" })).unwrap();
|
||||
assert_eq!(read("a.dxf"), "2010|false");
|
||||
assert_eq!((r["version"].clone(), r["binary"].clone()), (json!("2010"), json!(false)));
|
||||
assert_eq!(acadver(&s).as_deref(), Some("AC1024"), "the drawing is now a 2010 drawing");
|
||||
assert!(!s.state().unwrap().is_dirty());
|
||||
// QSAVE writes the drawing's own version (the plain writer reads it from the drawing).
|
||||
s.execute("qsave", &json!({})).unwrap();
|
||||
assert_eq!(read("a.dxf"), "plain");
|
||||
|
||||
// Binary, version as a number; QSAVE stays binary.
|
||||
s.execute("saveas", &json!({ "path": path("b.dxf"), "version": 2004, "binary": true })).unwrap();
|
||||
assert_eq!(read("b.dxf"), "2004|true");
|
||||
s.execute("qsave", &json!({})).unwrap();
|
||||
assert_eq!(read("b.dxf"), "-|true");
|
||||
// Saving as DXF without `binary` goes back to ASCII.
|
||||
s.execute("saveas", &json!({ "path": path("c.dxf") })).unwrap();
|
||||
assert_eq!(read("c.dxf"), "plain");
|
||||
assert!(!s.state().unwrap().binary_dxf);
|
||||
|
||||
// document.bytes takes the same options.
|
||||
let r = s.execute("document.bytes", &json!({ "version": "R2013" })).unwrap();
|
||||
assert_eq!(base64_decode(r["data"].as_str().unwrap()).unwrap(), b"2013|false");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_save_options_are_errors() {
|
||||
install();
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-save-version-bad-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = |name: &str| dir.join(name).to_string_lossy().to_string();
|
||||
let mut s = Session::new();
|
||||
let err = |s: &mut Session, p: serde_json::Value| s.execute("saveas", &p).unwrap_err().to_string();
|
||||
assert!(err(&mut s, json!({ "path": path("x.dxf"), "version": "2005" })).contains("2000, 2004, 2007, 2010, 2013, 2018"));
|
||||
assert!(err(&mut s, json!({ "path": path("x.dxf"), "version": [2018] })).contains("unknown version"));
|
||||
assert!(err(&mut s, json!({ "path": path("x.dxf"), "binary": "yes" })).contains("binary"));
|
||||
assert!(err(&mut s, json!({ "path": path("x.dwg"), "binary": true })).contains("DXF"));
|
||||
assert!(!dir.join("x.dxf").exists() && !dir.join("x.dwg").exists(), "nothing is written");
|
||||
assert_eq!(acadver(&s).as_deref(), Some("AC1032"), "a failed save leaves the version alone");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A drawing opened from a binary DXF saves as binary DXF again.
|
||||
#[test]
|
||||
fn opening_binary_dxf_keeps_it_binary() {
|
||||
install();
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-save-version-open-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut s = Session::new();
|
||||
let binary = b"AutoCAD Binary DXF\r\n\x1a\0\0\0EOF\0";
|
||||
s.execute("open", &json!({ "data": base64_encode(binary), "name": "bin.dxf" })).unwrap();
|
||||
assert!(s.state().unwrap().binary_dxf);
|
||||
let out = dir.join("again.dxf");
|
||||
s.execute("qsave", &json!({ "path": out.to_string_lossy() })).unwrap();
|
||||
assert_eq!(std::fs::read(&out).unwrap(), b"-|true");
|
||||
// An ASCII file stays ASCII.
|
||||
s.execute("open", &json!({ "data": base64_encode(b"0\nEOF\n"), "name": "text.dxf" })).unwrap();
|
||||
assert!(!s.state().unwrap().binary_dxf);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
|
@ -151,6 +151,7 @@
|
|||
Dark Темна
|
||||
Default Типово
|
||||
Default Save Format Типовий формат збереження
|
||||
Default Save Version Типова версія збереження
|
||||
Define Attributes Визначити атрибути
|
||||
Define Attributes... Визначити атрибути...
|
||||
Defined width Задана ширина
|
||||
|
|
@ -948,6 +949,12 @@
|
|||
Value Значення
|
||||
Value: Значення:
|
||||
Version Версія
|
||||
Version 2000 Версія 2000
|
||||
Version 2004 Версія 2004
|
||||
Version 2007 Версія 2007
|
||||
Version 2010 Версія 2010
|
||||
Version 2013 Версія 2013
|
||||
Version 2018 Версія 2018
|
||||
Version {version} Версія {version}
|
||||
Vertical Вертикальність
|
||||
VerticalDistance Вертикальна відстань
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -81,8 +81,14 @@ pub struct UiState {
|
|||
pub theme: theme::ThemePref,
|
||||
/// The format Save As suggests for a drawing that has no file yet: "dxf" or "dwg".
|
||||
pub save_format: SaveFormat,
|
||||
/// The version Save As writes for a drawing that has no file yet ("2000" … "2018"); a
|
||||
/// drawing opened from a file keeps that file's version.
|
||||
pub save_version: String,
|
||||
}
|
||||
|
||||
/// A DXF/DWG file version (2000 … 2018).
|
||||
pub use cadcraft_engine::cmd::file::Version as SaveVersion;
|
||||
|
||||
/// The default save format for new drawings, like AutoCAD's Options > Open and Save > "Save as".
|
||||
/// A drawing opened from a file keeps that file's format.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -145,6 +151,7 @@ impl Default for UiState {
|
|||
history_lines: 3,
|
||||
theme: theme::ThemePref::default(),
|
||||
save_format: SaveFormat::default(),
|
||||
save_version: SaveVersion::default().year().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -292,6 +299,7 @@ impl CadApp {
|
|||
"theme": self.ui.theme.as_str(),
|
||||
"interfaceLanguage": self.ui.interface_language.code(),
|
||||
"saveFormat": self.ui.save_format.as_str(),
|
||||
"saveVersion": self.ui.save_version,
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
|
@ -308,6 +316,9 @@ impl CadApp {
|
|||
if let Some(f) = v.get("saveFormat").and_then(Value::as_str).and_then(SaveFormat::parse) {
|
||||
self.ui.save_format = f;
|
||||
}
|
||||
if let Some(v) = v.get("saveVersion").and_then(Value::as_str).and_then(SaveVersion::parse) {
|
||||
self.ui.save_version = v.year().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, s: impl Into<String>) {
|
||||
|
|
@ -597,7 +608,7 @@ mod tests {
|
|||
frame(&mut a, &ctx, None);
|
||||
assert_eq!(Tokens::get(), Tokens::of(theme::SYSTEM_FALLBACK), "no OS appearance: the documented fallback");
|
||||
assert_eq!(a.ui.theme, ThemePref::System);
|
||||
assert_eq!(a.prefs_json(), r#"{"interfaceLanguage":"auto","saveFormat":"dxf","theme":"system"}"#);
|
||||
assert_eq!(a.prefs_json(), r#"{"interfaceLanguage":"auto","saveFormat":"dxf","saveVersion":"2018","theme":"system"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
|
|||
("ui.saveformat.dxf", "DXF", &["File", "Default Save Format", "DXF"], None),
|
||||
("ui.saveformat.dwg", "DWG", &["File", "Default Save Format", "DWG"], None),
|
||||
("ui.saveformat", "Default Save Format", &[], None),
|
||||
("ui.saveversion.2018", "Version 2018", &["File", "Default Save Format", "Version 2018"], None),
|
||||
("ui.saveversion.2013", "Version 2013", &["File", "Default Save Format", "Version 2013"], None),
|
||||
("ui.saveversion.2010", "Version 2010", &["File", "Default Save Format", "Version 2010"], None),
|
||||
("ui.saveversion.2007", "Version 2007", &["File", "Default Save Format", "Version 2007"], None),
|
||||
("ui.saveversion.2004", "Version 2004", &["File", "Default Save Format", "Version 2004"], None),
|
||||
("ui.saveversion.2000", "Version 2000", &["File", "Default Save Format", "Version 2000"], None),
|
||||
("ui.saveversion", "Default Save Version", &[], None),
|
||||
("ui.toggle.menubar", "In-window Menu Bar", &["Window", "In-window Menu Bar"], None),
|
||||
("ui.hidepalettes", "Hide Palettes", &["Window", "Hide Palettes"], None),
|
||||
("ui.resetpalettes", "Reset Palettes", &["Window", "Reset Palettes"], None),
|
||||
|
|
@ -110,7 +117,7 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
|
|||
let title = app.session.state().map(|s| s.title.clone()).ok();
|
||||
let name = app.ui.save_format.suggested_name(title.as_deref());
|
||||
if let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name)) {
|
||||
return Some(app.session.execute("saveas", &json!({ "path": p })).map_err(|e| e.to_string()));
|
||||
return Some(app.session.execute("saveas", &saveas_params(&app.ui, &app.session, &p)).map_err(|e| e.to_string()));
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
|
@ -209,6 +216,25 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
|
|||
},
|
||||
None => Ok(json!({ "saveFormat": app.ui.save_format.as_str() })),
|
||||
},
|
||||
// `{"version": "2000"…"2018"}` sets the version new drawings are saved in; without it,
|
||||
// reports it.
|
||||
id if id.starts_with("ui.saveversion") => {
|
||||
let name = id
|
||||
.strip_prefix("ui.saveversion.")
|
||||
.map(str::to_string)
|
||||
.or_else(|| params.get("version").map(|v| v.as_str().map_or_else(|| v.to_string(), str::to_string)));
|
||||
match name {
|
||||
Some(name) => match crate::SaveVersion::parse(&name) {
|
||||
Some(v) => {
|
||||
app.ui.save_version = v.year().to_string();
|
||||
app.session.echo(format!("New drawings will be saved as version {v}"));
|
||||
Ok(json!({ "saveVersion": app.ui.save_version }))
|
||||
}
|
||||
None => Err(format!("unknown version \"{}\" (2000, 2004, 2007, 2010, 2013 or 2018)", name.chars().take(40).collect::<String>())),
|
||||
},
|
||||
None => Ok(json!({ "saveVersion": app.ui.save_version })),
|
||||
}
|
||||
}
|
||||
"ui.hidepalettes" => {
|
||||
app.ui.show_palettes = false;
|
||||
app.ui.show_toolsets = false;
|
||||
|
|
@ -216,7 +242,8 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
|
|||
}
|
||||
"ui.resetpalettes" => {
|
||||
let (menu, theme, interface_language, save_format) = (app.ui.in_window_menu, app.ui.theme, app.ui.interface_language, app.ui.save_format);
|
||||
app.ui = crate::UiState { in_window_menu: menu, theme, interface_language, save_format, ..Default::default() };
|
||||
let save_version = std::mem::take(&mut app.ui.save_version);
|
||||
app.ui = crate::UiState { in_window_menu: menu, theme, interface_language, save_format, save_version, ..Default::default() };
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"ui.start" => {
|
||||
|
|
@ -300,6 +327,16 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
|
|||
Some(r)
|
||||
}
|
||||
|
||||
/// SAVEAS parameters for a path picked in the Save dialog: a drawing that has no file yet is saved
|
||||
/// in the default save version; one opened from a file keeps that file's version.
|
||||
fn saveas_params(ui: &crate::UiState, session: &cadcraft_engine::Session, path: &str) -> Value {
|
||||
let untitled = session.state().is_ok_and(|s| s.path.is_none());
|
||||
match crate::SaveVersion::parse(&ui.save_version) {
|
||||
Some(v) if untitled => json!({ "path": path, "version": v.year() }),
|
||||
_ => json!({ "path": path }),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_save_format(app: &mut CadApp, format: crate::SaveFormat) -> Value {
|
||||
app.ui.save_format = format;
|
||||
app.session.echo(format!("New drawings will be saved as {}", format.as_str().to_ascii_uppercase()));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
//! The default save version (#421): File > Default Save Format > Version …, kept in the
|
||||
//! preferences, applied by the Save dialog to drawings that have no file yet.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::cmd::file::{IoHooks, SaveAs, set_io, set_write_as};
|
||||
use cadcraft_engine::doc::Drawing;
|
||||
use cadcraft_ui_egui::{CadApp, Services};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn picked() -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!("cadcraft-ui-save-version-{}.dxf", std::process::id()))
|
||||
}
|
||||
|
||||
fn with_options(_: &Drawing, _: &str, o: &SaveAs) -> Result<Vec<u8>, String> {
|
||||
Ok(format!("{}|{}", o.version.map(|v| v.year()).unwrap_or("-"), o.binary).into_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_save_version_applies_to_new_drawings_and_persists() {
|
||||
set_io(IoHooks { read: |_, _| Ok(Drawing::new_metric()), write: |_, _| Ok(b"plain".to_vec()), plot: None });
|
||||
set_write_as(with_options);
|
||||
let services = Services { pick_save: Some(Box::new(|_: &str| Some(picked().to_string_lossy().to_string()))), ..Services::default() };
|
||||
let mut app = CadApp::new(Session::new(), services);
|
||||
assert_eq!(app.run("ui.saveversion", json!({})).ok(), Some(json!({ "saveVersion": "2018" })), "2018 by default");
|
||||
assert!(app.run("ui.saveversion", json!({ "version": "2005" })).is_err());
|
||||
assert_eq!(app.run("ui.saveversion", json!({ "version": 2010 })).ok(), Some(json!({ "saveVersion": "2010" })));
|
||||
|
||||
// The Save dialog saves an untitled drawing in the default version …
|
||||
app.run("ui.saveas", Value::Null).unwrap();
|
||||
assert_eq!(std::fs::read(picked()).unwrap(), b"2010|false");
|
||||
// … and a drawing that has a file in its own.
|
||||
app.run("ui.saveas", Value::Null).unwrap();
|
||||
assert_eq!(std::fs::read(picked()).unwrap(), b"plain");
|
||||
let _ = std::fs::remove_file(picked());
|
||||
|
||||
// Menu items, preferences (bad values ignored) and resetting the palettes.
|
||||
app.start("ui.saveversion.2004");
|
||||
assert_eq!(app.ui.save_version, "2004");
|
||||
let mut b = CadApp::new(Session::empty(), Services::default());
|
||||
b.load_prefs(&app.prefs_json());
|
||||
assert_eq!(b.ui.save_version, "2004");
|
||||
b.load_prefs(r#"{"saveVersion": "R14"}"#);
|
||||
assert_eq!(b.ui.save_version, "2004");
|
||||
b.start("ui.resetpalettes");
|
||||
assert_eq!(b.ui.save_version, "2004");
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ The server waits up to 60 s for the app to answer a request. After that it repli
|
|||
| `ui.click` / `ui.move` | `{x, y, button?, shift?}` | Real egui pointer events in screen points (reach every widget). |
|
||||
| `ui.drag` | `{x, y, to: [x, y], button?}` | Real press, move and release (drag the command line's top edge, the ViewCube ring…). |
|
||||
| `ui.key` / `ui.text` | `{key, cmd?, shift?, alt?}` / `{text}` | Synthetic keyboard input. |
|
||||
| `ui.set` | UiState fields | Show/hide palettes, toolbars, command line, ViewCube…; `theme: "system"\|"light"\|"dark"` (also `engine.execute` with `ui.theme {theme}`); `ui.inspect` reports the theme shown (`theme`: light/dark). `saveFormat: "dxf"\|"dwg"` (also `ui.saveformat {format}`) is the format Save As suggests for new drawings. |
|
||||
| `ui.set` | UiState fields | Show/hide palettes, toolbars, command line, ViewCube…; `theme: "system"\|"light"\|"dark"` (also `engine.execute` with `ui.theme {theme}`); `ui.inspect` reports the theme shown (`theme`: light/dark). `saveFormat: "dxf"\|"dwg"` (also `ui.saveformat {format}`) is the format Save As suggests for new drawings; `saveVersion: "2000"`…`"2018"` (also `ui.saveversion {version}`) the version it saves them in (SAVEAS also takes `version` and `binary` directly). |
|
||||
| `ui.resize` | `{width, height}` | Resize the window. |
|
||||
| `ui.focus` | | Activate the native window. Does not assign focus to a widget. |
|
||||
| `ui.screenshot` | `{path?}` | PNG of the window (needs a presented frame). |
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (WIPEOUT and hatch gradients now written, #147, #162) · **Target:** Autodesk AutoCAD 2027
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (DXF/DWG saved at a chosen version 2000–2018, binary DXF written, #421) · **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
|
||||
[target-app-parity.md](target-app-parity.md).
|
||||
|
||||
**Headline (estimated):** file formats ≈ **30%** of AutoCAD's. Opening the commonest 2D DXF/DWG
|
||||
drawings works, but saving is lossy: everything CADCraft doesn't model is dropped, output is
|
||||
always R2000, and no test uses a real third-party file. Remaining: **≈ 170–280 Opus 5.5 hours**:
|
||||
drawings works, but saving is lossy: everything CADCraft doesn't model is dropped, and no test
|
||||
uses a real third-party file. DXF and DWG save at 2000–2018 (2018 by default, #421). Remaining: **≈ 170–280 Opus 5.5 hours**:
|
||||
≈ 40–60 h for DXF/DWG fidelity (the part that decides beta), ≈ 110–180 h for the other formats
|
||||
below, and ≈ 20–40 h for 3D exchange formats that wait for 3D modelling.
|
||||
|
||||
|
|
@ -31,20 +31,23 @@ below, and ≈ 20–40 h for 3D exchange formats that wait for 3D modelling.
|
|||
|
||||
AutoCAD 2027 opens every DWG version from R2.0 onward and saves 2018 (AC1032, still the current
|
||||
DWG format), 2013, 2010, 2007, 2004, 2000 and R14. CADCraft reads DWG by converting it to DXF
|
||||
with the `acadrust` 0.6.1 crate and reading that DXF; it saves by writing its own R2000 DXF and
|
||||
converting it to DWG.
|
||||
with the `acadrust` 0.6.3 crate and reading that DXF; it saves by writing its own R2000 DXF and
|
||||
converting it to a DWG of the chosen version (#421): SAVEAS `version`, else the drawing's own
|
||||
version (the file's `$ACADVER`, 2018 for a new drawing), at least 2004 unless 2000 is asked for.
|
||||
The bridge repairs two acadrust 0.6.3 gaps in 2010+ DWG: merged table cells and table-style
|
||||
title/header suppression.
|
||||
|
||||
| DWG version | Code | AutoCAD 2027 | CADCraft read | CADCraft write | Tested how |
|
||||
|---|---|---|---|---|---|
|
||||
| R1.0–R2.6, R9, R10, R11/R12 | MC0.0–AC1009 | read | **no** (`is_dwg` accepts only `AC10xx` magic; acadrust doesn't target pre-R13) | no | — |
|
||||
| R13 | AC1012 | read | acadrust claims support; untested | no | none |
|
||||
| R14 | AC1014 | read, write | acadrust claims support; untested | **no** | none |
|
||||
| 2000–2002 | AC1015 | read, write | yes via bridge | **yes, always this version** | synthetic round trips (`dwg_roundtrip_keeps_non_ascii_text`, `dwg_roundtrip_keeps_extension_data`) |
|
||||
| 2004–2006 | AC1018 | read, write | via bridge; untested | no (version list in `crates/dwg` is unused) | none |
|
||||
| 2007–2009 | AC1021 | read, write | via bridge; untested | no | none |
|
||||
| 2010–2012 | AC1024 | read, write | via bridge | no | one synthetic file (`ac1024_dwg_with_many_entities_opens`, made by rewriting `$ACADVER`) |
|
||||
| 2013–2017 | AC1027 | read, write | via bridge; untested | no | none |
|
||||
| 2018–2027 | AC1032 | read, write (default) | via bridge; untested on real files | no | none |
|
||||
| 2000–2002 | AC1015 | read, write | yes via bridge | yes, when asked for (no true colours, transparency or table styles: the format has none) | synthetic round trips (`dwg_at_every_version_reads_back`, `dwg_roundtrip_keeps_non_ascii_text`) |
|
||||
| 2004–2006 | AC1018 | read, write | via bridge | yes | `dwg_at_every_version_reads_back`, `dwg_roundtrip_keeps_extension_data` |
|
||||
| 2007–2009 | AC1021 | read, write | via bridge | yes | `dwg_at_every_version_reads_back` |
|
||||
| 2010–2012 | AC1024 | read, write | via bridge | yes | `dwg_at_every_version_reads_back`, `ac1024_dwg_with_many_entities_opens` |
|
||||
| 2013–2017 | AC1027 | read, write | via bridge | yes | `dwg_at_every_version_reads_back` |
|
||||
| 2018–2027 | AC1032 | read, write (default) | via bridge; untested on real files | yes (default for new drawings) | `dwg_at_every_version_reads_back` (synthetic only) |
|
||||
| Web build | — | — | **no** (error: "save as DXF") | no | — |
|
||||
|
||||
Everything lost in the DXF read/write paths below is also lost through DWG, plus whatever
|
||||
|
|
@ -58,8 +61,8 @@ early (#175), but the throughput problem remains. **DWG ≈ 30% read, ≈ 15% wr
|
|||
|---|---|---|---|
|
||||
| R12 (AC1009), ASCII | read, write | yes: no version gating; old-style POLYLINE/VERTEX read (polyface meshes flattened to 3D polylines) | **no** |
|
||||
| R12 binary | read, write | **no** (binary parser expects R13+ 2-byte group codes) | no |
|
||||
| R13–R2018, ASCII | read, write each of 2000/2004/2007/2010/2013/2018 | yes, one reader for all versions | **R2000 (AC1015) only** |
|
||||
| R13–R2018, binary | read, write | yes | **no** (ASCII only) |
|
||||
| R13–R2018, ASCII | read, write each of 2000/2004/2007/2010/2013/2018 | yes, one reader for all versions | yes, each of 2000–2018 (#421): R2000 structure (later versions add only optional groups) with that `$ACADVER`; UTF-8 text from 2007; no CLASS instance counts (91) |
|
||||
| R13–R2018, binary | read, write | yes | yes, each of 2000–2018 (SAVEAS `binary`; a binary file stays binary on QSAVE) |
|
||||
| Code pages (`$DWGCODEPAGE`, ANSI_932/936/949/950/125x) | yes | **no**: UTF-8, else Latin-1, so pre-2007 CJK and Cyrillic files are garbled | writes `\U+` escapes (#169), which is correct |
|
||||
| `\U+`/`\M+` escapes | yes | `\U+` yes; `\M+` not checked | `\U+` yes |
|
||||
|
||||
|
|
@ -127,5 +130,6 @@ our output in AutoCAD black-box.
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | DXF (ASCII and binary) and DWG saved at 2000–2018, 2018 by default (#421) |
|
||||
| 2026-10-10 | minor | WIPEOUT and gradients written since #147/#162; other save losses remain (#201, #204, #208, #209, #213, #223) |
|
||||
| 2026-10-10 | major | First file-format parity doc: DWG and DXF version by version, per-object read/write, other formats |
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ do them first.
|
|||
| 1 | **[alpha blocker]** **Saving drops whatever we don't model** (Unknown entities, IMAGE, layer states, block descriptions, true colours, MULTILEADER written as LEADER+MTEXT, groups, named views/UCS, foreign xdata) | Opening a colleague's DWG and saving it silently deletes content: the one thing a CAD user can't forgive | 15–25 | [file formats](file-format-parity.md) |
|
||||
| 2 | **[alpha blocker]** **No real-file test corpus and no check in AutoCAD** | All 36 file tests are synthetic; nobody has verified a CADCraft DWG/DXF opens cleanly in AutoCAD | 10–15 + human | [file formats](file-format-parity.md#tests) |
|
||||
| 3 | **Tracking leftovers** (object snap tracking, Extension, Parallel and Apparent Intersection landed, #379): polar angles relative to the last segment, PolarSnap, temporary tracking point | Small now; ranks lower at the next review | 3–5 | [ui](ui-parity.md#tracking-polar-ortho-grid-and-snap--70) |
|
||||
| 4 | **[alpha blocker]** **DWG always saves as R2000; DXF only R2000 ASCII; no code pages** | Can't exchange at 2018 format; pre-2007 CJK/Cyrillic files are garbled | 10–15 | [file formats](file-format-parity.md) |
|
||||
| 4 | **No code pages when reading pre-2007 files** (saving DWG/DXF at 2000–2018 and binary DXF landed, #421) | Pre-2007 CJK/Cyrillic files are garbled | 3–5 | [file formats](file-format-parity.md) |
|
||||
| 5 | **[alpha blocker]** **Stubbed prompt options** left in PEDIT Edit vertex, SPLINEDIT and MTEXT columns | Scripts and muscle memory hit "not available yet" (the most-used commands are done) | 3–6 | [features](#features) |
|
||||
| 6 | **Ellipse and spline geometry is tessellated** (intersections, offsets, closest points; polyline offset ignores bulges) | Coordinates are subtly wrong; offsets of ellipses come out as polylines | 30–50 | [geometry](geometry-parity.md) |
|
||||
| 7 | **[alpha blocker]** **No xrefs, block editor, dynamic blocks or groups** | Most production drawings use them | 80–120 | [features](#features) |
|
||||
|
|
@ -82,7 +82,7 @@ Detail: [file-format-parity.md](file-format-parity.md).
|
|||
|---|---|---|---|
|
||||
| FF1 | **[alpha blocker]** Round-trip what we don't model: keep Unknown entities and objects, foreign xdata, extension dictionaries; write IMAGE (with IMAGEDEF), VIEW/UCS/VPORT tables, groups, layer states, block descriptions, true colours (#201, #204, #208, #209, #213, #223); WIPEOUT and gradients done (#147, #162) | High | 12–20 |
|
||||
| FF2 | **[alpha blocker]** Native MULTILEADER objects (read and write) | High | 6–10 |
|
||||
| FF3 | **[alpha blocker]** Write DXF 2004–2018 and binary DXF; save DWG at a chosen version (2018 default) instead of always R2000; SAVEAS version choice (PR #198 adds DWG to the dialog) | High | 8–12 |
|
||||
| FF3 | Save-version leftovers (DXF/DWG 2000–2018, binary DXF, SAVEAS `version`/`binary` and the default save version landed, #421): no R14 DWG or R12 DXF output; the Save dialog can't pick the version per file (the file picker doesn't report the chosen type; File > Default Save Format sets it); no command-line SAVEAS format prompt | Low | 2–4 |
|
||||
| FF4 | Code pages (`$DWGCODEPAGE`, ANSI_932/936/949/950/125x) when reading pre-2007 files | Medium-high (Asia, Eastern Europe) | 3–5 |
|
||||
| FF5 | **[alpha blocker]** Real-file corpus (`storytold/cadcraft-corpus`), open → save → reopen tests, black-box check in AutoCAD | High | 10–15 + human |
|
||||
| FF6 | **[alpha blocker]** DWG throughput: a 10 MB DWG took ~2 min to fail (#57) | High for real projects | 6–10 |
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ the alpha checklist. Main platform: macOS.
|
|||
| Dimension and annotate | partial, not blocking | All core DIM types, associativity, DIMSTYLE variables (43 of ~80), TEXT/MTEXT, MLEADER, tables work; no DIMSTYLE dialog, MTEXT editor not in place, MLEADER thin (F7, F9, F10). A drafter can dimension a sheet end to end | 0 for the gate (beta work) |
|
||||
| Use blocks (insert library blocks, edit them, attributes, xrefs) | **partial, blocking** | Static BLOCK/INSERT/attributes work; no block editor (BEDIT) or REFEDIT, so a block can't be edited in place; no xrefs; dynamic blocks from other drawings lose their behaviour (F2, F3) | 35–55 |
|
||||
| Layers, layouts and plotting a sheet | **partial, blocking** | Layers, layouts, viewports, page setups, PDF output and plot styles (CTB/STB, #410) work; no plot preview or plot dialog (#160); printers/plotters only via PDF (F14, H3) | 15–25 |
|
||||
| Save DWG that AutoCAD opens without loss, and reopen others' DWGs | **no** | Saving drops every entity and object we don't model (Unknown, IMAGE, WIPEOUT, gradients, groups, views, foreign xdata); MULTILEADER becomes LEADER+MTEXT; DWG always written as R2000; never checked in AutoCAD; no real-file tests; a 10 MB DWG failed after ~2 min (#57) (FF1–FF6) | 40–60 + owner (real-file corpus, AutoCAD check) |
|
||||
| Save DWG that AutoCAD opens without loss, and reopen others' DWGs | **no** | Saving drops every entity and object we don't model (Unknown, IMAGE, WIPEOUT, gradients, groups, views, foreign xdata); MULTILEADER becomes LEADER+MTEXT; never checked in AutoCAD; no real-file tests; a 10 MB DWG failed after ~2 min (#57) (FF1–FF6) | 40–60 + owner (real-file corpus, AutoCAD check) |
|
||||
| **To alpha** | | | **≈ 125–190** |
|
||||
|
||||
Ready for real work is ≈ 38%, also under the ~40% alpha bar; closing the gate rows lifts it past
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ area here; it is counted once, as the file-exchange discount.
|
|||
|---|---:|---|
|
||||
| Interaction fidelity (beyond the precision-input area) | ×0.90 | hot-grip options ignored, no context menus, selection keywords W/C/F/WP/CP rejected (#224), transparent 'ZOOM cancels the running command (#220), ELLIPSE/BREAK keywords ignored (#219) |
|
||||
| Stability on real machines | ×0.85 | 27 of 37 open issues are core-path bugs; about 60 correctness fixes merged on 2026-10-10 alone; lines not visible mid-command (#44); locked layers edited (#87); no CI runs the Rust tests on PRs |
|
||||
| Exchanging files with AutoCAD users | ×0.80 | saves drop unmodelled entities, images, layer states, multileader styles, true colours and more (#79, #201, #204, #208, #209, #213, #223); DWG always R2000; never checked in AutoCAD; a 10 MB DWG failed (#57) |
|
||||
| Exchanging files with AutoCAD users | ×0.80 | saves drop unmodelled entities, images, layer states, multileader styles, true colours and more (#79, #201, #204, #208, #209, #213, #223); never checked in AutoCAD; a 10 MB DWG failed (#57) |
|
||||
|
||||
**Mainstream practitioner ≈ 45.3% × 0.90 × 0.85 × 0.80 ≈ 28%** (estimated). It is lower than
|
||||
ready for real work (≈ 38%) for CADCraft, unlike most apps: the full number credits additively
|
||||
|
|
|
|||
Loading…
Reference in New Issue