mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-22 10:14:01 -04:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4214f7180 | ||
|
|
35934718ea | ||
|
|
916d440bed | ||
|
|
632967aa78 | ||
|
|
f15be2305c | ||
|
|
f2dadec23a | ||
|
|
00acec952e | ||
|
|
78254b4d3f | ||
|
|
8094d7746f | ||
|
|
4ad4f4f598 | ||
|
|
477eec5026 | ||
|
|
38750c6eb8 | ||
|
|
75d9d72703 | ||
|
|
82424e47c9 | ||
|
|
4d826ed91f | ||
|
|
a7bb28d722 | ||
|
|
27bff9cc30 | ||
|
|
d600a80354 | ||
|
|
81f0aa53ff | ||
|
|
27861ad0ed | ||
|
|
b72680cfb7 | ||
|
|
d9adce038c | ||
|
|
4c75b0c458 | ||
|
|
8a2b8f4bdf |
No files matched your search
@@ -7,6 +7,10 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/app-icon/hicolor/256x256/apps/ai.storyteller.cadcraft.png" width="128" height="128" alt="CADCraft icon: a drafting set square and compass arc on a blue grid tile">
|
||||
</p>
|
||||
|
||||
<h1 align="center">CADCraft</h1>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -61,6 +61,16 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
}
|
||||
}
|
||||
|
||||
/// How long the accepting thread waits after a failed accept. Accepting can keep failing, for
|
||||
/// example while the process has no file descriptors left, and retrying at once would keep a
|
||||
/// core busy until it recovers.
|
||||
const ACCEPT_RETRY: Duration = Duration::from_millis(100);
|
||||
|
||||
/// The connections `incoming` accepts, calling `wait` after each failed accept.
|
||||
fn accepted<T>(incoming: impl Iterator<Item = std::io::Result<T>>, mut wait: impl FnMut()) -> impl Iterator<Item = T> {
|
||||
incoming.filter_map(move |stream| stream.map_err(|_| wait()).ok())
|
||||
}
|
||||
|
||||
/// Accept connections on `listener` under `limits`; requests arrive on the returned receiver.
|
||||
fn serve_listener(listener: TcpListener, limits: Limits, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
let (tx, rx) = sync_channel::<ControlRequest>(limits.queue.max(1));
|
||||
@@ -80,7 +90,7 @@ impl Drop for Slot {
|
||||
|
||||
fn accept_loop(listener: TcpListener, tx: SyncSender<ControlRequest>, ctx: egui::Context, limits: Limits) {
|
||||
let open = Arc::new(AtomicUsize::new(0));
|
||||
for mut stream in listener.incoming().flatten() {
|
||||
for mut stream in accepted(listener.incoming(), || std::thread::sleep(ACCEPT_RETRY)) {
|
||||
let _ = stream.set_write_timeout(Some(limits.write_timeout));
|
||||
if open.fetch_add(1, Ordering::SeqCst) >= limits.max_clients {
|
||||
open.fetch_sub(1, Ordering::SeqCst);
|
||||
@@ -222,6 +232,18 @@ fn serve(stream: TcpStream, tx: &SyncSender<ControlRequest>, ctx: &egui::Context
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A failed accept is followed by a wait before the next one, so a listener that keeps
|
||||
/// failing does not spin, and the connections around the failures still come through.
|
||||
#[test]
|
||||
fn a_failed_accept_waits_before_the_next() {
|
||||
let failed = || Err(std::io::Error::other("no file descriptors left"));
|
||||
let incoming = vec![failed(), Ok(1), failed(), failed(), Ok(2)];
|
||||
let mut waits = 0;
|
||||
let connections: Vec<i32> = super::accepted(incoming.into_iter(), || waits += 1).collect();
|
||||
assert_eq!(connections, [1, 2]);
|
||||
assert_eq!(waits, 3, "one wait per failed accept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_are_read_up_to_the_limit() {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
@@ -752,6 +752,8 @@ pub struct PageSetup {
|
||||
pub landscape: bool,
|
||||
/// "layout" | "extents" | "display" | "window" | "limits".
|
||||
pub plot_area: String,
|
||||
/// Plot window corners `[x1, y1, x2, y2]` (DXF plot layout groups 48, 49, 140, 141); used when `plot_area` is "window".
|
||||
pub plot_window: [f64; 4],
|
||||
pub scale_to_fit: bool,
|
||||
/// Paper units per drawing unit.
|
||||
pub scale: f64,
|
||||
@@ -779,6 +781,7 @@ impl Default for PageSetup {
|
||||
margins_mm: [6.35, 6.35, 6.35, 6.35],
|
||||
landscape: true,
|
||||
plot_area: "layout".into(),
|
||||
plot_window: [0.0; 4],
|
||||
scale_to_fit: false,
|
||||
scale: 1.0,
|
||||
plot_style_table: String::new(),
|
||||
|
||||
@@ -494,6 +494,16 @@ mod native {
|
||||
}
|
||||
}
|
||||
|
||||
/// The current text style (`$TEXTSTYLE`) by its name, the same way as [`current_dimstyle`]:
|
||||
/// the handle read with the DXF needn't be the style's in the document, in which case the DWG
|
||||
/// writer fell back to Standard and AutoCAD opened the drawing with Standard current (#1650).
|
||||
fn current_text_style(doc: &mut acadrust::CadDocument) {
|
||||
let name = doc.header.current_text_style_name.clone();
|
||||
if let Some(h) = doc.text_styles.iter().find(|ts| ts.name.eq_ignore_ascii_case(&name)).map(|ts| ts.handle) {
|
||||
doc.header.current_text_style_handle = h;
|
||||
}
|
||||
}
|
||||
|
||||
fn convert(dxf: &[u8], version: Option<acadrust::DxfVersion>) -> Result<Vec<u8>, String> {
|
||||
let data = dxf.to_vec();
|
||||
let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read()))
|
||||
@@ -512,6 +522,7 @@ mod native {
|
||||
dimension_flags(&mut doc, true);
|
||||
drop_unwritable(&mut doc);
|
||||
current_dimstyle(&mut doc);
|
||||
current_text_style(&mut doc);
|
||||
std::panic::catch_unwind(AssertUnwindSafe(|| acadrust::DwgWriter::write_to_vec(&doc)))
|
||||
.map_err(|_| "the DWG writer failed on this drawing".to_string())?
|
||||
.map_err(|e| format!("DWG write: {e}"))
|
||||
@@ -585,6 +596,22 @@ mod tests {
|
||||
assert_eq!(version(b"AC1018xxxx").as_deref(), Some("AC1018"));
|
||||
}
|
||||
|
||||
/// A DXF with ROMANS as `$TEXTSTYLE` gives a DWG whose current text style is ROMANS, not
|
||||
/// Standard: the DXF names the style, and the handle read with it was Standard's (#1650).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn current_text_style_reaches_the_dwg() {
|
||||
let header = " 0\nSECTION\n 2\nHEADER\n 9\n$ACADVER\n 1\nAC1032\n 9\n$TEXTSTYLE\n 7\nROMANS\n 0\nENDSEC\n";
|
||||
let style = " 0\nSTYLE\n 5\n1F0\n100\nAcDbSymbolTableRecord\n100\nAcDbTextStyleTableRecord\n 2\nROMANS\n 70\n0\n";
|
||||
let style = format!("{style} 40\n0\n 41\n1\n 50\n0\n 71\n0\n 42\n2.5\n 3\nromans.shx\n 4\n\n");
|
||||
let dxf = format!("{header} 0\nSECTION\n 2\nTABLES\n 0\nTABLE\n 2\nSTYLE\n 70\n1\n{style} 0\nENDTAB\n 0\nENDSEC\n 0\nEOF\n");
|
||||
let dwg = dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let doc = acadrust::DwgReader::from_stream(std::io::Cursor::new(&dwg)).read().unwrap();
|
||||
let romans = doc.text_styles.iter().find(|ts| ts.name == "ROMANS").map(|ts| ts.handle);
|
||||
assert!(romans.is_some(), "ROMANS is missing from the DWG");
|
||||
assert_eq!(Some(doc.header.current_text_style_handle), romans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_is_an_error_not_a_crash() {
|
||||
assert!(dwg_to_dxf(b"AC1032 this is not a dwg file at all").is_err());
|
||||
|
||||
@@ -427,13 +427,13 @@ fn mleader_kind(s: &Session, pts: &[Vec2], text: &str, landing: bool) -> Option<
|
||||
let d = s.doc().ok()?;
|
||||
let cur = d.header.str("CMLEADERSTYLE", "Standard");
|
||||
let st = d.mleader_styles.iter().find(|m| m.name.eq_ignore_ascii_case(&cur)).or(d.mleader_styles.first()).cloned().unwrap_or_default();
|
||||
// An annotative style's sizes are paper sizes: the annotation scale applies when drawn.
|
||||
let k = if st.annotative { 1.0 } else { d.header.f64("DIMSCALE", 1.0) };
|
||||
let th = st.text_height * k;
|
||||
// The style's sizes are the drawn sizes: a multileader style has no scale of its own and
|
||||
// DIMSCALE (a dimension-style setting) does not apply, as in AutoCAD.
|
||||
let th = st.text_height;
|
||||
let dir = if arrow_pts.first().is_some_and(|a| a.x > landing_pt.x) { -1.0 } else { 1.0 };
|
||||
let attach = if dir > 0.0 { 4 } else { 6 };
|
||||
let dogleg = if landing { st.dogleg * k } else { 0.0 };
|
||||
let tpos = *landing_pt + Vec2::new((dogleg + st.landing_gap * k) * dir, 0.0);
|
||||
let dogleg = if landing { st.dogleg } else { 0.0 };
|
||||
let tpos = *landing_pt + Vec2::new((dogleg + st.landing_gap) * dir, 0.0);
|
||||
Some(EntityKind::MLeader(MLeader {
|
||||
leaders: vec![arrow_pts.iter().map(|p| v3(*p)).collect()],
|
||||
landing: v3(*landing_pt),
|
||||
@@ -451,7 +451,7 @@ fn mleader_kind(s: &Session, pts: &[Vec2], text: &str, landing: bool) -> Option<
|
||||
columns: None,
|
||||
}),
|
||||
style: st.name.clone(),
|
||||
arrow_size: st.arrow_size * k,
|
||||
arrow_size: st.arrow_size,
|
||||
spline: false,
|
||||
block: None,
|
||||
}))
|
||||
|
||||
@@ -307,11 +307,32 @@ fn chain(a: &[Value], ok: fn(std::cmp::Ordering) -> bool) -> R {
|
||||
}
|
||||
Ok(Value::t())
|
||||
}
|
||||
/// `=` and `/=`: numbers and strings compare as [`compare`] does, and any other atom compares by
|
||||
/// what it is, as `eq` does. AutoLISP answers `(= (type x) 'LIST)` -- the idiom for a type test,
|
||||
/// since `type` returns a symbol -- where coercing that symbol to a number failed the whole
|
||||
/// routine with "bad argument type" instead (#1597). A mismatch is `nil`, not an error, so
|
||||
/// `(= 1 "1")` and `(= '(1) '(1))` answer too. The ordering comparisons stay numbers and strings
|
||||
/// only, as AutoLISP has them.
|
||||
fn eq_chain(a: &[Value], want_equal: bool) -> R {
|
||||
arity(a, 1, usize::MAX)?;
|
||||
for w in a.windows(2) {
|
||||
let [x, y] = w else { continue };
|
||||
let same = match (x, y) {
|
||||
(Value::Str(_), Value::Str(_)) => compare(x, y)?.is_eq(),
|
||||
_ if x.as_number().is_some() && y.as_number().is_some() => compare(x, y)?.is_eq(),
|
||||
_ => eq(x, y),
|
||||
};
|
||||
if same != want_equal {
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
}
|
||||
Ok(Value::t())
|
||||
}
|
||||
fn num_eq(a: &[Value]) -> R {
|
||||
chain(a, |o| o.is_eq())
|
||||
eq_chain(a, true)
|
||||
}
|
||||
fn num_ne(a: &[Value]) -> R {
|
||||
chain(a, |o| o.is_ne())
|
||||
eq_chain(a, false)
|
||||
}
|
||||
fn lt(a: &[Value]) -> R {
|
||||
chain(a, |o| o.is_lt())
|
||||
|
||||
@@ -67,6 +67,12 @@ impl GetRequest {
|
||||
let mut p = Prompt::new(message, accept).base_opt(base);
|
||||
p.keywords = self.initget.keywords.clone();
|
||||
p.raw = true;
|
||||
// `entsel`/`nentsel` pick one object. The routine writes the message, so the prompt says
|
||||
// so itself rather than being read for the word "Select" -- with a message of its own
|
||||
// ("\nSelect the second line: ", or no mention of selecting at all) the pick box went
|
||||
// missing, while the same call without one got it from the default "Select object: "
|
||||
// (#1598).
|
||||
p.picks_one = matches!(self.kind, GetKind::EntSel);
|
||||
p
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,39 @@ fn arithmetic_follows_autolisp() {
|
||||
assert_eq!(eval(&mut s, "(+ 1 nil)"), Err("bad argument type: numberp: nil".into()));
|
||||
}
|
||||
|
||||
/// `=` answers for the symbols `type` returns, which is how AutoLISP routines test a type:
|
||||
/// `(= (type x) 'INT)`. Coercing the symbol to a number failed the whole routine with "bad
|
||||
/// argument type" instead, so a routine written for AutoLISP stopped at its first type test
|
||||
/// (#1597). The ordering comparisons are still numbers and strings, as AutoLISP has them.
|
||||
#[test]
|
||||
fn equality_compares_symbols_as_autolisp_does() {
|
||||
let mut s = session();
|
||||
assert_eq!(ok(&mut s, "(= (type 1) 'INT)"), "T");
|
||||
assert_eq!(ok(&mut s, "(= (type 1.0) 'INT)"), "nil");
|
||||
assert_eq!(ok(&mut s, "(= (type '(1 2)) 'LIST)"), "T");
|
||||
assert_eq!(ok(&mut s, "(= (type \"a\") 'STR)"), "T");
|
||||
assert_eq!(ok(&mut s, "(/= (type 1) 'REAL)"), "T");
|
||||
assert_eq!(ok(&mut s, "(= 'a 'a 'a)"), "T");
|
||||
assert_eq!(ok(&mut s, "(= 'a 'b)"), "nil");
|
||||
|
||||
// The routine from the report runs.
|
||||
ok(&mut s, "(defun isint (a) (if (= (type a) 'INT) T nil))");
|
||||
assert_eq!(ok(&mut s, "(isint 3)"), "T");
|
||||
assert_eq!(ok(&mut s, "(isint 3.0)"), "nil");
|
||||
assert_eq!(ok(&mut s, "(isint \"3\")"), "nil");
|
||||
|
||||
// A mismatch of kinds is nil, not an error; numbers and strings are unchanged.
|
||||
assert_eq!(ok(&mut s, "(= 1 \"1\")"), "nil");
|
||||
assert_eq!(ok(&mut s, "(= '(1) '(1))"), "nil", "`=` is not `equal`");
|
||||
assert_eq!(ok(&mut s, "(= nil nil)"), "T");
|
||||
assert_eq!(ok(&mut s, "(= 1 1.0)"), "T");
|
||||
assert_eq!(ok(&mut s, "(= \"a\" \"a\" \"a\")"), "T");
|
||||
assert_eq!(ok(&mut s, "(= 1 1 2)"), "nil");
|
||||
|
||||
// Ordering still refuses what AutoLISP refuses.
|
||||
assert_eq!(eval(&mut s, "(< 'a 'b)"), Err("bad argument type: numberp: A".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_and_strings() {
|
||||
let mut s = session();
|
||||
@@ -452,6 +485,37 @@ fn hostile_lisp_never_panics() {
|
||||
assert!(!s.lisp_running());
|
||||
}
|
||||
|
||||
/// `entsel` and `nentsel` pick one object, so the cursor shows the pick box at their prompt. It
|
||||
/// was read off the message's first word, and a routine's own message begins with a newline -- so
|
||||
/// the same call showed the pick box without a message and lost it with one (#1598).
|
||||
#[test]
|
||||
fn an_entsel_prompt_picks_objects_whatever_its_message_says() {
|
||||
let mut s = session();
|
||||
// No message: the default "Select object: ".
|
||||
s.cmdline("(nentsel)").unwrap();
|
||||
assert!(s.current_prompt().unwrap().picks_objects(), "{}", s.prompt_text());
|
||||
s.cancel();
|
||||
|
||||
// The reported case: the routine's own message, starting with a newline.
|
||||
s.cmdline("(nentsel \"\nSelect second line or polyline: \")").unwrap();
|
||||
assert!(s.current_prompt().unwrap().picks_objects(), "{}", s.prompt_text());
|
||||
s.cancel();
|
||||
|
||||
// Wording that never says "select" picks an object just the same.
|
||||
s.cmdline("(entsel \"\nPick a line: \")").unwrap();
|
||||
assert!(s.current_prompt().unwrap().picks_objects(), "{}", s.prompt_text());
|
||||
s.cancel();
|
||||
|
||||
// A point prompt is not a pick, and keeps its point modifiers -- including one whose own
|
||||
// message happens to begin with the word the built-in commands are read by. `picks_one` says
|
||||
// which calls pick, so the wording is only ever read where the engine wrote it (#1635 review).
|
||||
for point in ["(getpoint \"\nSpecify a point: \")", "(getpoint \"\nSelect a point: \")", "(getpoint \"Select a point: \")"] {
|
||||
s.cmdline(point).unwrap();
|
||||
assert!(!s.current_prompt().unwrap().picks_objects(), "{point}: {}", s.prompt_text());
|
||||
s.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_waiting_routine_is_a_prompt_with_a_rubber_band() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -62,6 +62,10 @@ pub struct Prompt {
|
||||
/// pointer boxes.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub distance: bool,
|
||||
/// The prompt picks one object whatever it says: an AutoLISP `entsel`/`nentsel`, whose message
|
||||
/// the routine writes, so it cannot be read for the word "Select" (#1598).
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub picks_one: bool,
|
||||
}
|
||||
|
||||
/// Parse `@distance<angle` (relative polar), returning `(distance, angle in radians)`.
|
||||
@@ -128,11 +132,14 @@ impl Prompt {
|
||||
self.default = Some(d.into());
|
||||
self
|
||||
}
|
||||
/// The prompt picks objects: a "Select objects" prompt, or a point prompt whose click selects
|
||||
/// one object ("Select object to offset", "Select first line"…, the commands that pick a
|
||||
/// single object themselves). The UI shows the pick box there.
|
||||
/// The prompt picks objects: a "Select objects" prompt, one that says it picks a single object
|
||||
/// ([`Prompt::picks_one`]), or a point prompt whose click selects one ("Select object to
|
||||
/// offset", "Select first line"…, the commands that pick a single object themselves). The UI
|
||||
/// shows the pick box there. The wording is only read where the engine wrote it: a routine
|
||||
/// writes its own message (`raw`), which may open with "Select" and still ask for a point,
|
||||
/// and says through `picks_one` when it picks instead (#1635 review).
|
||||
pub fn picks_objects(&self) -> bool {
|
||||
self.accept.select || (self.accept.point && self.message.starts_with("Select"))
|
||||
self.picks_one || self.accept.select || (self.accept.point && !self.raw && self.message.starts_with("Select"))
|
||||
}
|
||||
/// "Specify next point or [Undo/Close] <default>:".
|
||||
pub fn display(&self) -> String {
|
||||
|
||||
@@ -121,3 +121,20 @@ fn json_form_takes_leader_type_and_block() {
|
||||
assert!(s.execute("mleader", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// DIMSCALE belongs to dimension styles: a multileader keeps its style's text height, arrowhead
|
||||
/// and dogleg whatever DIMSCALE is (#1648).
|
||||
#[test]
|
||||
fn mleader_sizes_ignore_dimscale() {
|
||||
let mut s = Session::new();
|
||||
s.execute("mleaderstyle", &json!({"name": "M5", "textHeight": 5, "arrowSize": 5, "current": true})).unwrap();
|
||||
s.execute("setvar", &json!({"name": "DIMSCALE", "value": 20})).unwrap();
|
||||
s.execute("mleader", &json!({"points": [[0, 0], [30, 20]], "text": "CADCRAFT"})).unwrap();
|
||||
let m = last_mleader(&s);
|
||||
assert_eq!(m.arrow_size, 5.0);
|
||||
let st = s.doc().unwrap().mleader_styles.iter().find(|x| x.name == "M5").unwrap().clone();
|
||||
assert_eq!(m.dogleg, st.dogleg);
|
||||
let t = m.text.unwrap();
|
||||
assert_eq!(t.height, 5.0);
|
||||
assert!((t.insert.x - (30.0 + st.dogleg + st.landing_gap)).abs() < 1e-9, "{}", t.insert.x);
|
||||
}
|
||||
@@ -437,6 +437,10 @@ pub fn bulge_to_arc(a: Vec2, b: Vec2, bulge: f64) -> Option<(Arc, bool)> {
|
||||
return None;
|
||||
}
|
||||
let theta = 4.0 * bulge.atan(); // signed included angle
|
||||
// A sweep at or below EPS would be read back as a full circle by `ccw_sweep`; it is a straight segment.
|
||||
if theta.abs() <= EPS {
|
||||
return None;
|
||||
}
|
||||
let chord = a.dist(b);
|
||||
let r = chord / (2.0 * (theta / 2.0).sin().abs());
|
||||
let mid = a.mid(b);
|
||||
|
||||
@@ -74,6 +74,16 @@ fn bulge_quarter_arc() {
|
||||
assert!(close(arc.sweep(), PI / 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_bulge_is_not_a_full_circle() {
|
||||
let a = Vec2::new(0.0, 0.0);
|
||||
let b = Vec2::new(10.0, 0.0);
|
||||
let p = Polyline { vertices: vec![PolyVertex::with_bulge(a, 1e-10), PolyVertex::new(b)], closed: false };
|
||||
assert!((p.len() - 10.0).abs() < 1e-6, "len={}", p.len());
|
||||
let bb = p.bounds();
|
||||
assert!(bb.max.x < 10.001 && bb.max.y < 0.001 && bb.min.y > -0.001, "bounds={bb:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polyline_area_and_length() {
|
||||
let p = Polyline::from_points(&[Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0), Vec2::new(4.0, 3.0), Vec2::new(0.0, 3.0)], true);
|
||||
|
||||
@@ -51,8 +51,6 @@ const MAX_OBJECTS: usize = 1_000_000;
|
||||
const MAX_XDATA_GROUPS: usize = 16_384;
|
||||
/// Longest ownership chain followed.
|
||||
const MAX_DEPTH: usize = 64;
|
||||
/// Handles above this are not believed (hostile files); references to them are dropped.
|
||||
const MAX_HANDLE: u64 = 1 << 48;
|
||||
|
||||
/// Record types that structure a file: never written as an entity of another type.
|
||||
const STRUCTURAL: &[&str] = &["", "SECTION", "ENDSEC", "EOF", "TABLE", "ENDTAB", "BLOCK", "ENDBLK", "SEQEND", "VERTEX", "ATTRIB", "CLASS"];
|
||||
@@ -85,9 +83,10 @@ fn tag(r: &RawTag) -> Tag {
|
||||
Tag::new(r.code, value.unwrap_or_else(|| Value::Str(r.value.clone())))
|
||||
}
|
||||
|
||||
/// A handle value we believe: hexadecimal, not null, not absurdly large.
|
||||
/// A handle value we believe: hexadecimal and not null. DXF handles are up to 16 hex digits and
|
||||
/// AutoCAD writes some above 2^48 (e.g. 0xDAD1…), so the whole 64-bit range is believed.
|
||||
fn handle(s: &str) -> Option<Handle> {
|
||||
Handle::parse_hex(s).filter(|h| h.0 != 0 && h.0 <= MAX_HANDLE)
|
||||
Handle::parse_hex(s).filter(|h| h.0 != 0)
|
||||
}
|
||||
|
||||
/// Group codes whose values are handles of other objects (DXF Reference, group code ranges).
|
||||
|
||||
@@ -1425,7 +1425,7 @@ fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
|
||||
match kind.as_str() {
|
||||
"LAYER" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
if name.is_empty() {
|
||||
if name.is_empty() || d.layers.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
let aci = t.i(62).unwrap_or(7);
|
||||
@@ -1462,7 +1462,7 @@ fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
|
||||
}
|
||||
"LTYPE" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
if name.is_empty() || d.linetype(&name).is_some() {
|
||||
if name.is_empty() || d.linetype(&name).is_some() || d.linetypes.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
let pattern = ltype_pattern(&tg, <ype_styles);
|
||||
@@ -1470,7 +1470,7 @@ fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
|
||||
}
|
||||
"STYLE" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
if name.is_empty() {
|
||||
if name.is_empty() || d.text_styles.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
// 70 bit 4: vertical; 71 bits 2/4: backwards / upside down.
|
||||
@@ -1552,7 +1552,7 @@ fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
|
||||
}
|
||||
"DIMSTYLE" => {
|
||||
let name = t.s(2).unwrap_or_default();
|
||||
if name.is_empty() {
|
||||
if name.is_empty() || d.dim_styles.len() >= MAX_OBJECTS {
|
||||
continue;
|
||||
}
|
||||
// Every variable with a DIMSTYLE group code; values the style can't take are
|
||||
@@ -2248,6 +2248,7 @@ fn page_setup(t: &T) -> PageSetup {
|
||||
_ => "layout",
|
||||
}
|
||||
.into();
|
||||
p.plot_window = [t.fd(48, 0.0), t.fd(49, 0.0), t.fd(140, 0.0), t.fd(141, 0.0)].map(|v| if v.is_finite() { v } else { 0.0 });
|
||||
p.plot_style_table = t.s(7).unwrap_or_default();
|
||||
p
|
||||
}
|
||||
|
||||
@@ -1441,8 +1441,11 @@ fn table_size(t: &Table) -> (usize, usize) {
|
||||
|
||||
/// The anonymous `*T` block of a table: cell fills, borders and texts as its cell styles draw
|
||||
/// them, for readers that only draw the block.
|
||||
fn table_block_entities(d: &Drawing, t: &Table, layer: &str) -> Vec<Entity> {
|
||||
let g = cadcraft_render::table_geometry(d, t);
|
||||
///
|
||||
/// The block is relative to its own origin: the ACAD_TABLE reference adds the insertion point.
|
||||
pub(crate) fn table_block_entities(d: &Drawing, t: &Table, layer: &str) -> Vec<Entity> {
|
||||
let at_origin = Table { insert: Vec3::ZERO, ..t.clone() };
|
||||
let g = cadcraft_render::table_geometry(d, &at_origin);
|
||||
let c = Common { layer: layer.into(), color: Color::ByBlock, ..Common::default() };
|
||||
let mut out = Vec::new();
|
||||
for f in &g.fills {
|
||||
@@ -2565,10 +2568,10 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
|
||||
(45, page.height_mm),
|
||||
(46, 0.0),
|
||||
(47, 0.0),
|
||||
(48, 0.0),
|
||||
(49, 0.0),
|
||||
(140, 0.0),
|
||||
(141, 0.0),
|
||||
(48, page.plot_window[0]),
|
||||
(49, page.plot_window[1]),
|
||||
(140, page.plot_window[2]),
|
||||
(141, page.plot_window[3]),
|
||||
(142, scale),
|
||||
(143, 1.0),
|
||||
] {
|
||||
|
||||
@@ -1343,6 +1343,23 @@ fn tables_roundtrip_with_title_header_and_merges() {
|
||||
assert_eq!(again.blocks.len(), back.blocks.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_block_is_relative_to_the_insertion_point() {
|
||||
let d = Drawing::new_imperial();
|
||||
let t = table_sample();
|
||||
let ents = crate::dxf_write::table_block_entities(&d, &t, "0");
|
||||
let mut xs = Vec::new();
|
||||
for e in &ents {
|
||||
if let EntityKind::Line(l) = &e.kind {
|
||||
xs.extend([l.a.x, l.b.x]);
|
||||
}
|
||||
}
|
||||
assert!(!xs.is_empty());
|
||||
let (min, max) = xs.iter().fold((f64::MAX, f64::MIN), |(lo, hi), &x| (lo.min(x), hi.max(x)));
|
||||
// Columns total 8 wide; the block starts at its own origin, not at the insert x of 5.
|
||||
assert!(min.abs() < 1e-9 && (max - 8.0).abs() < 1e-9, "block x range {min}..{max}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_style_flags_roundtrip() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
|
||||
@@ -536,3 +536,29 @@ fn dragging_a_distance_grip_changes_the_distance() {
|
||||
assert!(drag_grip(&mut d, h("30"), 2, GripPart::End, Vec2::new(130.0, 0.0)).is_err(), "not a reference");
|
||||
assert_eq!(d, before);
|
||||
}
|
||||
|
||||
/// The DOOR drawing with every handle moved above 2^48: AutoCAD writes such handles (seen in
|
||||
/// drawings whose handles start at 0xDAD1…), and the dynamic block's objects must still be kept.
|
||||
fn far_handles(src: &str) -> String {
|
||||
const SHIFT: u64 = 0x000D_AD10_0000_0000;
|
||||
let is_handle = |code: i32| matches!(code, 5 | 105 | 320..=369 | 390..=399 | 480 | 481 | 1005);
|
||||
let mut out = String::new();
|
||||
for line in src.lines() {
|
||||
let (code, value) = line.trim().split_once(' ').unwrap_or((line.trim(), ""));
|
||||
let moved = match (code.parse::<i32>(), u64::from_str_radix(value, 16)) {
|
||||
(Ok(c), Ok(h)) if is_handle(c) && h != 0 => format!("{:X}", h + SHIFT),
|
||||
_ => value.to_string(),
|
||||
};
|
||||
out.push_str(&format!("{code} {moved}\n"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dynamic_block_with_handles_above_2_pow_48_is_kept() {
|
||||
let src = far_handles(&fixture());
|
||||
let d = read_dxf(dxf(&src).as_bytes()).unwrap();
|
||||
let before = dynamic_block(&d, "Door").expect("the dynamic block with far handles");
|
||||
let again = read_dxf(write_dxf(&d).as_bytes()).unwrap();
|
||||
assert_eq!(by_node(dynamic_block(&again, "Door").unwrap()), by_node(before));
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//! #1667: a layout whose page setup plots a window (DXF plot type 4) lost the window corners
|
||||
//! (groups 48, 49, 140, 141) on save: they were written as zero.
|
||||
|
||||
use cadcraft_doc::{Drawing, Layout, PageSetup};
|
||||
use cadcraft_io::{read, write_dxf};
|
||||
|
||||
#[test]
|
||||
fn plot_window_corners_round_trip_through_dxf() {
|
||||
let mut d = Drawing::new_metric();
|
||||
let mut l = Layout::new("WIN", 3);
|
||||
l.page = PageSetup {
|
||||
width_mm: 297.0,
|
||||
height_mm: 420.0,
|
||||
margins_mm: [0.0; 4],
|
||||
plot_area: "window".into(),
|
||||
plot_window: [0.0, -297.0, 420.0, 0.0],
|
||||
..PageSetup::default()
|
||||
};
|
||||
d.layouts.push(l);
|
||||
|
||||
let back = read(write_dxf(&d).as_bytes(), "win.dxf").unwrap();
|
||||
let page = &back.layouts.iter().find(|l| l.name == "WIN").unwrap().page;
|
||||
assert_eq!(page.plot_area, "window");
|
||||
assert_eq!(page.plot_window, [0.0, -297.0, 420.0, 0.0]);
|
||||
}
|
||||
Reference in new issue
Block a user