mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-15 02:15:21 -06:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54dfb2ee00 | ||
|
|
2a9fc8f241 | ||
|
|
ddfa833bc2 | ||
|
|
bf581c2376 |
@@ -23,43 +23,5 @@ jobs:
|
||||
uses: storytold/craft-common-actions/.github/workflows/nix.yml@main
|
||||
|
||||
git-sources:
|
||||
name: Git dependencies come from storytold
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
|
||||
# Source replacement, patches and path overrides in cargo config never show up
|
||||
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
|
||||
# Cargo still reads the legacy .cargo/config.
|
||||
python3 - <<'PY'
|
||||
import os, sys, tomllib
|
||||
for path in (".cargo/config", ".cargo/config.toml"):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
config = tomllib.load(open(path, "rb"))
|
||||
replaced = [name for name, source in config.get("source", {}).items()
|
||||
if isinstance(source, dict) and "replace-with" in source]
|
||||
if replaced or "patch" in config or "paths" in config:
|
||||
print(f"{path} replaces, patches or overrides a source")
|
||||
sys.exit(1)
|
||||
PY
|
||||
# Fails when Cargo.lock is out of date with the manifests.
|
||||
cargo metadata --locked --format-version 1 > /dev/null
|
||||
python3 - <<'PY'
|
||||
import sys, tomllib
|
||||
lock = tomllib.load(open("Cargo.lock", "rb"))
|
||||
bad = sorted({p["source"] for p in lock.get("package", [])
|
||||
if p.get("source", "").startswith("git+")
|
||||
and not p["source"].startswith("git+https://github.com/storytold/")})
|
||||
if bad:
|
||||
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
# Tracks @main so suite-wide CI fixes reach every app without a PR in each repository.
|
||||
uses: storytold/craft-common-actions/.github/workflows/git-sources.yml@main
|
||||
|
||||
@@ -28,51 +28,9 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
uses: storytold/craft-common-actions/.github/workflows/refresh-contributors.yml@main
|
||||
git-sources:
|
||||
name: Git dependencies come from storytold
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
|
||||
# Source replacement, patches and path overrides in cargo config never show up
|
||||
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
|
||||
# Cargo still reads the legacy .cargo/config.
|
||||
python3 - <<'PY'
|
||||
import os, sys, tomllib
|
||||
for path in (".cargo/config", ".cargo/config.toml"):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
config = tomllib.load(open(path, "rb"))
|
||||
replaced = [name for name, source in config.get("source", {}).items()
|
||||
if isinstance(source, dict) and "replace-with" in source]
|
||||
if replaced or "patch" in config or "paths" in config:
|
||||
print(f"{path} replaces, patches or overrides a source")
|
||||
sys.exit(1)
|
||||
PY
|
||||
# Fails when Cargo.lock is out of date with the manifests.
|
||||
cargo metadata --locked --format-version 1 > /dev/null
|
||||
python3 - <<'PY'
|
||||
import sys, tomllib
|
||||
lock = tomllib.load(open("Cargo.lock", "rb"))
|
||||
bad = sorted({p["source"] for p in lock.get("package", [])
|
||||
if p.get("source", "").startswith("git+")
|
||||
and not p["source"].startswith("git+https://github.com/storytold/")})
|
||||
if bad:
|
||||
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
release:
|
||||
needs: [git-sources, credits]
|
||||
needs: credits
|
||||
permissions:
|
||||
# The reusable release workflow tags and uploads the draft release.
|
||||
contents: write
|
||||
|
||||
Generated
+11
-9
@@ -687,14 +687,6 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-dwg"
|
||||
version = "0.20.1"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-engine"
|
||||
version = "0.20.1"
|
||||
@@ -743,10 +735,10 @@ dependencies = [
|
||||
"acadrust",
|
||||
"cadcraft-color",
|
||||
"cadcraft-doc",
|
||||
"cadcraft-dwg",
|
||||
"cadcraft-fonts",
|
||||
"cadcraft-geom",
|
||||
"cadcraft-render",
|
||||
"craft-dwg",
|
||||
"craft-dxf",
|
||||
"craft-pdf",
|
||||
"image",
|
||||
@@ -796,6 +788,7 @@ dependencies = [
|
||||
"cadcraft-engine",
|
||||
"cadcraft-fonts",
|
||||
"cadcraft-geom",
|
||||
"cadcraft-io",
|
||||
"cadcraft-render",
|
||||
"chrono",
|
||||
"craft-ui",
|
||||
@@ -1143,6 +1136,15 @@ name = "craft-docpath"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
|
||||
[[package]]
|
||||
name = "craft-dwg"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=7bb6c90076a0177d01cc4c482e7fd8dc9f31a8bf#7bb6c90076a0177d01cc4c482e7fd8dc9f31a8bf"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-dxf"
|
||||
version = "0.1.0"
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ repository = "https://github.com/storytold/cadcraft"
|
||||
cadcraft-geom = { path = "crates/geom" }
|
||||
# DXF tags: the shared reader and writer (moved from crates/dxf; craft-libs#791).
|
||||
craft-dxf = { git = "https://github.com/storytold/craft-libs", rev = "006e4df225fca57e95ead0fb1f91920900798a99" }
|
||||
cadcraft-dwg = { path = "crates/dwg" }
|
||||
craft-dwg = { git = "https://github.com/storytold/craft-libs", rev = "7bb6c90076a0177d01cc4c482e7fd8dc9f31a8bf" }
|
||||
cadcraft-color = { path = "crates/color" }
|
||||
cadcraft-doc = { path = "crates/doc" }
|
||||
cadcraft-constraints = { path = "crates/constraints" }
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "cadcraft-dwg"
|
||||
description = "CADCraft DWG bridge: converts DWG to and from DXF bytes using the acadrust crate (MPL-2.0, used unmodified)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
|
||||
# The same targets as build.rs's `no_dwg_bridge` leave acadrust out: wasm32, and FreeBSD aarch64,
|
||||
# where acadrust 0.6.6 passes a `*const c_char` (u8 there) to its own `*const i8` sysctlbyname.
|
||||
# 0.6.7 and 0.6.8 fix that but read some damaged DWGs as empty with no note (tests.rs, #800).
|
||||
[target.'cfg(not(any(target_arch = "wasm32", all(target_os = "freebsd", target_arch = "aarch64"))))'.dependencies]
|
||||
# MPL-2.0: used as an unmodified dependency only (see plan/adr/0001 and NOTICE).
|
||||
acadrust = "0.6.6"
|
||||
@@ -1,12 +0,0 @@
|
||||
//! Sets `no_dwg_bridge` on the targets built without acadrust (see Cargo.toml): the DWG functions
|
||||
//! there return an error that says to use DXF.
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo::rustc-check-cfg=cfg(no_dwg_bridge)");
|
||||
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if arch == "wasm32" || (os == "freebsd" && arch == "aarch64") {
|
||||
println!("cargo::rustc-cfg=no_dwg_bridge");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,632 +0,0 @@
|
||||
//! Another application's entities and their classes through the DWG bridge: on reading a DWG,
|
||||
//! registered entities acadrust has no model for become proxy entities of their class; on writing
|
||||
//! one, the DWG gets the source DXF's classes in their places, so every proxy names its own class.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use acadrust::CadDocument;
|
||||
use acadrust::classes::{DxfClass, ProxyFlags};
|
||||
use acadrust::entities::{EntityType, ExtendedEntityData, ProxyEntityData};
|
||||
use acadrust::objects::{ObjectType, ProxyPayload};
|
||||
|
||||
fn class_number(index: usize) -> Result<i16, String> {
|
||||
index.checked_add(500).and_then(|n| i16::try_from(n).ok()).ok_or_else(|| "DXF: too many application classes".into())
|
||||
}
|
||||
|
||||
/// After a DWG read: each registered entity of another application's class that acadrust keeps
|
||||
/// only as its record and opaque data (no decoded properties) becomes a proxy entity of that class
|
||||
/// (DXF Reference, ACAD_PROXY_ENTITY), with its graphics, data bits, R2007+ strings and object
|
||||
/// references, so the DXF keeps it; and every proxy's class number becomes 500 plus its class's
|
||||
/// place in the classes, as a DXF numbers it. A record that can't be converted (its class is
|
||||
/// missing or not unique) is left out and named in the notes; one with decoded properties stays as
|
||||
/// read, also named, as its opaque data has no DXF form beside them.
|
||||
pub(super) fn prepare(doc: &mut CadDocument) -> Vec<String> {
|
||||
let classes: Vec<_> = doc.classes.iter().cloned().collect();
|
||||
let mut numbers = HashMap::new();
|
||||
let mut identities = HashMap::new();
|
||||
for (index, class) in classes.iter().enumerate() {
|
||||
let number = class_number(index).ok();
|
||||
numbers.entry(i32::from(class.class_number)).and_modify(|v| *v = None).or_insert(number.map(i32::from));
|
||||
let identity = (class.dxf_name.as_str(), class.cpp_class_name.as_str(), class.is_an_entity);
|
||||
identities.entry(identity).and_modify(|v| *v = None).or_insert(number);
|
||||
}
|
||||
let identity = |dxf: &str, cpp: &str, entity: bool| {
|
||||
identities
|
||||
.get(&(dxf, cpp, entity))
|
||||
.copied()
|
||||
.flatten()
|
||||
.map(i32::from)
|
||||
.ok_or_else(|| format!("missing or ambiguous application CLASS for {dxf:.80} ({cpp:.80})"))
|
||||
};
|
||||
let Some(dwg_version) = super::native::proxy_dwg_version(doc.dwg_source_version.unwrap_or(doc.version)) else { return Vec::new() };
|
||||
let maintenance = i32::from(doc.maintenance_version);
|
||||
let remap = |id: i32| numbers.get(&id).copied().flatten().ok_or_else(|| format!("proxy names missing or ambiguous application class {id}"));
|
||||
// Only mutate proxies: entities_mut invalidates the decoder's verbatim records for every entity.
|
||||
let handles: Vec<_> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Extended(e) => match &e.data {
|
||||
ExtendedEntityData::Proxy(_) => Some(e.common.handle),
|
||||
ExtendedEntityData::RegisteredClass(data) if data.properties.is_empty() || data.payload.bit_count > 0 => Some(e.common.handle),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let mut notes = Vec::new();
|
||||
let mut entity_count = 0usize;
|
||||
let mut object_count = 0usize;
|
||||
let mut semantic = 0usize;
|
||||
for handle in handles {
|
||||
// Ok(None): not an application record; Ok(Some(None)): kept as read, with its properties.
|
||||
let converted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> Result<Option<Option<ExtendedEntityData>>, String> {
|
||||
let Some(EntityType::Extended(entity)) = doc.get_entity(handle) else { return Ok(None) };
|
||||
let data = match &entity.data {
|
||||
ExtendedEntityData::Proxy(data) => {
|
||||
let mut proxy = data.clone();
|
||||
proxy.class_id = remap(proxy.class_id)?;
|
||||
ExtendedEntityData::Proxy(proxy)
|
||||
}
|
||||
ExtendedEntityData::RegisteredClass(data) => {
|
||||
if !data.properties.is_empty() {
|
||||
return Ok(Some(None));
|
||||
}
|
||||
let class_id = identity(&data.dxf_name, &data.cpp_class_name, true)?;
|
||||
let text_payload = entity.common.raw_record.as_deref().map(registered_text).transpose()?.unwrap_or_default();
|
||||
ExtendedEntityData::Proxy(ProxyEntityData {
|
||||
proxy_id: 498,
|
||||
class_id,
|
||||
dxf_subclass: String::new(),
|
||||
version: (maintenance << 16) | dwg_version,
|
||||
dwg_version,
|
||||
maintenance_version: maintenance,
|
||||
from_dxf: false,
|
||||
graphics: ProxyPayload::from_bytes(entity.common.graphic_data.as_deref().unwrap_or_default()),
|
||||
payload: data.payload.clone(),
|
||||
text_payload,
|
||||
object_ids: data.object_ids.clone(),
|
||||
})
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(Some(data)))
|
||||
}))
|
||||
.unwrap_or_else(|_| Err("application record conversion failed".into()));
|
||||
match converted {
|
||||
Ok(Some(Some(data))) => {
|
||||
if let Some(EntityType::Extended(entity)) = doc.get_entity_mut(handle) {
|
||||
entity.data = data;
|
||||
}
|
||||
}
|
||||
Ok(Some(None)) => semantic = semantic.saturating_add(1),
|
||||
Ok(None) => {}
|
||||
Err(why) => {
|
||||
doc.remove_entity(handle);
|
||||
entity_count = entity_count.saturating_add(1);
|
||||
skipped_note(&mut notes, "entity", handle, &why);
|
||||
}
|
||||
}
|
||||
}
|
||||
doc.objects.retain(|handle, object| {
|
||||
if let ObjectType::ProxyObject(proxy) = object {
|
||||
match remap(proxy.class_id) {
|
||||
Ok(id) => proxy.class_id = id,
|
||||
Err(why) => {
|
||||
object_count = object_count.saturating_add(1);
|
||||
skipped_note(&mut notes, "object", *handle, &why);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
if entity_count > 0 || object_count > 0 {
|
||||
notes.insert(0, format!("DWG: partial read; application proxy records left out (entities: {entity_count}, objects: {object_count})"));
|
||||
}
|
||||
if semantic > 0 {
|
||||
notes.push(format!(
|
||||
"DWG: {semantic} registered entities with decoded properties keep them, without their other data; saving over the original file would lose it"
|
||||
));
|
||||
}
|
||||
notes
|
||||
}
|
||||
|
||||
fn skipped_note(notes: &mut Vec<String>, kind: &str, handle: acadrust::types::Handle, why: &str) {
|
||||
if notes.len() < 6 {
|
||||
let why: String = why.chars().take(256).collect();
|
||||
notes.push(format!("DWG: proxy {kind} {handle} left out: {why}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The decoder's registered-class representation omits the separate R2007+ string stream.
|
||||
fn registered_text(raw: &acadrust::entities::RawRecord) -> Result<ProxyPayload, String> {
|
||||
use acadrust::io::dwg::DwgVersion;
|
||||
use acadrust::io::dwg::dwg_stream_readers::object_reader::DwgObjectReader;
|
||||
use acadrust::io::dwg::dwg_stream_writers::bit_writer::DwgBitWriter;
|
||||
use acadrust::types::DxfVersion;
|
||||
|
||||
if raw.version < DxfVersion::AC1021 {
|
||||
return Ok(ProxyPayload::default());
|
||||
}
|
||||
let version = DwgVersion::from_dxf_version(raw.version).map_err(|e| format!("DWG proxy text: {e}"))?;
|
||||
let handle_bits = usize::try_from(raw.handle_bits).map_err(|_| "DWG proxy text: negative handle stream size")?;
|
||||
let total_bits = raw.data.len().checked_mul(8).ok_or("DWG proxy text: record too large")?;
|
||||
if handle_bits > total_bits {
|
||||
return Err("DWG proxy text: handle stream exceeds record".into());
|
||||
}
|
||||
// Frame the verbatim record for the dependency's checked object reader.
|
||||
let mut frame = DwgBitWriter::new(version, raw.version);
|
||||
frame.write_modular_short(raw.data.len());
|
||||
if raw.version >= DxfVersion::AC1024 {
|
||||
frame.write_modular_char(handle_bits);
|
||||
}
|
||||
frame.write_bytes(&raw.data);
|
||||
let reader = DwgObjectReader::new(frame.into_bytes(), raw.version, HashMap::new()).map_err(|e| format!("DWG proxy text: {e}"))?;
|
||||
let (kind, mut streams) = reader.read_record_at(0).map_err(|e| format!("DWG proxy text: {e}"))?;
|
||||
reader.read_common_entity_data(&mut streams, kind);
|
||||
if kind == 498 {
|
||||
// An existing registered-class envelope is a proxy and carries its class name first.
|
||||
streams.read_variable_text();
|
||||
} else if kind < 500 {
|
||||
return Err("DWG proxy text: registered entity has no application class".into());
|
||||
}
|
||||
streams.check_error().map_err(|e| format!("DWG proxy text: {e}"))?;
|
||||
let bits = usize::try_from(streams.text_remaining_bits()).map_err(|_| "DWG proxy text: negative string stream size")?;
|
||||
if bits > total_bits {
|
||||
return Err("DWG proxy text: string stream exceeds record".into());
|
||||
}
|
||||
let count = u32::try_from(bits).map_err(|_| "DWG proxy text: string stream too large")?;
|
||||
let mut bytes = vec![0; bits.div_ceil(8)];
|
||||
for (index, byte) in bytes.iter_mut().enumerate() {
|
||||
for bit in 0..8.min(bits.saturating_sub(index * 8)) {
|
||||
if streams.read_text_bit() {
|
||||
*byte |= 0x80 >> bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
streams.check_error().map_err(|e| format!("DWG proxy text: {e}"))?;
|
||||
Ok(ProxyPayload::from_bits(&bytes, count))
|
||||
}
|
||||
|
||||
/// ASCII DXF pairs decoded with the bridge's public code-page and escape codecs. A malformed
|
||||
/// pair yields `None`, so both class scans fall back rather than guessing its identity.
|
||||
pub(super) fn text_pairs(dxf: &[u8]) -> Option<impl Iterator<Item = Option<(i32, String)>> + '_> {
|
||||
use acadrust::io::dxf::code_page::{decode_legacy_escapes, encoding_from_code_page};
|
||||
|
||||
if dxf.starts_with(b"AutoCAD Binary DXF") {
|
||||
return None;
|
||||
}
|
||||
let version =
|
||||
super::native::header_value(dxf, b"$ACADVER").and_then(|(_, v)| std::str::from_utf8(v).ok()).map(acadrust::DxfVersion::from_version_string);
|
||||
let unicode = version.is_some_and(|v| v >= acadrust::DxfVersion::AC1021);
|
||||
let encoding = if version.is_some_and(|v| v < acadrust::DxfVersion::AC1021) {
|
||||
super::native::header_value(dxf, b"$DWGCODEPAGE").and_then(|(_, page)| std::str::from_utf8(page).ok()).and_then(encoding_from_code_page)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut lines = dxf.split(|b| *b == b'\n').map(|line| line.strip_suffix(b"\r").unwrap_or(line));
|
||||
Some(std::iter::from_fn(move || {
|
||||
let code = lines.next()?;
|
||||
let Some(value) = lines.next() else { return (!code.trim_ascii().is_empty()).then_some(None) };
|
||||
let pair = || {
|
||||
let code = std::str::from_utf8(code).ok()?.trim().parse().ok()?;
|
||||
let text = if let Some(encoding) = encoding {
|
||||
encoding.decode(value).0.into_owned()
|
||||
} else if unicode {
|
||||
String::from_utf8_lossy(value).into_owned()
|
||||
} else {
|
||||
// The decoder uses UTF-8 with Latin-1 fallback when no encoding was declared.
|
||||
std::str::from_utf8(value).map_or_else(|_| value.iter().map(|&b| char::from(b)).collect(), str::to_string)
|
||||
};
|
||||
let text = decode_legacy_escapes(&text);
|
||||
// DXF's escaped control characters are decoded after CIF/MIF, as in the bridge.
|
||||
let text = text.replace("^J", "\n").replace("^M", "\r").replace("^I", "\t").replace("^ ", "^");
|
||||
Some((code, text))
|
||||
};
|
||||
Some(pair())
|
||||
}))
|
||||
}
|
||||
|
||||
/// The CLASS records of an ASCII DXF, in CLASSES order and numbered by place from 500 (DXF
|
||||
/// Reference, CLASSES; a proxy's group 91 is that number). `None` for a binary DXF (CADCraft's own
|
||||
/// bridge DXF is ASCII), and for one with a record that can't be written back as it is: no names,
|
||||
/// flags or instance count out of range, more classes than the numbers allow. Then
|
||||
/// `proxy_classes` matches the classes by name instead.
|
||||
pub(super) fn source_classes(dxf: &[u8]) -> Option<Vec<DxfClass>> {
|
||||
let pairs = text_pairs(dxf)?;
|
||||
let mut records: Vec<Vec<(i32, String)>> = Vec::new();
|
||||
let (mut section, mut in_classes) = (false, false);
|
||||
for pair in pairs {
|
||||
let (code, value) = pair?;
|
||||
match (code, value.as_str()) {
|
||||
(0, "ENDSEC") if in_classes => break,
|
||||
(0, "SECTION") => section = true,
|
||||
(2, name) if section => {
|
||||
in_classes = name == "CLASSES";
|
||||
section = false;
|
||||
}
|
||||
(0, kind) if in_classes => {
|
||||
if kind != "CLASS" {
|
||||
return None;
|
||||
}
|
||||
records.push(Vec::new());
|
||||
}
|
||||
(code, value) if in_classes => records.last_mut()?.push((code, value.to_string())),
|
||||
_ => section = false,
|
||||
}
|
||||
}
|
||||
records.iter().enumerate().map(|(index, groups)| source_class(groups, class_number(index).ok()?)).collect()
|
||||
}
|
||||
|
||||
/// One CLASS record's groups as the class `number`, or `None` when it can't be written back.
|
||||
fn source_class(groups: &[(i32, String)], number: i16) -> Option<DxfClass> {
|
||||
let text = |code| groups.iter().find(|(c, _)| *c == code).map(|(_, v)| v.clone()).unwrap_or_default();
|
||||
let integer = |code| groups.iter().find(|(c, _)| *c == code).map_or(Some(0), |(_, v)| v.trim().parse::<i64>().ok());
|
||||
let mut class = DxfClass::new(text(1), text(2));
|
||||
if class.dxf_name.is_empty() || class.cpp_class_name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
class.application_name = text(3);
|
||||
class.proxy_flags = ProxyFlags(u16::try_from(integer(90)?).ok()?);
|
||||
class.instance_count = i32::try_from(integer(91)?).ok()?;
|
||||
class.was_zombie = integer(280)? != 0;
|
||||
class.is_an_entity = integer(281)? != 0;
|
||||
class.item_class_id = if class.is_an_entity { 498 } else { 499 };
|
||||
class.class_number = number;
|
||||
Some(class)
|
||||
}
|
||||
|
||||
/// Give a DWG of 2013 or later the source DXF's classes (`classes`, from [`source_classes`]) in
|
||||
/// their places, with their metadata and duplicate names, so each proxy's DXF class number names
|
||||
/// its own class; the decoder's default classes the source lacks follow them. A proxy whose number
|
||||
/// names no source class is left out, as `proxy_classes` leaves it out. Returns how many proxy objects and
|
||||
/// entities were left out; nothing changes when the drawing has no proxies. `None`, with nothing
|
||||
/// changed, when the classes would be more than the class numbers allow.
|
||||
pub(super) fn restore_classes(doc: &mut CadDocument, classes: Vec<DxfClass>) -> Option<(usize, usize)> {
|
||||
let fits = |number: i32| number.checked_sub(500).and_then(|n| usize::try_from(n).ok()).is_some_and(|i| i < classes.len());
|
||||
let mut any = false;
|
||||
let mut entities = Vec::new();
|
||||
for entity in doc.entities() {
|
||||
if let EntityType::Extended(e) = entity
|
||||
&& let ExtendedEntityData::Proxy(p) = &e.data
|
||||
{
|
||||
any = true;
|
||||
if !fits(p.class_id) {
|
||||
entities.push(e.common.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut objects = Vec::new();
|
||||
for (handle, object) in &doc.objects {
|
||||
if let ObjectType::ProxyObject(p) = object {
|
||||
any = true;
|
||||
if !fits(p.class_id) {
|
||||
objects.push(*handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return Some((0, 0));
|
||||
}
|
||||
let mut names: HashSet<_> = classes.iter().map(|c| c.dxf_name.to_uppercase()).collect();
|
||||
let defaults: Vec<_> = doc.classes.iter().filter(|c| names.insert(c.dxf_name.to_uppercase())).cloned().collect();
|
||||
if let Some(last) = classes.len().checked_add(defaults.len())?.checked_sub(1) {
|
||||
class_number(last).ok()?;
|
||||
}
|
||||
for handle in &entities {
|
||||
doc.remove_entity(*handle);
|
||||
}
|
||||
for handle in &objects {
|
||||
doc.objects.remove(handle);
|
||||
}
|
||||
doc.classes.clear();
|
||||
for (index, mut class) in classes.into_iter().chain(defaults).enumerate() {
|
||||
class.class_number = class_number(index).ok()?;
|
||||
doc.classes.push_preserving(class);
|
||||
}
|
||||
Some((objects.len(), entities.len()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use acadrust::entities::{EntityCommon, ExtendedEntity, RegisteredClassEntityData};
|
||||
use acadrust::types::DxfVersion;
|
||||
|
||||
fn proxy_doc() -> CadDocument {
|
||||
let mut doc = CadDocument::new();
|
||||
doc.version = DxfVersion::AC1032;
|
||||
doc.add_entity(EntityType::Extended(Box::new(ExtendedEntity {
|
||||
common: EntityCommon::default(),
|
||||
data: ExtendedEntityData::Proxy(ProxyEntityData {
|
||||
proxy_id: 498,
|
||||
class_id: 501,
|
||||
dxf_subclass: String::new(),
|
||||
version: 33,
|
||||
dwg_version: 33,
|
||||
maintenance_version: 0,
|
||||
from_dxf: false,
|
||||
graphics: ProxyPayload::default(),
|
||||
payload: ProxyPayload::from_bits(&[0x80], 1),
|
||||
text_payload: ProxyPayload::default(),
|
||||
object_ids: vec![],
|
||||
}),
|
||||
})))
|
||||
.unwrap();
|
||||
doc
|
||||
}
|
||||
|
||||
/// An ASCII DXF of `groups`, `(code, value)` per pair of lines.
|
||||
fn dxf(groups: &[(i32, &str)]) -> Vec<u8> {
|
||||
groups.iter().map(|(c, v)| format!("{c}\n{v}\n")).collect::<String>().into_bytes()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_classes_keep_duplicates_and_metadata_before_defaults() {
|
||||
let bytes = dxf(&[
|
||||
(0, "SECTION"),
|
||||
(2, "HEADER"),
|
||||
(9, "$DIMSTYLE"),
|
||||
(2, "CLASSES"),
|
||||
(0, "ENDSEC"),
|
||||
(0, "SECTION"),
|
||||
(2, "CLASSES"),
|
||||
(0, "CLASS"),
|
||||
(1, "Объект"),
|
||||
(2, "First"),
|
||||
(3, "Первый"),
|
||||
(90, "1025"),
|
||||
(91, "7"),
|
||||
(280, "1"),
|
||||
(281, "1"),
|
||||
(0, "CLASS"),
|
||||
(1, "Объект"),
|
||||
(2, "Second"),
|
||||
(3, "Второй"),
|
||||
(90, "128"),
|
||||
(281, "1"),
|
||||
(0, "ENDSEC"),
|
||||
(0, "EOF"),
|
||||
]);
|
||||
let classes = source_classes(&bytes).unwrap();
|
||||
assert_eq!(classes.len(), 2);
|
||||
assert_eq!(classes[0].application_name, "Первый");
|
||||
assert_eq!(classes[0].proxy_flags.0, 1025);
|
||||
assert_eq!(classes[0].instance_count, 7);
|
||||
assert!(classes[0].was_zombie);
|
||||
assert_eq!((classes[0].class_number, classes[1].class_number), (500, 501));
|
||||
assert_eq!(classes[1].cpp_class_name, "Second");
|
||||
let mut doc = proxy_doc();
|
||||
assert_eq!(restore_classes(&mut doc, classes.clone()), Some((0, 0)));
|
||||
assert_eq!(doc.classes.iter().take(2).cloned().collect::<Vec<_>>(), classes);
|
||||
// A binary DXF, and a CLASS record that can't be written back as it is, give no classes.
|
||||
assert!(source_classes(b"AutoCAD Binary DXF\r\n\x1a\0").is_none());
|
||||
for (code, value) in [(1, ""), (2, ""), (90, "65536"), (91, "9223372036854775807"), (281, "x")] {
|
||||
let mut class = vec![(1, "A"), (2, "A")];
|
||||
class.retain(|(c, _)| *c != code);
|
||||
class.push((code, value));
|
||||
let mut groups = vec![(0, "SECTION"), (2, "CLASSES"), (0, "CLASS")];
|
||||
groups.extend(class);
|
||||
groups.extend([(0, "ENDSEC"), (0, "EOF")]);
|
||||
assert!(source_classes(&dxf(&groups)).is_none(), "{code} {value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_classes_decode_escapes_once_and_respect_legacy_code_pages() {
|
||||
let body = "0\nCLASS\n1\n\\U+041E\\U+0431\\U+044A\\U+0435\\U+043A\\U+0442\n2\nClass\n3\nCaf\\U+00E9\\U+D83D\\U+DE42\\U+005CU+0041\n281\n1\n0\nENDSEC\n0\nEOF\n";
|
||||
let text = format!("0\nSECTION\n2\nCLASSES\n{body}");
|
||||
let classes = source_classes(text.as_bytes()).unwrap();
|
||||
assert_eq!(classes[0].dxf_name, "Объект");
|
||||
assert_eq!(classes[0].application_name, "Café🙂\\U+0041");
|
||||
|
||||
let prefix =
|
||||
b"0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$DWGCODEPAGE\n3\nANSI_1251\n0\nENDSEC\n0\nSECTION\n2\nCLASSES\n0\nCLASS\n1\n";
|
||||
let encoded = [prefix.as_slice(), b"\xCF\xF0\xE8\xEC\xE5\xF0", b"\n2\nClass\n3\nApp\n281\n1\n0\nENDSEC\n0\nEOF\n"].concat();
|
||||
assert_eq!(source_classes(&encoded).unwrap()[0].dxf_name, "Пример");
|
||||
// R2007+ text is UTF-8 even with a legacy code-page declaration.
|
||||
let modern = String::from_utf8_lossy(prefix).replace("AC1015", "AC1032") + "Пример\n2\nClass\n3\nApp\n281\n1\n0\nENDSEC\n0\nEOF\n";
|
||||
assert_eq!(source_classes(modern.as_bytes()).unwrap()[0].dxf_name, "Пример");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxies_of_missing_classes_are_left_out() {
|
||||
for id in [-1, 499, 501, i32::MAX] {
|
||||
let mut doc = proxy_doc();
|
||||
if let Some(EntityType::Extended(e)) = doc.entities_mut().next()
|
||||
&& let ExtendedEntityData::Proxy(p) = &mut e.data
|
||||
{
|
||||
p.class_id = id;
|
||||
}
|
||||
let mut class = DxfClass::new_entity("ENTITY", "Entity");
|
||||
class.class_number = 500;
|
||||
assert_eq!(restore_classes(&mut doc, vec![class]), Some((0, 1)), "{id}");
|
||||
assert_eq!(doc.entities().count(), 0);
|
||||
}
|
||||
// Class 501 is there: the proxy keeps it, in its place.
|
||||
let mut doc = proxy_doc();
|
||||
assert_eq!(restore_classes(&mut doc, vec![DxfClass::new("A", "A"), DxfClass::new_entity("B", "B")]), Some((0, 0)));
|
||||
assert_eq!(doc.entities().count(), 1);
|
||||
assert_eq!(doc.classes.iter().nth(1).map(|c| (c.dxf_name.clone(), c.class_number)), Some(("B".to_string(), 501)));
|
||||
// Without proxies the classes stay the decoder's.
|
||||
let mut doc = CadDocument::new();
|
||||
let before: Vec<_> = doc.classes.iter().cloned().collect();
|
||||
assert_eq!(restore_classes(&mut doc, vec![DxfClass::new("A", "A")]), Some((0, 0)));
|
||||
assert_eq!(doc.classes.iter().cloned().collect::<Vec<_>>(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrepresentable_proxies_keep_other_geometry_and_bounded_diagnostics() {
|
||||
use acadrust::entities::{Line, RawRecord};
|
||||
use acadrust::objects::ProxyObject;
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
for count in [1, 10] {
|
||||
let mut doc = proxy_doc();
|
||||
if let Some(EntityType::Extended(e)) = doc.entities_mut().next()
|
||||
&& let ExtendedEntityData::Proxy(p) = &mut e.data
|
||||
{
|
||||
p.class_id = i32::MAX;
|
||||
}
|
||||
let template = doc.entities().next().unwrap().clone();
|
||||
for _ in 1..count {
|
||||
let mut entity = template.clone();
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
doc.add_entity(entity).unwrap();
|
||||
}
|
||||
let object = Handle::new(u64::MAX - 1);
|
||||
assert!(!doc.objects.contains_key(&object));
|
||||
doc.objects.insert(object, ObjectType::ProxyObject(ProxyObject { handle: object, class_id: i32::MAX, ..Default::default() }));
|
||||
let line = doc.add_entity(EntityType::Line(Line::from_points(Vector3::new(2.0, 3.0, 0.0), Vector3::new(12.0, 13.0, 0.0)))).unwrap();
|
||||
let raw = RawRecord { data: vec![0xAA, 0x55], handle_bits: 0, version: DxfVersion::AC1032 };
|
||||
doc.get_entity_mut(line).unwrap().common_mut().raw_record = Some(std::sync::Arc::new(raw.clone()));
|
||||
let notes = prepare(&mut doc);
|
||||
assert_eq!(notes.len(), (count + 1).min(6) + 1);
|
||||
assert!(notes.iter().all(|n| n.starts_with("DWG:")));
|
||||
assert!(notes[0].contains(&format!("entities: {count}, objects: 1")));
|
||||
assert_eq!(doc.entities().count(), 1);
|
||||
assert!(!doc.objects.contains_key(&object));
|
||||
let Some(EntityType::Line(actual)) = doc.get_entity(line) else { panic!("readable line") };
|
||||
assert_eq!(actual.start, Vector3::new(2.0, 3.0, 0.0));
|
||||
assert_eq!(actual.end, Vector3::new(12.0, 13.0, 0.0));
|
||||
assert_eq!(doc.get_entity(line).unwrap().common().raw_record.as_deref(), Some(&raw));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn opaque_registered_class_ambiguity_is_not_guessed() {
|
||||
let mut doc = CadDocument::new();
|
||||
doc.classes.push_preserving(DxfClass::new_entity("CUSTOM", "Custom"));
|
||||
doc.classes.push_preserving(DxfClass::new_entity("CUSTOM", "Custom"));
|
||||
doc.add_entity(EntityType::Extended(Box::new(ExtendedEntity {
|
||||
common: EntityCommon::default(),
|
||||
data: ExtendedEntityData::RegisteredClass(RegisteredClassEntityData {
|
||||
dxf_name: "CUSTOM".into(),
|
||||
cpp_class_name: "Custom".into(),
|
||||
properties: vec![],
|
||||
payload: ProxyPayload::from_bits(&[0x80], 1),
|
||||
object_ids: vec![],
|
||||
}),
|
||||
})))
|
||||
.unwrap();
|
||||
assert!(prepare(&mut doc).iter().any(|n| n.contains("ambiguous")));
|
||||
assert_eq!(doc.entities().count(), 0);
|
||||
assert!(class_number(usize::MAX).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn semantic_records_keep_verbatim_data_and_mixed_payloads_are_reported() {
|
||||
use acadrust::entities::RawRecord;
|
||||
use acadrust::objects::{SemanticProperty, SemanticPropertyValue};
|
||||
for payload in [ProxyPayload::default(), ProxyPayload::from_bits(&[0x80], 1)] {
|
||||
let mut doc = CadDocument::new();
|
||||
let raw = RawRecord { data: vec![0xAA], handle_bits: 0, version: DxfVersion::AC1032 };
|
||||
let handle = doc
|
||||
.add_entity(EntityType::Extended(Box::new(ExtendedEntity {
|
||||
common: EntityCommon::default(),
|
||||
data: ExtendedEntityData::RegisteredClass(RegisteredClassEntityData {
|
||||
dxf_name: "SEMANTIC".into(),
|
||||
cpp_class_name: "Semantic".into(),
|
||||
properties: vec![SemanticProperty {
|
||||
subclass: "Semantic".into(),
|
||||
code: 1,
|
||||
value: SemanticPropertyValue::Text("value".into()),
|
||||
}],
|
||||
payload: payload.clone(),
|
||||
object_ids: vec![],
|
||||
}),
|
||||
})))
|
||||
.unwrap();
|
||||
doc.get_entity_mut(handle).unwrap().common_mut().raw_record = Some(std::sync::Arc::new(raw.clone()));
|
||||
assert_eq!(doc.get_entity(handle).unwrap().common().raw_record.as_deref(), Some(&raw));
|
||||
if payload.bit_count == 0 {
|
||||
assert!(prepare(&mut doc).is_empty());
|
||||
assert_eq!(doc.get_entity(handle).unwrap().common().raw_record.as_deref(), Some(&raw));
|
||||
} else {
|
||||
// Kept as read, and named: its opaque data has no DXF form beside the properties.
|
||||
assert!(prepare(&mut doc).iter().any(|n| n.contains("decoded properties")));
|
||||
assert_eq!(doc.get_entity(handle).unwrap().common().raw_record.as_deref(), Some(&raw));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod string_stream_tests {
|
||||
use super::*;
|
||||
use acadrust::entities::{EntityCommon, ExtendedEntity, RawRecord, RegisteredClassEntityData};
|
||||
use acadrust::types::DxfVersion;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn registered_transport_recovers_separate_non_byte_aligned_strings() {
|
||||
for version in [DxfVersion::AC1027, DxfVersion::AC1032] {
|
||||
let mut source = CadDocument::new();
|
||||
source.version = version;
|
||||
source.classes.add_or_update(DxfClass::new_entity("CUSTOM_TEXT", "CustomText"));
|
||||
let class_id = i32::from(source.classes.get_by_name("CUSTOM_TEXT").unwrap().class_number);
|
||||
let text = ProxyPayload::from_bits(&[0xA5, 0xC7, 0xE0], 19);
|
||||
let payload = ProxyPayload::from_bits(&[0x80], 1);
|
||||
source
|
||||
.add_entity(EntityType::Extended(Box::new(ExtendedEntity {
|
||||
common: EntityCommon::default(),
|
||||
data: ExtendedEntityData::Proxy(ProxyEntityData {
|
||||
proxy_id: 498,
|
||||
class_id,
|
||||
dxf_subclass: "CustomText".into(),
|
||||
version: 33,
|
||||
dwg_version: 33,
|
||||
maintenance_version: 0,
|
||||
from_dxf: false,
|
||||
graphics: ProxyPayload::default(),
|
||||
payload: payload.clone(),
|
||||
text_payload: text.clone(),
|
||||
object_ids: vec![],
|
||||
}),
|
||||
})))
|
||||
.unwrap();
|
||||
let bytes = acadrust::DwgWriter::write_to_vec(&source).unwrap();
|
||||
let mut decoded = acadrust::DwgReader::from_stream(Cursor::new(bytes)).read().unwrap();
|
||||
let entity = decoded.entities().next().unwrap();
|
||||
let handle = entity.common().handle;
|
||||
let raw = entity.common().raw_record.clone().unwrap();
|
||||
assert_eq!(registered_text(&raw).unwrap().data(), text.data());
|
||||
assert_eq!(registered_text(&raw).unwrap().bit_count, 19);
|
||||
let EntityType::Extended(entity) = decoded.get_entity_mut(handle).unwrap() else { panic!() };
|
||||
entity.data = ExtendedEntityData::RegisteredClass(RegisteredClassEntityData {
|
||||
dxf_name: "CUSTOM_TEXT".into(),
|
||||
cpp_class_name: "CustomText".into(),
|
||||
properties: vec![],
|
||||
payload: payload.clone(),
|
||||
object_ids: vec![],
|
||||
});
|
||||
entity.common.raw_record = Some(raw);
|
||||
assert!(prepare(&mut decoded).is_empty());
|
||||
for _ in 0..2 {
|
||||
let mut dxf = Vec::new();
|
||||
acadrust::DxfWriter::new(&decoded).write_to_writer(&mut dxf).unwrap();
|
||||
let bytes = crate::dxf_to_dwg_as(&dxf, "AC1032").unwrap();
|
||||
decoded = acadrust::DwgReader::from_stream(Cursor::new(bytes)).read().unwrap();
|
||||
let p = decoded
|
||||
.entities()
|
||||
.find_map(|e| match e {
|
||||
EntityType::Extended(e) => match &e.data {
|
||||
ExtendedEntityData::Proxy(p) => Some(p),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(p.payload.bit_count, 1);
|
||||
assert_eq!(p.payload.data(), payload.data());
|
||||
assert_eq!(p.text_payload.bit_count, 19);
|
||||
assert_eq!(p.text_payload.data(), text.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_string_stream_boundaries_return_errors() {
|
||||
for handle_bits in [-1, 1000] {
|
||||
let raw = RawRecord { data: vec![0; 8], handle_bits, version: DxfVersion::AC1032 };
|
||||
assert!(registered_text(&raw).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
//! The DWG reader and writer in a child process.
|
||||
//!
|
||||
//! An allocation failure or stack overflow in the third-party code ends the process it runs in, and
|
||||
//! `catch_unwind` can't stop that. So the app runs them in a copy of itself: [`use_own_worker`] says
|
||||
//! how to start one, and [`serve_if_asked`], first thing in `main`, turns the copy into a worker
|
||||
//! that reads one request from standard input and writes one reply to standard output. A file that
|
||||
//! kills the worker then costs that one open or save, and every other open drawing stays safe.
|
||||
//!
|
||||
//! Without a worker (libraries, tests, a build that can't start one) [`decode`] and [`encode_as`]
|
||||
//! run in the calling process, as they always did. There's no memory limit on the worker yet: that
|
||||
//! takes per-platform system calls, which `unsafe_code = "forbid"` keeps out of this crate.
|
||||
//!
|
||||
//! A reply comes from a process that just handled a possibly hostile file, so it is parsed as
|
||||
//! untrusted input.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::mpsc::{RecvTimeoutError, channel};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::Limits;
|
||||
|
||||
/// The argument that makes the app a worker.
|
||||
pub const WORKER_ARG: &str = "--dwg-worker";
|
||||
/// Bumped when the messages change: an app updated while running may start a newer copy.
|
||||
const PROTOCOL: u8 = 2;
|
||||
const DECODE: u8 = 1;
|
||||
const ENCODE: u8 = 2;
|
||||
const CRASH: u8 = 0xff;
|
||||
/// How long one file may take before the worker is stopped. It can't be interrupted otherwise.
|
||||
const TIMEOUT: Duration = Duration::from_secs(30 * 60);
|
||||
/// Most bytes accepted for what a reply carries besides the drawing.
|
||||
const MAX_NOTES_BYTES: u64 = 64 << 20;
|
||||
/// Most bytes accepted as a written DWG.
|
||||
const MAX_DWG_REPLY: u64 = 2 << 30;
|
||||
/// Most bytes one request holds: the largest input, whichever the direction, and its header.
|
||||
const MAX_REQUEST: u64 = (2 << 30) + 256;
|
||||
|
||||
struct Worker {
|
||||
program: PathBuf,
|
||||
args: Vec<OsString>,
|
||||
}
|
||||
|
||||
static WORKER: Mutex<Option<Worker>> = Mutex::new(None);
|
||||
|
||||
/// Start `program args` as the worker from now on. `program` must call [`serve_if_asked`].
|
||||
pub fn use_worker(program: PathBuf, args: Vec<OsString>) {
|
||||
if let Ok(mut w) = WORKER.lock() {
|
||||
*w = Some(Worker { program, args });
|
||||
}
|
||||
}
|
||||
|
||||
/// Use a copy of this program as the worker, started with [`WORKER_ARG`]. If its path can't be
|
||||
/// found, the reader and writer stay in this process.
|
||||
pub fn use_own_worker() {
|
||||
match std::env::current_exe() {
|
||||
Ok(program) => use_worker(program, vec![WORKER_ARG.into()]),
|
||||
Err(e) => log::warn!("DWG worker: can't find this program: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve one request and return the exit code if this process was started as a worker.
|
||||
pub fn serve_if_asked() -> Option<i32> {
|
||||
(std::env::args_os().nth(1)? == WORKER_ARG).then(|| serve(std::io::stdin().lock(), std::io::stdout().lock()))
|
||||
}
|
||||
|
||||
/// [`super::dwg_to_dxf_noted`] in the worker, when there is one.
|
||||
pub fn decode(bytes: &[u8], limits: Limits) -> Result<(Vec<u8>, Vec<String>), String> {
|
||||
// Before starting a process, and so the same limit holds with or without one.
|
||||
if bytes.len() > limits.max_dwg_bytes {
|
||||
return super::dwg_to_dxf_noted(bytes, limits);
|
||||
}
|
||||
let reply_limit = (limits.max_dxf_bytes as u64).saturating_add(MAX_NOTES_BYTES);
|
||||
match call(&request(DECODE, limits, ""), bytes, reply_limit, "reading this file") {
|
||||
Some(reply) => reply?,
|
||||
None => super::dwg_to_dxf_noted(bytes, limits),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`super::dxf_to_dwg_as`] in the worker, when there is one.
|
||||
pub fn encode_as(dxf: &[u8], version: &str) -> Result<Vec<u8>, String> {
|
||||
encode_noted(dxf, version).map(|(dwg, _)| dwg)
|
||||
}
|
||||
|
||||
/// [`super::dxf_to_dwg_noted`] in the worker, when there is one. What the writer left out comes
|
||||
/// back as notes of `TYPE<tab>count`.
|
||||
pub fn encode_noted(dxf: &[u8], version: &str) -> Result<(Vec<u8>, super::Unwritten), String> {
|
||||
encode_kept_noted(dxf, version, None)
|
||||
}
|
||||
|
||||
/// [`encode_noted`] for a DXF whose carried-over groups were read from a file of version `kept`
|
||||
/// ([`super::dxf_to_dwg_kept_noted`]); `None` is [`encode_noted`].
|
||||
pub fn encode_kept_noted(dxf: &[u8], version: &str, kept: Option<&str>) -> Result<(Vec<u8>, super::Unwritten), String> {
|
||||
if u8::try_from(version.len()).is_err() {
|
||||
return Err(super::unsupported(version));
|
||||
}
|
||||
// A kept version too long to send is no version CADCraft knows: it changes nothing.
|
||||
let kept = kept.filter(|k| u8::try_from(k.len()).is_ok());
|
||||
let mut header = request(ENCODE, Limits::DEFAULT, version);
|
||||
push_str(&mut header, kept.unwrap_or(""));
|
||||
match call(&header, dxf, MAX_DWG_REPLY, "writing this file") {
|
||||
Some(reply) => reply?.map(|(dwg, notes)| (dwg, unwritten_from(¬es))),
|
||||
None => encode_here(dxf, version, kept),
|
||||
}
|
||||
}
|
||||
|
||||
/// The conversion in this process.
|
||||
fn encode_here(dxf: &[u8], version: &str, kept: Option<&str>) -> Result<(Vec<u8>, super::Unwritten), String> {
|
||||
match kept {
|
||||
Some(kept) => super::dxf_to_dwg_kept_noted(dxf, version, kept),
|
||||
None => super::dxf_to_dwg_noted(dxf, version),
|
||||
}
|
||||
}
|
||||
|
||||
fn unwritten_from(notes: &[String]) -> super::Unwritten {
|
||||
notes.iter().filter_map(|n| n.split_once('\t')).filter_map(|(kind, n)| Some((kind.to_string(), n.parse::<usize>().ok()?))).fold(
|
||||
super::Unwritten::new(),
|
||||
|mut m, (kind, n)| {
|
||||
*m.entry(kind).or_insert(0) = n;
|
||||
m
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Make the worker abort, as the third-party code can: what the tests prove isolation with.
|
||||
#[doc(hidden)]
|
||||
pub fn crash_worker_for_test() -> Result<(), String> {
|
||||
call(&request(CRASH, Limits::DEFAULT, ""), &[], 1024, "testing").ok_or("no worker")??.map(|_| ())
|
||||
}
|
||||
|
||||
fn request(op: u8, limits: Limits, version: &str) -> Vec<u8> {
|
||||
let mut r = vec![PROTOCOL, op];
|
||||
r.extend((limits.max_dwg_bytes as u64).to_le_bytes());
|
||||
r.extend((limits.max_dxf_bytes as u64).to_le_bytes());
|
||||
push_str(&mut r, version);
|
||||
r
|
||||
}
|
||||
|
||||
/// `text` with a one-byte length before it; callers keep it under 256 bytes.
|
||||
fn push_str(r: &mut Vec<u8>, text: &str) {
|
||||
r.push(u8::try_from(text.len()).unwrap_or(0));
|
||||
r.extend(text.as_bytes());
|
||||
}
|
||||
|
||||
type Reply = Result<(Vec<u8>, Vec<String>), String>;
|
||||
|
||||
/// Run one request in a worker. `None`: none could be started, so the caller does the work.
|
||||
/// `Err`: the worker went wrong, and `doing` says at what, for the user.
|
||||
fn call(header: &[u8], payload: &[u8], reply_limit: u64, doing: &str) -> Option<Result<Reply, String>> {
|
||||
let mut command = {
|
||||
let worker = WORKER.lock().ok()?;
|
||||
let worker = worker.as_ref()?;
|
||||
let mut c = Command::new(&worker.program);
|
||||
c.args(&worker.args);
|
||||
c
|
||||
};
|
||||
command.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
let mut child = match command.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::warn!("DWG worker: can't start it: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let (Some(mut to_child), Some(from_child)) = (child.stdin.take(), child.stdout.take()) else {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
};
|
||||
let received = std::thread::scope(|scope| {
|
||||
// Written beside the read: a worker that replies before it has read everything, or dies,
|
||||
// must block neither side. A failed write shows up as the reply going wrong.
|
||||
scope.spawn(move || {
|
||||
let _ = to_child.write_all(header).and_then(|()| to_child.write_all(payload));
|
||||
});
|
||||
let (tx, rx) = channel();
|
||||
scope.spawn(move || {
|
||||
let mut reply = Vec::new();
|
||||
let read = from_child.take(reply_limit).read_to_end(&mut reply);
|
||||
let _ = tx.send(read.map(|_| reply));
|
||||
});
|
||||
let received = rx.recv_timeout(TIMEOUT);
|
||||
if matches!(received, Err(RecvTimeoutError::Timeout)) {
|
||||
let _ = child.kill();
|
||||
}
|
||||
received
|
||||
});
|
||||
let status = child.wait();
|
||||
let stopped = || format!("the DWG worker stopped while {doing}; nothing else is affected");
|
||||
Some(match (received, status) {
|
||||
(Err(RecvTimeoutError::Timeout), _) => Err(format!("{doing} took too long, so the DWG worker was stopped; nothing else is affected")),
|
||||
(Ok(Ok(reply)), Ok(status)) if status.success() => parse_reply(reply).ok_or_else(stopped),
|
||||
_ => Err(stopped()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Take `n` bytes off the front of `b`.
|
||||
fn take<'a>(b: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
|
||||
let (head, tail) = b.split_at_checked(n)?;
|
||||
*b = tail;
|
||||
Some(head)
|
||||
}
|
||||
|
||||
fn take_u32(b: &mut &[u8]) -> Option<usize> {
|
||||
let bytes: [u8; 4] = take(b, 4)?.try_into().ok()?;
|
||||
usize::try_from(u32::from_le_bytes(bytes)).ok()
|
||||
}
|
||||
|
||||
fn take_u64(b: &mut &[u8]) -> Option<u64> {
|
||||
Some(u64::from_le_bytes(take(b, 8)?.try_into().ok()?))
|
||||
}
|
||||
|
||||
/// A reply: 0, the note count, each note with its length, then the bytes with their length; or 1
|
||||
/// and a message. `None` when it isn't one, whole. Takes the reply, because the bytes can be most
|
||||
/// of the memory the app may use: they are kept, not copied.
|
||||
fn parse_reply(mut reply: Vec<u8>) -> Option<Reply> {
|
||||
let total = reply.len();
|
||||
let mut b = &reply[..];
|
||||
match *take(&mut b, 1)?.first()? {
|
||||
0 => {
|
||||
let count = take_u32(&mut b)?;
|
||||
let mut notes = Vec::new();
|
||||
for _ in 0..count {
|
||||
let n = take_u32(&mut b)?;
|
||||
notes.push(String::from_utf8_lossy(take(&mut b, n)?).into_owned());
|
||||
}
|
||||
let n = usize::try_from(take_u64(&mut b)?).ok()?;
|
||||
if b.len() != n {
|
||||
return None;
|
||||
}
|
||||
reply.drain(..total - n);
|
||||
Some(Ok((reply, notes)))
|
||||
}
|
||||
1 => Some(Err(String::from_utf8_lossy(b).into_owned())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_reply(out: &mut impl Write, reply: Reply) -> std::io::Result<()> {
|
||||
match reply {
|
||||
Ok((bytes, notes)) => {
|
||||
out.write_all(&[0])?;
|
||||
out.write_all(&u32::try_from(notes.len()).unwrap_or(0).to_le_bytes())?;
|
||||
for note in ¬es {
|
||||
out.write_all(&u32::try_from(note.len()).unwrap_or(0).to_le_bytes())?;
|
||||
if u32::try_from(note.len()).is_ok() {
|
||||
out.write_all(note.as_bytes())?;
|
||||
}
|
||||
}
|
||||
out.write_all(&(bytes.len() as u64).to_le_bytes())?;
|
||||
out.write_all(&bytes)?;
|
||||
}
|
||||
Err(e) => {
|
||||
out.write_all(&[1])?;
|
||||
out.write_all(e.as_bytes())?;
|
||||
}
|
||||
}
|
||||
out.flush()
|
||||
}
|
||||
|
||||
/// What a request asks for, or why it can't be served.
|
||||
fn handle(mut b: &[u8]) -> Reply {
|
||||
let bad = || "the DWG worker got a request it can't read (a different CADCraft version?)".to_string();
|
||||
if take(&mut b, 1).and_then(<[u8]>::first) != Some(&PROTOCOL) {
|
||||
return Err(bad());
|
||||
}
|
||||
let op = take(&mut b, 1).and_then(<[u8]>::first).copied().ok_or_else(bad)?;
|
||||
let max_dwg_bytes = take_u64(&mut b).and_then(|n| usize::try_from(n).ok()).ok_or_else(bad)?;
|
||||
let max_dxf_bytes = take_u64(&mut b).and_then(|n| usize::try_from(n).ok()).ok_or_else(bad)?;
|
||||
let version_len = take(&mut b, 1).and_then(<[u8]>::first).copied().ok_or_else(bad)?;
|
||||
let version = take(&mut b, usize::from(version_len)).and_then(|v| std::str::from_utf8(v).ok()).ok_or_else(bad)?;
|
||||
match op {
|
||||
DECODE => super::dwg_to_dxf_noted(b, Limits { max_dwg_bytes, max_dxf_bytes }),
|
||||
ENCODE => {
|
||||
// The version the DXF's carried-over groups were read from; empty for none.
|
||||
let kept_len = take(&mut b, 1).and_then(<[u8]>::first).copied().ok_or_else(bad)?;
|
||||
let kept = take(&mut b, usize::from(kept_len)).and_then(|v| std::str::from_utf8(v).ok()).ok_or_else(bad)?;
|
||||
encode_here(b, version, (!kept.is_empty()).then_some(kept))
|
||||
.map(|(dwg, left_out)| (dwg, left_out.iter().map(|(kind, n)| format!("{kind}\t{n}")).collect()))
|
||||
}
|
||||
CRASH => std::process::abort(),
|
||||
_ => Err(bad()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve the one request on `input`. The exit code is 0 unless the reply couldn't be sent.
|
||||
fn serve(input: impl Read, mut output: impl Write) -> i32 {
|
||||
let mut request = Vec::new();
|
||||
let reply = match input.take(MAX_REQUEST).read_to_end(&mut request) {
|
||||
Ok(_) => handle(&request),
|
||||
Err(e) => Err(format!("the DWG worker could not read its request: {e}")),
|
||||
};
|
||||
i32::from(write_reply(&mut output, reply).is_err())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn wire(reply: Reply) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
write_reply(&mut out, reply).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_the_writer_left_out_survives_the_reply() {
|
||||
let notes = vec!["ACME_WIDGET\t2".to_string(), "not a count".to_string(), "X\tmany".to_string()];
|
||||
assert_eq!(unwritten_from(¬es), [("ACME_WIDGET".to_string(), 2)].into_iter().collect());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replies_round_trip() {
|
||||
let ok: Reply = Ok((b"bytes".to_vec(), vec!["one".into(), "two é".into()]));
|
||||
assert_eq!(parse_reply(wire(ok.clone())), Some(ok));
|
||||
let failed: Reply = Err("DWG: no".into());
|
||||
assert_eq!(parse_reply(wire(failed.clone())), Some(failed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_cut_short_or_made_up_is_refused() {
|
||||
let whole = wire(Ok((b"bytes".to_vec(), vec!["note".into()])));
|
||||
for n in 0..whole.len() {
|
||||
assert_eq!(parse_reply(whole[..n].to_vec()), None, "cut at {n}");
|
||||
}
|
||||
assert_eq!(parse_reply(vec![7, 0, 0]), None);
|
||||
// A note count far beyond what follows ends at the first missing note.
|
||||
assert_eq!(parse_reply(vec![0, 0xff, 0xff, 0xff, 0xff]), None);
|
||||
assert_eq!(parse_reply(vec![0, 1, 0, 0, 0, 0xff, 0xff, 0xff, 0xff]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_it_cannot_read_is_an_error_not_a_crash() {
|
||||
for request in [
|
||||
&[][..],
|
||||
&[PROTOCOL + 1, DECODE],
|
||||
&[PROTOCOL],
|
||||
&[PROTOCOL, DECODE, 1, 2, 3],
|
||||
&[PROTOCOL, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
] {
|
||||
assert!(handle(request).is_err(), "{request:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An encode request carries the kept version after the target one; a request without it is
|
||||
/// one it can't read, and one with it reaches the converter.
|
||||
#[test]
|
||||
fn an_encode_request_carries_the_kept_version() {
|
||||
let bad = "the DWG worker got a request it can't read (a different CADCraft version?)";
|
||||
let short = self::request(ENCODE, Limits::DEFAULT, "AC1032");
|
||||
assert_eq!(handle(&short).unwrap_err(), bad);
|
||||
for kept in ["", "AC1032"] {
|
||||
let mut request = self::request(ENCODE, Limits::DEFAULT, "AC1032");
|
||||
push_str(&mut request, kept);
|
||||
request.extend(b"not a drawing");
|
||||
let err = handle(&request).unwrap_err();
|
||||
assert_ne!(err, bad, "kept {kept:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serving_reports_a_decode_failure_as_a_reply() {
|
||||
let mut request = self::request(DECODE, Limits::DEFAULT, "");
|
||||
request.extend(b"AC1032 not really a drawing");
|
||||
let mut out = Vec::new();
|
||||
assert_eq!(serve(&request[..], &mut out), 0);
|
||||
assert!(matches!(parse_reply(out.clone()), Some(Err(_))), "{out:?}");
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,8 @@ pub(crate) struct GripEdit {
|
||||
hot: Vec<(Handle, usize)>,
|
||||
/// Typed `GRIP`: still collecting grips ("Select grips").
|
||||
picking: bool,
|
||||
/// What MOVE/ROTATE/SCALE/MIRROR transform: the hot grips' objects and the selection.
|
||||
/// What MOVE/ROTATE/SCALE/MIRROR transform: the hot grips' objects and the objects of the
|
||||
/// selection that the drawing shows.
|
||||
targets: Vec<Handle>,
|
||||
base: Vec2,
|
||||
mode: GripMode,
|
||||
@@ -69,6 +70,12 @@ fn positive(v: Option<f64>) -> Result<f64> {
|
||||
v.filter(|f| f.is_finite() && *f > 1e-12).ok_or_else(|| other("Requires a positive number."))
|
||||
}
|
||||
|
||||
/// Whether the drawing shows `h`. A hidden object (HIDEOBJECTS, an off or frozen layer) takes no
|
||||
/// part in a grip edit, even when Undo or a selection command put it back into the selection.
|
||||
fn shown(s: &Session, h: Handle) -> bool {
|
||||
s.doc().is_ok_and(|d| d.entity(h).is_some_and(|e| d.is_visible(e)))
|
||||
}
|
||||
|
||||
impl GripEdit {
|
||||
fn new(s: &Session, hot: Vec<(Handle, usize)>) -> Result<Self> {
|
||||
let mut g = GripEdit {
|
||||
@@ -87,16 +94,21 @@ impl GripEdit {
|
||||
Ok(g)
|
||||
}
|
||||
|
||||
/// Make grip `index` of `h` hot (the first one becomes the base point).
|
||||
/// Make grip `index` of `h` hot (the first one becomes the base point). The grip of an object
|
||||
/// the drawing does not show is left out, and so are the hidden objects of the selection.
|
||||
fn add_hot(&mut self, s: &Session, (h, i): (Handle, usize)) -> Result<()> {
|
||||
if self.hot.contains(&(h, i)) || self.hot.len() >= MAX_HOT {
|
||||
return Ok(());
|
||||
}
|
||||
let p = s.grips_of(h)?.get(i).copied().ok_or_else(|| other("no such grip"))?;
|
||||
// After the lookup: a missing object or grip stays the error it was.
|
||||
if !shown(s, h) {
|
||||
return Ok(());
|
||||
}
|
||||
if self.hot.is_empty() {
|
||||
self.base = p;
|
||||
self.targets.push(h);
|
||||
self.targets.extend(s.selection().into_iter().filter(|x| *x != h));
|
||||
self.targets.extend(s.selection().into_iter().filter(|x| *x != h && shown(s, *x)));
|
||||
} else if !self.targets.contains(&h) {
|
||||
self.targets.push(h);
|
||||
}
|
||||
@@ -113,7 +125,7 @@ impl GripEdit {
|
||||
if super::curves::is_locked(s, h) {
|
||||
continue;
|
||||
}
|
||||
let Some(e) = d.entity(h) else { continue };
|
||||
let Some(e) = d.entity(h).filter(|e| d.is_visible(e)) else { continue };
|
||||
let gs = e.kind.grips();
|
||||
// The nearest grip of this object within the tolerance.
|
||||
let best = gs
|
||||
@@ -470,7 +482,9 @@ impl Interactive for GripEdit {
|
||||
impl Session {
|
||||
/// Make grips hot and start editing them (the UI's click on a grip; Shift+click collects
|
||||
/// several first). `hot` lists (object, grip index); the first is the base grip. The grip
|
||||
/// modes and their options then run at the command line like any prompt.
|
||||
/// modes and their options then run at the command line like any prompt. Grips of objects the
|
||||
/// drawing does not show are left out (the first grip left is the base grip), and it is an
|
||||
/// error when none is left.
|
||||
pub fn start_grip_edit(&mut self, hot: &[(Handle, usize)]) -> Result<()> {
|
||||
if self.running.is_some() {
|
||||
self.cancel();
|
||||
@@ -479,6 +493,9 @@ impl Session {
|
||||
return Err(other("no grip"));
|
||||
}
|
||||
let g = GripEdit::new(self, hot.to_vec())?;
|
||||
if g.hot.is_empty() {
|
||||
return Err(other("no grip"));
|
||||
}
|
||||
let label = g.label();
|
||||
let machine = crate::guard::Guarded::wrap(Box::new(g));
|
||||
let st = self.state()?;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
//! A hidden object that is back in the selection takes no part in a grip edit: Undo, `sssetfirst`
|
||||
//! or a control call can restore it there, and the grips it shares with a visible object must not
|
||||
//! drag it along.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{EntityKind, Handle};
|
||||
use cadcraft_engine::geom::Vec2;
|
||||
use serde_json::json;
|
||||
|
||||
/// Draws a line from `a` to `b` and returns its handle.
|
||||
fn line(s: &mut Session, a: [f64; 2], b: [f64; 2]) -> Handle {
|
||||
let r = s.execute("line", &json!({ "points": [a, b] })).unwrap();
|
||||
Handle::parse_hex(r["handles"][0].as_str().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// The two ends of the line `h`.
|
||||
fn ends(s: &Session, h: Handle) -> (Vec2, Vec2) {
|
||||
match &s.doc().unwrap().entity(h).unwrap().kind {
|
||||
EntityKind::Line(l) => (l.a.xy(), l.b.xy()),
|
||||
k => panic!("not a line: {k:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Two lines sharing the end (0, 0), the second one hidden, both in the selection.
|
||||
fn shared_corner() -> (Session, Handle, Handle) {
|
||||
let mut s = Session::new();
|
||||
let shown = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
|
||||
let hidden = line(&mut s, [0.0, 0.0], [0.0, 10.0]);
|
||||
s.execute("hideobjects", &json!({ "handles": [hidden.hex()] })).unwrap();
|
||||
let d = s.doc().unwrap();
|
||||
assert!(!d.is_visible(d.entity(hidden).unwrap()), "the second line must be hidden");
|
||||
s.set_selection(vec![shown, hidden]);
|
||||
(s, shown, hidden)
|
||||
}
|
||||
|
||||
/// The index of the grip of `h` at `p`.
|
||||
fn grip_at(s: &Session, h: Handle, p: Vec2) -> usize {
|
||||
s.grips_of(h).unwrap().iter().position(|g| g.near(p, 1e-9)).unwrap()
|
||||
}
|
||||
|
||||
/// Stretching the shared corner moves the visible line only; the hidden one stays where it was.
|
||||
#[test]
|
||||
fn a_hidden_selected_line_is_not_stretched_with_a_shared_grip() {
|
||||
let (mut s, shown, hidden) = shared_corner();
|
||||
let i = grip_at(&s, shown, Vec2::new(0.0, 0.0));
|
||||
s.start_grip_edit(&[(shown, i)]).unwrap();
|
||||
s.cmdline("3,4").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let (start, end) = ends(&s, shown);
|
||||
assert!(start.near(Vec2::new(3.0, 4.0), 1e-9) && end.near(Vec2::new(10.0, 0.0), 1e-9), "{start:?} {end:?}");
|
||||
let (start, end) = ends(&s, hidden);
|
||||
assert!(start.near(Vec2::new(0.0, 0.0), 1e-9) && end.near(Vec2::new(0.0, 10.0), 1e-9), "the hidden line moved: {start:?} {end:?}");
|
||||
}
|
||||
|
||||
/// The Move mode of a grip edit moves the visible selection only.
|
||||
#[test]
|
||||
fn a_hidden_selected_line_is_not_moved_by_the_move_mode_of_a_grip_edit() {
|
||||
let (mut s, shown, hidden) = shared_corner();
|
||||
let i = grip_at(&s, shown, Vec2::new(10.0, 0.0));
|
||||
s.start_grip_edit(&[(shown, i)]).unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.prompt_text().contains("** MOVE **"), "{}", s.prompt_text());
|
||||
s.cmdline("12,5").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let (start, end) = ends(&s, shown);
|
||||
assert!(start.near(Vec2::new(2.0, 5.0), 1e-9) && end.near(Vec2::new(12.0, 5.0), 1e-9), "{start:?} {end:?}");
|
||||
let (start, end) = ends(&s, hidden);
|
||||
assert!(start.near(Vec2::new(0.0, 0.0), 1e-9) && end.near(Vec2::new(0.0, 10.0), 1e-9), "the hidden line moved: {start:?} {end:?}");
|
||||
}
|
||||
|
||||
/// The grip of a hidden object is never made hot, so a grip edit cannot start on it.
|
||||
#[test]
|
||||
fn the_grip_of_a_hidden_object_cannot_be_made_hot() {
|
||||
let (mut s, _, hidden) = shared_corner();
|
||||
let before = s.state().unwrap().doc.clone();
|
||||
let err = s.start_grip_edit(&[(hidden, 0)]).unwrap_err().to_string();
|
||||
assert!(err.contains("no grip"), "{err}");
|
||||
assert!(s.running.is_none());
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.state().unwrap().doc), "nothing may change");
|
||||
}
|
||||
|
||||
/// A hidden object's grip among the hot grips is left out, wherever it stands in the list: the
|
||||
/// first grip the drawing shows is the base grip, and only the visible line is stretched.
|
||||
#[test]
|
||||
fn a_hidden_grip_among_the_hot_grips_is_left_out() {
|
||||
for hidden_first in [false, true] {
|
||||
let (mut s, shown, hidden) = shared_corner();
|
||||
let mut hot = vec![(shown, grip_at(&s, shown, Vec2::new(0.0, 0.0))), (hidden, grip_at(&s, hidden, Vec2::new(0.0, 10.0)))];
|
||||
if hidden_first {
|
||||
hot.reverse();
|
||||
}
|
||||
s.start_grip_edit(&hot).unwrap();
|
||||
s.cmdline("3,4").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let (start, end) = ends(&s, shown);
|
||||
assert!(start.near(Vec2::new(3.0, 4.0), 1e-9) && end.near(Vec2::new(10.0, 0.0), 1e-9), "hidden first: {hidden_first}: {start:?} {end:?}");
|
||||
let (start, end) = ends(&s, hidden);
|
||||
assert!(
|
||||
start.near(Vec2::new(0.0, 0.0), 1e-9) && end.near(Vec2::new(0.0, 10.0), 1e-9),
|
||||
"hidden first: {hidden_first}: the hidden line moved: {start:?} {end:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Only what the drawing does not show is left out: a grip of an object that does not exist is
|
||||
/// still an error, also after a good one.
|
||||
#[test]
|
||||
fn a_missing_object_among_the_hot_grips_is_still_an_error() {
|
||||
let (mut s, shown, _) = shared_corner();
|
||||
let i = grip_at(&s, shown, Vec2::new(0.0, 0.0));
|
||||
let err = s.start_grip_edit(&[(shown, i), (Handle(0xFFFF), 0)]).unwrap_err().to_string();
|
||||
assert!(err.contains("no such object"), "{err}");
|
||||
assert!(s.running.is_none());
|
||||
}
|
||||
|
||||
/// A selected line on a layer that is off is not shown either, and is not stretched with a
|
||||
/// shared grip.
|
||||
#[test]
|
||||
fn a_selected_line_on_a_layer_that_is_off_is_not_stretched_with_a_shared_grip() {
|
||||
let mut s = Session::new();
|
||||
let shown = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
|
||||
s.execute("layer.new", &json!({ "name": "Guides", "current": true })).unwrap();
|
||||
let off = line(&mut s, [0.0, 0.0], [0.0, 10.0]);
|
||||
s.execute("layer.current", &json!({ "name": "0" })).unwrap();
|
||||
s.execute("layer.set", &json!({ "name": "Guides", "on": false })).unwrap();
|
||||
let d = s.doc().unwrap();
|
||||
assert!(!d.is_visible(d.entity(off).unwrap()), "the second line must be on a layer that is off");
|
||||
s.set_selection(vec![shown, off]);
|
||||
let i = grip_at(&s, shown, Vec2::new(0.0, 0.0));
|
||||
s.start_grip_edit(&[(shown, i)]).unwrap();
|
||||
s.cmdline("3,4").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let (start, end) = ends(&s, shown);
|
||||
assert!(start.near(Vec2::new(3.0, 4.0), 1e-9) && end.near(Vec2::new(10.0, 0.0), 1e-9), "{start:?} {end:?}");
|
||||
let (start, end) = ends(&s, off);
|
||||
assert!(
|
||||
start.near(Vec2::new(0.0, 0.0), 1e-9) && end.near(Vec2::new(0.0, 10.0), 1e-9),
|
||||
"the line on the layer that is off moved: {start:?} {end:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The typed GRIP command finds no grip where only a hidden object has one, and at the shared
|
||||
/// corner it makes only the visible line's grip hot.
|
||||
#[test]
|
||||
fn the_typed_grip_command_finds_no_grip_of_a_hidden_object() {
|
||||
let (mut s, shown, hidden) = shared_corner();
|
||||
s.cmdline("grip").unwrap();
|
||||
assert!(s.prompt_text().contains("Select grip"), "{}", s.prompt_text());
|
||||
// Only the hidden line has a grip at (0, 10): the command says so and asks again.
|
||||
s.cmdline("0,10").unwrap();
|
||||
assert!(s.log.iter().any(|l| l.contains("No grip of a selected object there")), "{:?}", s.log);
|
||||
s.cmdline("0,0").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
assert!(s.prompt_text().contains("** STRETCH **"), "{}", s.prompt_text());
|
||||
s.cmdline("3,4").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let (start, end) = ends(&s, shown);
|
||||
assert!(start.near(Vec2::new(3.0, 4.0), 1e-9) && end.near(Vec2::new(10.0, 0.0), 1e-9), "{start:?} {end:?}");
|
||||
let (start, end) = ends(&s, hidden);
|
||||
assert!(start.near(Vec2::new(0.0, 0.0), 1e-9) && end.near(Vec2::new(0.0, 10.0), 1e-9), "the hidden line moved: {start:?} {end:?}");
|
||||
}
|
||||
@@ -62,6 +62,7 @@ mod hatch_dialog_helpers;
|
||||
mod hatch_numeric_validation;
|
||||
mod hatch_options;
|
||||
mod helix;
|
||||
mod hidden_grip_targets;
|
||||
mod inquiry_keywords;
|
||||
mod inquiry_variables;
|
||||
mod inquiry_z;
|
||||
|
||||
@@ -13,7 +13,7 @@ cadcraft-geom = { workspace = true }
|
||||
cadcraft-color = { workspace = true }
|
||||
cadcraft-doc = { workspace = true }
|
||||
craft-dxf = { workspace = true }
|
||||
cadcraft-dwg = { workspace = true }
|
||||
craft-dwg = { workspace = true }
|
||||
cadcraft-render = { workspace = true }
|
||||
cadcraft-fonts = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -29,7 +29,7 @@ tiny-skia = { workspace = true }
|
||||
# Reads plotted PDFs back.
|
||||
craft-pdf = { workspace = true, features = ["read"] }
|
||||
|
||||
# Inspect DWG output only where cadcraft-dwg builds its native bridge. An unconditional
|
||||
# Inspect DWG output only where craft-dwg builds its native bridge. An unconditional
|
||||
# dev-dependency also builds acadrust during --all-targets, breaking FreeBSD aarch64 (#2526).
|
||||
[target.'cfg(not(any(target_arch = "wasm32", all(target_os = "freebsd", target_arch = "aarch64"))))'.dev-dependencies]
|
||||
acadrust = "0.6.6"
|
||||
|
||||
+23
-10
@@ -61,11 +61,24 @@ pub enum IoError {
|
||||
|
||||
pub type Result<T> = std::result::Result<T, IoError>;
|
||||
|
||||
/// How a program runs the DWG reader and writer in a child process (`cadcraft_dwg::worker`).
|
||||
/// Restore CADCraft's public diagnostics after the shared byte bridge supplies generic wording.
|
||||
fn cadcraft_dwg_message(message: &str) -> String {
|
||||
message
|
||||
.replace("accepted by this bridge", "CADCraft opens")
|
||||
.replace("types this bridge can't preserve", "types CADCraft doesn't keep")
|
||||
.replace("this bridge writes", "CADCraft writes")
|
||||
.replace("accepted for DWG conversion", "CADCraft converts to DWG")
|
||||
}
|
||||
|
||||
fn dwg_error(message: String) -> IoError {
|
||||
IoError::Format(cadcraft_dwg_message(&message))
|
||||
}
|
||||
|
||||
/// How a program runs the DWG reader and writer in a child process (`craft_dwg::worker`).
|
||||
pub mod dwg_worker {
|
||||
pub use cadcraft_dwg::{WORKER_ARG, serve_if_asked, use_own_worker};
|
||||
pub use craft_dwg::{WORKER_ARG, serve_if_asked, use_own_worker};
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(all(target_os = "freebsd", target_arch = "aarch64"))))]
|
||||
pub use cadcraft_dwg::{crash_worker_for_test, use_worker};
|
||||
pub use craft_dwg::{crash_worker_for_test, use_worker};
|
||||
}
|
||||
|
||||
fn ext(name: impl AsRef<Path>) -> String {
|
||||
@@ -81,10 +94,10 @@ pub fn read(bytes: &[u8], name: impl AsRef<Path>) -> Result<Drawing> {
|
||||
/// [`read`], also returning the DXF rendition a DWG was read through (`None` for other files),
|
||||
/// which the round-trip harness counts.
|
||||
pub(crate) fn read_source(bytes: &[u8], name: &Path) -> Result<(Drawing, Option<Vec<u8>>)> {
|
||||
if cadcraft_dwg::is_dwg(bytes) {
|
||||
let (dxf, notes) = cadcraft_dwg::decode(bytes, cadcraft_dwg::Limits::DEFAULT).map_err(IoError::Format)?;
|
||||
if craft_dwg::is_dwg(bytes) {
|
||||
let (dxf, notes) = craft_dwg::decode(bytes, craft_dwg::Limits::DEFAULT).map_err(dwg_error)?;
|
||||
let mut d = dxf_read::read(&dxf).map_err(|e| IoError::Format(format!("{e} (in the DXF converted from this DWG)")))?;
|
||||
d.read_notes = notes;
|
||||
d.read_notes = notes.into_iter().map(|note| cadcraft_dwg_message(¬e)).collect();
|
||||
return Ok((d, Some(dxf)));
|
||||
}
|
||||
let d = match (Format::from_path(name), ext(name).as_str()) {
|
||||
@@ -132,21 +145,21 @@ pub fn write_with(d: &Drawing, name: impl AsRef<Path>, opts: &SaveOptions) -> Re
|
||||
/// [`write_with`], also returning what the file leaves out of the drawing: for a DWG, how many
|
||||
/// records of each DXF type the DWG writer can't write (other programs' custom entities and
|
||||
/// objects); for other formats, nothing.
|
||||
pub fn write_noted(d: &Drawing, name: impl AsRef<Path>, opts: &SaveOptions) -> Result<(Vec<u8>, cadcraft_dwg::Unwritten)> {
|
||||
pub fn write_noted(d: &Drawing, name: impl AsRef<Path>, opts: &SaveOptions) -> Result<(Vec<u8>, craft_dwg::Unwritten)> {
|
||||
let name = name.as_ref();
|
||||
if matches!(Format::from_path(name), Some(Format::Dwg)) && !opts.binary {
|
||||
return write_dwg(d, opts.version.unwrap_or_else(|| save_version(d).max(Version::R2004)));
|
||||
}
|
||||
write_with(d, name, opts).map(|b| (b, cadcraft_dwg::Unwritten::new()))
|
||||
write_with(d, name, opts).map(|b| (b, craft_dwg::Unwritten::new()))
|
||||
}
|
||||
|
||||
/// The DWG bridge reads our R2000 DXF and writes the DWG at `version`, in the DWG worker when
|
||||
/// there is one. The objects CADCraft keeps without modelling them are still in the groups of the
|
||||
/// file they came from (`Foreign::version`, not `$ACADVER`, which SAVEAS changes), and the bridge
|
||||
/// reads them as that version.
|
||||
fn write_dwg(d: &Drawing, version: Version) -> Result<(Vec<u8>, cadcraft_dwg::Unwritten)> {
|
||||
fn write_dwg(d: &Drawing, version: Version) -> Result<(Vec<u8>, craft_dwg::Unwritten)> {
|
||||
let bridge = dxf_write::dwg_bridge(d, version);
|
||||
cadcraft_dwg::encode_kept_noted(bridge.as_bytes(), version.code(), d.foreign.version.as_deref()).map_err(IoError::Format)
|
||||
craft_dwg::encode_kept_noted(bridge.as_bytes(), version.code(), d.foreign.version.as_deref()).map_err(dwg_error)
|
||||
}
|
||||
|
||||
/// Like [`write`], but image formats (PNG, SVG, PDF) show `window` (a model-space rectangle in
|
||||
|
||||
@@ -36,7 +36,7 @@ impl Format {
|
||||
}
|
||||
/// The format of a file's bytes: DWG by its magic, otherwise DXF.
|
||||
pub fn of(bytes: &[u8]) -> Format {
|
||||
if cadcraft_dwg::is_dwg(bytes) { Format::Dwg } else { Format::Dxf }
|
||||
if craft_dwg::is_dwg(bytes) { Format::Dwg } else { Format::Dxf }
|
||||
}
|
||||
fn other(self) -> Format {
|
||||
match self {
|
||||
@@ -279,7 +279,7 @@ pub fn check_bytes(bytes: &[u8], rel: &str, opts: &CorpusOptions) -> FileReport
|
||||
let format = Format::of(bytes);
|
||||
let mut f = FileReport { path: rel.to_string(), format: Some(format), bytes: bytes.len() as u64, ..FileReport::default() };
|
||||
if format == Format::Dwg {
|
||||
f.version = cadcraft_dwg::version(bytes).unwrap_or_default();
|
||||
f.version = craft_dwg::version(bytes).unwrap_or_default();
|
||||
}
|
||||
let start = Instant::now();
|
||||
let name = format!("source.{}", format.ext());
|
||||
|
||||
+42
-22
@@ -797,7 +797,7 @@ fn landscape_media_keeps_its_size_order_and_rotation() {
|
||||
assert_eq!(p.paper, media);
|
||||
|
||||
let dwg = write(&d, "t.dwg").unwrap();
|
||||
let back = String::from_utf8_lossy(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).into_owned();
|
||||
let back = String::from_utf8_lossy(&craft_dwg::dwg_to_dxf(&dwg).unwrap()).into_owned();
|
||||
assert_eq!(plot_paper_groups(&back, "Layout1"), (420.0, 297.0, rotation), "DWG, landscape {landscape}");
|
||||
let r = read(&dwg, "t.dwg").unwrap();
|
||||
let p = &r.layout("Layout1").unwrap().page;
|
||||
@@ -844,7 +844,7 @@ fn a_files_own_paper_groups_are_written_back() {
|
||||
assert_eq!((p.width_mm, p.height_mm, p.landscape), (pw, ph, landscape), "{media} {groups:?} read");
|
||||
assert_eq!(plot_paper_groups(&write_dxf(&r), "Layout1"), groups, "{media} {groups:?} DXF");
|
||||
let dwg = write(&r, "t.dwg").unwrap();
|
||||
let back = String::from_utf8_lossy(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).into_owned();
|
||||
let back = String::from_utf8_lossy(&craft_dwg::dwg_to_dxf(&dwg).unwrap()).into_owned();
|
||||
assert_eq!(plot_paper_groups(&back, "Layout1"), groups, "{media} {groups:?} DWG");
|
||||
let r = read(&dwg, "t.dwg").unwrap();
|
||||
assert_eq!(plot_paper_groups(&write_dxf(&r), "Layout1"), groups, "{media} {groups:?} DWG read and saved again");
|
||||
@@ -1234,7 +1234,7 @@ fn viewport_frozen_layers_survive_dwg() {
|
||||
let back = read(&dwg, "t.dwg").unwrap();
|
||||
assert_eq!(viewport_frozen(&back, 2), vec!["Walls".to_string(), "A B".to_string()]);
|
||||
// The DWG holds them in group 331, what AutoCAD reads.
|
||||
let p = pairs(&String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap());
|
||||
let p = pairs(&String::from_utf8(craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap());
|
||||
assert_eq!(frozen_331(&p), [layer_handle(&p, "Walls"), layer_handle(&p, "A B")]);
|
||||
}
|
||||
|
||||
@@ -1376,8 +1376,8 @@ fn generated_arrowhead_blocks_are_not_kept_as_user_blocks() {
|
||||
for (_, name) in crate::dxf_ext::ARROWS.iter().filter(|(k, _)| *k != cadcraft_render::Arrowhead::ClosedFilled) {
|
||||
assert!(dxf.contains(&format!("\r\n{name}\r\n")), "{name} is written");
|
||||
}
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let from_dwg = read_dxf(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let from_dwg = read_dxf(&craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
for (back, what) in [(read_dxf(dxf.as_bytes()).unwrap(), "DXF"), (from_dwg, "DWG")] {
|
||||
assert!(back.blocks.keys().all(|k| !k.starts_with('_')), "{what}: {:?}", back.blocks.keys().collect::<Vec<_>>());
|
||||
assert!(roundtrip(&back).blocks.keys().all(|k| !k.starts_with('_')), "{what}: second save");
|
||||
@@ -1408,8 +1408,8 @@ fn file_arrowhead_blocks_with_standard_names_are_kept() {
|
||||
d.dim_styles.push(DimStyle { name: "Dots".into(), arrow_block1: "_Dot".into(), arrow_block2: "_DotSmall".into(), ..DimStyle::default() });
|
||||
let kinds = |d: &Drawing, name: &str| d.block(name).map(|b| b.entities.iter().map(|e| e.kind.clone()).collect::<Vec<_>>()).unwrap_or_default();
|
||||
let dxf = write_dxf(&d);
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let from_dwg = read_dxf(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let from_dwg = read_dxf(&craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
for (back, what) in [(read_dxf(dxf.as_bytes()).unwrap(), "DXF"), (from_dwg, "DWG")] {
|
||||
for name in ["_Dot", "_DotSmall"] {
|
||||
assert_eq!(kinds(&back, name), kinds(&d, name), "{what}: {name}");
|
||||
@@ -1479,8 +1479,8 @@ fn many_entities(n: usize) -> Drawing {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn ac1024_dwg(n: usize) -> Vec<u8> {
|
||||
let dxf = write_dxf(&many_entities(n)).replacen("AC1015", "AC1024", 1);
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
assert_eq!(cadcraft_dwg::version(&dwg).as_deref(), Some("AC1024"));
|
||||
let dwg = craft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
assert_eq!(craft_dwg::version(&dwg).as_deref(), Some("AC1024"));
|
||||
dwg
|
||||
}
|
||||
|
||||
@@ -1492,7 +1492,7 @@ fn ac1024_dwg(n: usize) -> Vec<u8> {
|
||||
fn ac1024_dwg_with_many_entities_opens() {
|
||||
let n = 30_000;
|
||||
let dwg = ac1024_dwg(n);
|
||||
let dxf = cadcraft_dwg::dwg_to_dxf(&dwg).unwrap();
|
||||
let dxf = craft_dwg::dwg_to_dxf(&dwg).unwrap();
|
||||
let tags = craft_dxf::parse(&dxf).unwrap().len();
|
||||
assert!(dxf.len() < dwg.len() * 20, "DXF {} bytes from {} DWG bytes", dxf.len(), dwg.len());
|
||||
assert!(tags < dwg.len(), "{tags} group codes from {} DWG bytes", dwg.len());
|
||||
@@ -1509,16 +1509,16 @@ fn ac1024_dwg_with_many_entities_opens() {
|
||||
#[test]
|
||||
fn oversized_dwg_and_dxf_are_rejected_early() {
|
||||
let dwg = ac1024_dwg(3000);
|
||||
let full = cadcraft_dwg::dwg_to_dxf(&dwg).unwrap().len();
|
||||
let full = craft_dwg::dwg_to_dxf(&dwg).unwrap().len();
|
||||
assert!(full > 400_000, "{full}");
|
||||
let limits = cadcraft_dwg::Limits { max_dxf_bytes: 300_000, ..cadcraft_dwg::Limits::DEFAULT };
|
||||
let e = cadcraft_dwg::dwg_to_dxf_with(&dwg, limits).unwrap_err();
|
||||
let limits = craft_dwg::Limits { max_dxf_bytes: 300_000, ..craft_dwg::Limits::DEFAULT };
|
||||
let e = craft_dwg::dwg_to_dxf_with(&dwg, limits).unwrap_err();
|
||||
assert!(e.starts_with("DWG: converting this 0."), "{e}");
|
||||
assert!(e.contains("MB file produced more than 0.3 MB of drawing data (the limit)"), "{e}");
|
||||
|
||||
let limits = cadcraft_dwg::Limits { max_dwg_bytes: 100_000, ..cadcraft_dwg::Limits::DEFAULT };
|
||||
let e = cadcraft_dwg::dwg_to_dxf_with(&dwg, limits).unwrap_err();
|
||||
assert!(e.starts_with("DWG: the file is 0.") && e.ends_with("MB, larger than the 0.1 MB CADCraft opens"), "{e}");
|
||||
let limits = craft_dwg::Limits { max_dwg_bytes: 100_000, ..craft_dwg::Limits::DEFAULT };
|
||||
let e = craft_dwg::dwg_to_dxf_with(&dwg, limits).unwrap_err();
|
||||
assert!(e.starts_with("DWG: the file is 0.") && e.ends_with("MB, larger than the 0.1 MB accepted by this bridge"), "{e}");
|
||||
|
||||
// Lines that aren't even group codes: the size is reported before anything is parsed.
|
||||
let text = "not a group code\nx\n".repeat(60);
|
||||
@@ -1526,6 +1526,26 @@ fn oversized_dwg_and_dxf_are_rejected_early() {
|
||||
assert_eq!(e.to_string(), "drawing too large: 60 DXF group codes, more than the 50 CADCraft reads (set CADCRAFT_MAX_DXF_TAGS to read more)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_dwg_diagnostics_keep_cadcraft_vocabulary() {
|
||||
assert_eq!(
|
||||
cadcraft_dwg_message("DWG: the file is 0.1 MB, larger than the 0.1 MB accepted by this bridge"),
|
||||
"DWG: the file is 0.1 MB, larger than the 0.1 MB CADCraft opens"
|
||||
);
|
||||
assert_eq!(
|
||||
cadcraft_dwg_message("DWG: 2 objects of types this bridge can't preserve were left out (FOO × 2)"),
|
||||
"DWG: 2 objects of types CADCraft doesn't keep were left out (FOO × 2)"
|
||||
);
|
||||
assert_eq!(
|
||||
cadcraft_dwg_message("DWG version `AC1015` can't be written (this bridge writes AC1018+)"),
|
||||
"DWG version `AC1015` can't be written (CADCraft writes AC1018+)"
|
||||
);
|
||||
assert_eq!(
|
||||
cadcraft_dwg_message("DXF: the input is 1024 MB, larger than the 1024 MB accepted for DWG conversion"),
|
||||
"DXF: the input is 1024 MB, larger than the 1024 MB CADCraft converts to DWG"
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue #800: one unreadable object record doesn't fail the whole DWG. The reader skips it, the
|
||||
/// rest opens, and the drawing's read notes say how many records were left out and of which
|
||||
/// types, so a partial read never passes for a complete one. A complete read has no notes.
|
||||
@@ -2336,8 +2356,8 @@ fn arc_length_dimension_keeps_its_kind_and_centre_on_roundtrip() {
|
||||
_ => panic!("not a dimension"),
|
||||
}
|
||||
// And through DWG.
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(text.as_bytes()).unwrap();
|
||||
let back = read_dxf(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg(text.as_bytes()).unwrap();
|
||||
let back = read_dxf(&craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let dims: Vec<_> = back.model.iter().filter_map(|e| if let EntityKind::Dimension(x) = &e.kind { Some(x.clone()) } else { None }).collect();
|
||||
assert_eq!(dims.len(), 1);
|
||||
assert!(matches!(dims[0].kind, DimKind::ArcLength), "{:?}", dims[0].kind);
|
||||
@@ -3568,7 +3588,7 @@ fn proxies_keep_their_class_in_dwg() {
|
||||
fn a_proxy_whose_class_is_unknown_is_left_out_of_the_dwg() {
|
||||
use acadrust::entities::{EntityType, ExtendedEntityData};
|
||||
use acadrust::objects::ObjectType;
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg_as(&proxy_file(), "AC1032").unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg_as(&proxy_file(), "AC1032").unwrap();
|
||||
let doc = acadrust::DwgReader::from_stream(std::io::Cursor::new(dwg)).read().unwrap();
|
||||
let class_of = |id: i32| doc.classes.iter().find(|c| i32::from(c.class_number) == id).map(|c| c.dxf_name.clone());
|
||||
let mut objects: Vec<Option<String>> = doc
|
||||
@@ -3599,11 +3619,11 @@ fn a_proxy_whose_class_is_unknown_is_left_out_of_the_dwg() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn a_proxy_left_out_of_the_dwg_is_counted_as_unwritten() {
|
||||
let (_, unwritten) = cadcraft_dwg::dxf_to_dwg_noted(&proxy_file(), "AC1032").unwrap();
|
||||
let (_, unwritten) = craft_dwg::dxf_to_dwg_noted(&proxy_file(), "AC1032").unwrap();
|
||||
assert_eq!(unwritten, [("ACAD_PROXY_OBJECT".to_string(), 1)].into_iter().collect());
|
||||
let d = read(&proxy_file(), "p.dxf").unwrap();
|
||||
let binary = write_with(&d, "p.dxf", &SaveOptions { version: Some(Version::R2018), binary: true }).unwrap();
|
||||
let (_, unwritten) = cadcraft_dwg::dxf_to_dwg_noted(&binary, "AC1032").unwrap();
|
||||
let (_, unwritten) = craft_dwg::dxf_to_dwg_noted(&binary, "AC1032").unwrap();
|
||||
// CADCraft's own save already left the stray proxy out; the binary DXF's other three go.
|
||||
assert_eq!(unwritten, [("ACAD_PROXY_ENTITY".to_string(), 1), ("ACAD_PROXY_OBJECT".to_string(), 2)].into_iter().collect());
|
||||
}
|
||||
@@ -3620,7 +3640,7 @@ fn a_binary_dxf_leaves_its_proxies_out_of_the_dwg() {
|
||||
let d = read(&proxy_file(), "p.dxf").unwrap();
|
||||
let binary = write_with(&d, "p.dxf", &SaveOptions { version: Some(Version::R2018), binary: true }).unwrap();
|
||||
assert!(binary.starts_with(b"AutoCAD Binary DXF"));
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg_as(&binary, "AC1032").unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg_as(&binary, "AC1032").unwrap();
|
||||
let doc = acadrust::DwgReader::from_stream(std::io::Cursor::new(dwg)).read().unwrap();
|
||||
let proxies = doc.objects.values().filter(|o| matches!(o, ObjectType::ProxyObject(_))).count()
|
||||
+ doc.entities().filter(|e| matches!(e, EntityType::Extended(x) if matches!(x.data, ExtendedEntityData::Proxy(_)))).count();
|
||||
|
||||
@@ -285,7 +285,7 @@ fn standard_dimassoc_associates_in_dxf_and_dwg() {
|
||||
let back = read_dxf(plain.as_bytes()).unwrap();
|
||||
assert_standard_assoc(&c, &back, "DXF");
|
||||
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg_as(plain.as_bytes(), Version::R2018.code()).unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg_as(plain.as_bytes(), Version::R2018.code()).unwrap();
|
||||
let back = read(&dwg, "case.dwg").unwrap();
|
||||
assert_standard_assoc(&c, &back, "DWG");
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ fn an_ac1018_dwg_gets_the_variable_dictionary_autocad_always_writes() {
|
||||
#[test]
|
||||
fn the_dictionary_keeps_the_drawings_dimassoc_and_hidetext() {
|
||||
let dxf = "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$DIMASSOC\n70\n1\n9\n$HIDETEXT\n70\n0\n0\nENDSEC\n0\nEOF\n";
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg_as(dxf.as_bytes(), "AC1018").expect("write the DWG");
|
||||
let dwg = craft_dwg::dxf_to_dwg_as(dxf.as_bytes(), "AC1018").expect("write the DWG");
|
||||
let doc = read_dwg(&dwg);
|
||||
let vars = variable_dictionary(&doc).expect("AC1018 DWG: the named object dictionary has an AcDbVariableDictionary");
|
||||
assert_eq!(value(&vars, "DIMASSOC"), Some("1"), "DIMASSOC in {vars:?}");
|
||||
@@ -107,7 +107,7 @@ fn the_dictionary_keeps_the_drawings_dimassoc_and_hidetext() {
|
||||
#[test]
|
||||
fn impossible_header_values_fall_back_to_the_defaults() {
|
||||
let dxf = "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$DIMASSOC\n70\n9\n9\n$HIDETEXT\n70\n7\n0\nENDSEC\n0\nEOF\n";
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg_as(dxf.as_bytes(), "AC1018").expect("write the DWG");
|
||||
let dwg = craft_dwg::dxf_to_dwg_as(dxf.as_bytes(), "AC1018").expect("write the DWG");
|
||||
let doc = read_dwg(&dwg);
|
||||
let vars = variable_dictionary(&doc).expect("AC1018 DWG: the named object dictionary has an AcDbVariableDictionary");
|
||||
assert_eq!(value(&vars, "DIMASSOC"), Some("2"), "DIMASSOC in {vars:?}");
|
||||
@@ -139,7 +139,7 @@ fn a_variable_dictionary_the_drawing_has_is_left_alone() {
|
||||
assert!(source.set_drawing_variable("CMLEADERSTYLE", "Notes"));
|
||||
let dxf = acadrust::DxfWriter::new(&source).write_to_vec().expect("write a DXF with the dictionary");
|
||||
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg_as(&dxf, "AC1018").expect("write the DWG");
|
||||
let dwg = craft_dwg::dxf_to_dwg_as(&dxf, "AC1018").expect("write the DWG");
|
||||
let doc = read_dwg(&dwg);
|
||||
let vars = variable_dictionary(&doc).expect("the drawing's dictionary is kept");
|
||||
assert_eq!(vars.len(), 1, "{vars:?}");
|
||||
|
||||
@@ -155,7 +155,7 @@ fn a_rotated_linear_dimension_stores_its_projected_length() {
|
||||
Vec3::new(-40.0, 105.0, 0.0),
|
||||
);
|
||||
let dwg = write_with(&d, "case.dwg", &SaveOptions { version: Some(Version::R2018), binary: false }).unwrap();
|
||||
let dxf = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let dxf = String::from_utf8(craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let t = tags(&dxf);
|
||||
let dim = t.iter().position(|(c, v)| *c == 0 && v == "DIMENSION").unwrap();
|
||||
let m: f64 = t[dim..].iter().find(|(c, _)| *c == 42).unwrap().1.parse().unwrap();
|
||||
@@ -242,7 +242,7 @@ fn justified_text_made_here_writes_its_baseline_start_for_every_justification()
|
||||
/// Group 42 of a dimension in the DXF AutoCAD reads from `d`'s DWG.
|
||||
fn stored_measurement(d: &Drawing) -> f64 {
|
||||
let dwg = write_with(d, "case.dwg", &SaveOptions { version: Some(Version::R2018), binary: false }).unwrap();
|
||||
let dxf = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let dxf = String::from_utf8(craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let t = tags(&dxf);
|
||||
let dim = t.iter().position(|(c, v)| *c == 0 && v == "DIMENSION").unwrap();
|
||||
t[dim..].iter().find(|(c, _)| *c == 42).unwrap().1.parse().unwrap()
|
||||
|
||||
@@ -93,7 +93,7 @@ fn dwg_keeps_the_solids_geometry() {
|
||||
let d = cadcraft_io::read_dxf(dxf().as_bytes()).unwrap();
|
||||
let dwg = cadcraft_io::write(&d, "x.dwg").unwrap();
|
||||
// The DWG read back (as the bridge's DXF) binds the same bytes to both solids.
|
||||
let back = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let back = String::from_utf8(craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let mut got = asm_data(&back);
|
||||
got.sort();
|
||||
assert_eq!(got, vec![("2A".to_string(), SAB_HEX.to_string()), ("2B".to_string(), SAB_HEX.to_string())]);
|
||||
|
||||
@@ -248,7 +248,7 @@ fn hostile_multibyte_never_panics() {
|
||||
#[test]
|
||||
fn pre_2007_dwg_text_decodes_through_the_bridge() {
|
||||
let dxf = ascii_dxf("AC1015", "ANSI_1251", b"\xD1\xEB\xEE\xE9", b"\xC1\xEB\xEE\xEA", b"\xCF\xF0\xE8\xE2\xE5\xF2");
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(&dxf).unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg(&dxf).unwrap();
|
||||
let d = cadcraft_io::read(&dwg, "x.dwg").unwrap();
|
||||
assert_eq!(names(&d), ("Слой".into(), "Блок".into(), "Привет".into()));
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ fn dwg_keeps_true_colours_and_transparency() {
|
||||
);
|
||||
|
||||
let dwg = cadcraft_io::write(&d, "x.dwg").unwrap();
|
||||
assert_eq!(cadcraft_dwg::version(&dwg).as_deref(), Some("AC1032"));
|
||||
assert_eq!(craft_dwg::version(&dwg).as_deref(), Some("AC1032"));
|
||||
let header = records(&cadcraft_io::write_dxf(&d), "SECTION");
|
||||
assert_eq!(header.first().and_then(|h| group(h, "1")), Some("AC1015"), "the DXF stays R2000");
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ fn dwg_at_every_version_reads_back() {
|
||||
let d = sample();
|
||||
for v in Version::ALL {
|
||||
let dwg = write_with(&d, "x.dwg", &opts(v, false)).unwrap();
|
||||
assert_eq!(cadcraft_dwg::version(&dwg).as_deref(), Some(v.code()), "{v}");
|
||||
assert_eq!(craft_dwg::version(&dwg).as_deref(), Some(v.code()), "{v}");
|
||||
let back = read(&dwg, "x.dwg").unwrap();
|
||||
assert_eq!(save_version(&back), v, "{v}");
|
||||
assert_eq!(back.model.len(), d.model.len(), "{v}");
|
||||
@@ -187,6 +187,6 @@ fn dwg_at_every_version_reads_back() {
|
||||
#[test]
|
||||
fn dwg_default_is_at_least_2004() {
|
||||
let from_r2000 = read(write_dxf(&sample()).as_bytes(), "x.dxf").unwrap();
|
||||
assert_eq!(cadcraft_dwg::version(&write(&from_r2000, "x.dwg").unwrap()).as_deref(), Some("AC1018"));
|
||||
assert_eq!(cadcraft_dwg::version(&write(&sample(), "x.dwg").unwrap()).as_deref(), Some("AC1032"));
|
||||
assert_eq!(craft_dwg::version(&write(&from_r2000, "x.dwg").unwrap()).as_deref(), Some("AC1018"));
|
||||
assert_eq!(craft_dwg::version(&write(&sample(), "x.dwg").unwrap()).as_deref(), Some("AC1032"));
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ fn with_scale_action() -> String {
|
||||
|
||||
/// A scale action goes through CADCraft's own DWG save (the bridge DXF, then acadrust) whole:
|
||||
/// the DWG holds it with its connections, and it reads back as the same action. AutoCAD refused
|
||||
/// such DWGs while acadrust left out the byte after its connections (cadcraft-dwg's
|
||||
/// such DWGs while acadrust left out the byte after its connections (craft-dwg's
|
||||
/// `scale_actions_keep_the_byte_autocad_reads_after_their_connections` covers the byte).
|
||||
#[cfg(not(any(target_arch = "wasm32", all(target_os = "freebsd", target_arch = "aarch64"))))]
|
||||
#[test]
|
||||
|
||||
@@ -59,7 +59,7 @@ fn model_layout_plots_extents_fitted_and_layouts_their_sheet() {
|
||||
fn model_layout_plot_type_survives_the_dwg_bridge() {
|
||||
let dwg = cadcraft_io::write(&drawing(), "x.dwg").unwrap();
|
||||
// What the DWG holds, as the bridge's own DXF.
|
||||
let dxf = String::from_utf8_lossy(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).to_string();
|
||||
let dxf = String::from_utf8_lossy(&craft_dwg::dwg_to_dxf(&dwg).unwrap()).to_string();
|
||||
let p = plots(&dxf);
|
||||
let model = p.iter().find(|(n, _, _)| n.eq_ignore_ascii_case("Model")).unwrap();
|
||||
assert_eq!(model.1, Some(1), "{p:?}");
|
||||
|
||||
@@ -82,7 +82,7 @@ fn dwg_mtext_caret_spaces_round_trip() -> TestResult {
|
||||
let mut d = original.clone();
|
||||
for pass in 1..=2 {
|
||||
let bytes = write_with(&d, "carets.dwg", &SaveOptions { version: Some(version), binary: false })?;
|
||||
assert_eq!(cadcraft_dwg::version(&bytes).as_deref(), Some(version.code()));
|
||||
assert_eq!(craft_dwg::version(&bytes).as_deref(), Some(version.code()));
|
||||
// Inspect the stored DWG text before its DXF rendition: a compensating reader
|
||||
// must not hide corruption on save. This uses the same codec, not an external oracle.
|
||||
let native = acadrust::DwgReader::from_stream(std::io::Cursor::new(&bytes)).read()?;
|
||||
@@ -93,7 +93,7 @@ fn dwg_mtext_caret_spaces_round_trip() -> TestResult {
|
||||
});
|
||||
assert_eq!(text, Some(contents), "native DWG {version:?}, pass={pass}, {handle:?}");
|
||||
}
|
||||
let dxf = cadcraft_dwg::dwg_to_dxf(&bytes)?;
|
||||
let dxf = craft_dwg::dwg_to_dxf(&bytes)?;
|
||||
let converted = read(&dxf, "bridge.dxf")?;
|
||||
assert_contents(&converted, &expected, &format!("DWG bridge DXF {version:?}, pass={pass}"))?;
|
||||
d = read(&bytes, "carets.dwg")?;
|
||||
@@ -112,7 +112,7 @@ fn bridge_caret_space_is_escaped_once_in_each_direction() -> TestResult {
|
||||
let native = acadrust::DxfReader::from_reader(std::io::Cursor::new(dxf.into_bytes()))?.read()?;
|
||||
assert!(native.entities().any(|e| matches!(e, acadrust::EntityType::MText(t) if t.value == "^ C")));
|
||||
let dwg = write_with(&d, "carets.dwg", &SaveOptions { version: Some(Version::R2018), binary: false })?;
|
||||
let returned = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg)?)?;
|
||||
let returned = String::from_utf8(craft_dwg::dwg_to_dxf(&dwg)?)?;
|
||||
assert!(returned.contains(" 1\r\n^ C\r\n"), "the return bridge must encode the separator independently of authored whitespace");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ fn dwg_keeps_ole_objects() {
|
||||
assert_eq!(ole(&d), 1);
|
||||
|
||||
let dwg = cadcraft_io::write(&d, "x.dwg").unwrap();
|
||||
let back = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let back = String::from_utf8(craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let got = frames(&back);
|
||||
assert_eq!(got.len(), 1, "{got:?}");
|
||||
assert_kept(&got[0]);
|
||||
@@ -99,14 +99,14 @@ fn dwg_keeps_ole_objects() {
|
||||
let again = cadcraft_io::read(&dwg, "x.dwg").unwrap();
|
||||
assert_eq!(ole(&again), 1);
|
||||
let dwg = cadcraft_io::write(&again, "x.dwg").unwrap();
|
||||
let got = frames(&String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap());
|
||||
let got = frames(&String::from_utf8(craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap());
|
||||
assert_eq!(got.len(), 1, "{got:?}");
|
||||
assert_kept(&got[0]);
|
||||
|
||||
// Older DWG versions keep it the same way.
|
||||
for version in [cadcraft_io::Version::R2000, cadcraft_io::Version::R2010] {
|
||||
let dwg = cadcraft_io::write_with(&d, "x.dwg", &cadcraft_io::SaveOptions { version: Some(version), binary: false }).unwrap();
|
||||
let got = frames(&String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap());
|
||||
let got = frames(&String::from_utf8(craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap());
|
||||
assert_eq!(got.len(), 1, "{version:?}: {got:?}");
|
||||
assert_kept(&got[0]);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ fn check_version_using(case: &str, original: &Drawing, expected: &Drawing, versi
|
||||
for pass in 1..=2 {
|
||||
let context = format!("{case} · DWG {version} · pass={pass}");
|
||||
let bytes = write_with(¤t, "memory.dwg", &options).unwrap_or_else(|e| panic!("{context}: write: {e}"));
|
||||
assert_eq!(cadcraft_dwg::version(&bytes).as_deref(), Some(version.code()), "{context}: wire version");
|
||||
assert_eq!(craft_dwg::version(&bytes).as_deref(), Some(version.code()), "{context}: wire version");
|
||||
let back = read(&bytes, "memory.dwg").unwrap_or_else(|e| panic!("{context}: read: {e}"));
|
||||
assert!(back.read_notes.is_empty(), "{context}: read notes: {:?}", back.read_notes);
|
||||
assert!(back.skipped_records.is_empty(), "{context}: skipped records: {:?}", back.skipped_records);
|
||||
|
||||
@@ -82,7 +82,7 @@ pub(super) fn check(path: &str, version: Version, binary: bool) {
|
||||
assert_eq!(craft_dxf::is_binary(&bytes), binary, "{context}: encoding");
|
||||
dxf_owner(&bytes);
|
||||
} else {
|
||||
assert_eq!(cadcraft_dwg::version(&bytes).as_deref(), Some(version.code()), "{context}: wire version");
|
||||
assert_eq!(craft_dwg::version(&bytes).as_deref(), Some(version.code()), "{context}: wire version");
|
||||
}
|
||||
let back = read(&bytes, path).unwrap();
|
||||
preserved(&original, &back, &context);
|
||||
|
||||
@@ -107,8 +107,8 @@ fn the_block_draw_order_survives_dxf_and_dwg() {
|
||||
/// The table itself (not CADCraft's reordered records) goes through the DWG bridge (#2410).
|
||||
#[test]
|
||||
fn a_partial_block_table_survives_the_dwg_bridge() {
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(block_dxf().as_bytes()).unwrap();
|
||||
let dxf = cadcraft_dwg::dwg_to_dxf(&dwg).unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg(block_dxf().as_bytes()).unwrap();
|
||||
let dxf = craft_dwg::dwg_to_dxf(&dwg).unwrap();
|
||||
let d = cadcraft_io::read_dxf(&dxf).unwrap();
|
||||
assert_eq!(block_kinds(&d), ["wipeout", "circle", "hline", "vline"]);
|
||||
}
|
||||
@@ -120,8 +120,8 @@ fn a_zero_sort_handle_draws_last() {
|
||||
let dxf = block_dxf_with("331\n25B\n5\n0\n331\n25D\n5\n25E\n331\n25E\n5\n25B\n");
|
||||
let want = ["wipeout", "circle", "vline", "hline"];
|
||||
assert_eq!(block_kinds(&cadcraft_io::read_dxf(dxf.as_bytes()).unwrap()), want, "DXF");
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let back = cadcraft_io::read_dxf(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let back = cadcraft_io::read_dxf(&craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
assert_eq!(block_kinds(&back), want, "DWG");
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ fn two_unlisted_block_entities_each_sort_by_their_own_handle() {
|
||||
let dxf = block_dxf_with("331\n25B\n5\n25B\n331\n25D\n5\n25D\n");
|
||||
let want = ["hline", "circle", "vline", "wipeout"];
|
||||
assert_eq!(block_kinds(&cadcraft_io::read_dxf(dxf.as_bytes()).unwrap()), want, "DXF");
|
||||
let dwg = cadcraft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let back = cadcraft_io::read_dxf(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
let dwg = craft_dwg::dxf_to_dwg(dxf.as_bytes()).unwrap();
|
||||
let back = cadcraft_io::read_dxf(&craft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
|
||||
assert_eq!(block_kinds(&back), want, "DWG");
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ fn check(bytes: &[u8], name: &str) -> cadcraft_doc::Drawing {
|
||||
})
|
||||
.unwrap();
|
||||
assert!(unknown.tags.iter().any(|t| t.code == 340 && t.value == line.hex()));
|
||||
let dxf = if name.ends_with(".dwg") { cadcraft_dwg::dwg_to_dxf(bytes).unwrap() } else { bytes.to_vec() };
|
||||
let dxf = if name.ends_with(".dwg") { craft_dwg::dwg_to_dxf(bytes).unwrap() } else { bytes.to_vec() };
|
||||
let tags = craft_dxf::parse(&dxf).unwrap();
|
||||
let sections = craft_dxf::sections(&tags);
|
||||
let classes = sections.iter().find(|s| s.name == "CLASSES").unwrap();
|
||||
@@ -124,7 +124,7 @@ fn duplicate_names_keep_distinct_classes_for_entities_and_dictionary_objects() {
|
||||
for _ in 0..3 {
|
||||
for name in ["copy.dxf", "copy.dwg"] {
|
||||
let bytes = cadcraft_io::write_with(&drawing, name, &SaveOptions { version: Some(Version::R2018), ..Default::default() }).unwrap();
|
||||
let dxf = if name.ends_with(".dwg") { cadcraft_dwg::dwg_to_dxf(&bytes).unwrap() } else { bytes.clone() };
|
||||
let dxf = if name.ends_with(".dwg") { craft_dwg::dwg_to_dxf(&bytes).unwrap() } else { bytes.clone() };
|
||||
let tags = craft_dxf::parse(&dxf).unwrap();
|
||||
let sections = craft_dxf::sections(&tags);
|
||||
let classes = craft_dxf::records(§ions.iter().find(|s| s.name == "CLASSES").unwrap().tags);
|
||||
@@ -197,7 +197,7 @@ fn unused_malformed_classes_do_not_block_plain_geometry() {
|
||||
let dxf = format!(
|
||||
"0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1032\n0\nENDSEC\n0\nSECTION\n2\nCLASSES\n0\nCLASS\n1\n{name}\n2\n{cpp}\n3\nSynthetic\n90\n{flags}\n91\n{count}\n281\n1\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nLINE\n5\n10\n100\nAcDbEntity\n8\n0\n100\nAcDbLine\n10\n2\n20\n3\n11\n12\n21\n13\n0\nENDSEC\n0\nEOF\n"
|
||||
);
|
||||
let bytes = cadcraft_dwg::dxf_to_dwg_as(dxf.as_bytes(), "AC1032").unwrap();
|
||||
let bytes = craft_dwg::dxf_to_dwg_as(dxf.as_bytes(), "AC1032").unwrap();
|
||||
let drawing = cadcraft_io::read(&bytes, "plain.dwg").unwrap();
|
||||
let lines: Vec<_> = drawing.model.iter().filter_map(|e| if let EntityKind::Line(l) = &e.kind { Some(l) } else { None }).collect();
|
||||
assert_eq!(lines.len(), 1);
|
||||
@@ -219,7 +219,7 @@ fn proxy_spelling_is_preserved_in_dxf_and_normalized_only_for_dwg() {
|
||||
let tags = craft_dxf::parse(&out).unwrap();
|
||||
assert!(tags.iter().any(|t| t.code == 0 && t.str() == entity));
|
||||
let bytes = cadcraft_io::write_with(&drawing, "copy.dwg", &SaveOptions { version: Some(Version::R2018), ..Default::default() }).unwrap();
|
||||
let decoded = cadcraft_dwg::dwg_to_dxf(&bytes).unwrap();
|
||||
let decoded = craft_dwg::dwg_to_dxf(&bytes).unwrap();
|
||||
let tags = craft_dxf::parse(&decoded).unwrap();
|
||||
let proxies: Vec<_> =
|
||||
craft_dxf::sections(&tags).iter().flat_map(|s| craft_dxf::records(&s.tags)).filter(|(kind, _)| kind.starts_with("ACAD_PROXY_")).collect();
|
||||
@@ -283,7 +283,7 @@ fn ambiguous_fallback_classes_are_reported_without_losing_plain_geometry() {
|
||||
source = source.replace("1\nCUSTOM\n2\nSecond", "1\n\\U+0043USTOM\n2\nSecond");
|
||||
source = source.replace("1\nCUSTOM\n2\nObject", "1\nOTHER\n2\nObject");
|
||||
}
|
||||
let (bytes, unwritten) = cadcraft_dwg::dxf_to_dwg_noted(source.as_bytes(), "AC1032").unwrap();
|
||||
let (bytes, unwritten) = craft_dwg::dxf_to_dwg_noted(source.as_bytes(), "AC1032").unwrap();
|
||||
assert_eq!(unwritten.get("ACAD_PROXY_ENTITY"), Some(&2));
|
||||
assert_eq!(unwritten.get("ACAD_PROXY_OBJECT"), if escaped { None } else { Some(&1) });
|
||||
let drawing = cadcraft_io::read(&bytes, "copy.dwg").unwrap();
|
||||
|
||||
@@ -40,6 +40,8 @@ craft-ui = { workspace = true, features = ["native-windows"] }
|
||||
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
||||
# Validates the canvas WGSL and its GLSL ES 3.0 (WebGL2) translation in tests (same naga wgpu uses).
|
||||
naga = { version = "30", features = ["wgsl-in", "glsl-out"] }
|
||||
# Reads the synthetic dynamic-block fixture of the io tests for the dynamic grips tests.
|
||||
cadcraft-io = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1103,7 +1103,9 @@ fn paint_canvas(frame: CanvasPaint<'_>) {
|
||||
{
|
||||
let gs = app.session.settings.gripsize as f32;
|
||||
for h in sel.iter().take(200) {
|
||||
if let Some(e) = st.doc.entity(*h) {
|
||||
if let Some(e) = st.doc.entity(*h)
|
||||
&& st.doc.is_visible(e)
|
||||
{
|
||||
let grips = e.kind.grips();
|
||||
let count = grips.len();
|
||||
for (gi, g) in grips.into_iter().enumerate() {
|
||||
|
||||
@@ -3,6 +3,10 @@ use super::{CadApp, Handle, Input, Pos2, Rect, Xf, vec2};
|
||||
/// A click on grip `g` (idle): Shift+click makes it hot or cold again; a plain click starts
|
||||
/// editing it, with the other hot grips when it is one of them.
|
||||
pub(super) fn click_grip(app: &mut CadApp, g: (Handle, usize), shift: bool) {
|
||||
// A hot grip whose object was hidden since (a layer turned off, HIDEOBJECTS) is no longer
|
||||
// painted, so it must not be stretched along with the visible ones.
|
||||
let Ok(st) = app.session.state() else { return };
|
||||
app.canvas.hot_grips.retain(|(h, _)| st.doc.entity(*h).is_some_and(|e| st.doc.is_visible(e)));
|
||||
let hot = &mut app.canvas.hot_grips;
|
||||
if shift {
|
||||
match hot.iter().position(|x| *x == g) {
|
||||
@@ -85,7 +89,7 @@ pub(super) fn grip_at(app: &CadApp, xf: &Xf, hover: Option<Pos2>) -> Option<(Han
|
||||
}
|
||||
for h in sel.iter().take(200) {
|
||||
let e = st.doc.entity(*h)?;
|
||||
if st.doc.layer(&e.common.layer).is_some_and(|l| l.locked) {
|
||||
if !st.doc.is_visible(e) || st.doc.layer(&e.common.layer).is_some_and(|l| l.locked) {
|
||||
continue;
|
||||
}
|
||||
let grips = e.kind.grips();
|
||||
|
||||
@@ -230,3 +230,21 @@ fn dynamic_input_distance_follows_the_drawing_units() {
|
||||
let text = painted_text(&mut app, &ctx, at);
|
||||
assert!(text.contains(&format!(" {} <", arch(c.len()))), "no {} in:\n{text}", arch(c.len()));
|
||||
}
|
||||
|
||||
/// A hot grip whose object was hidden since (HIDEOBJECTS, a layer turned off) is no longer
|
||||
/// painted, so clicking another hot grip must not stretch the hidden object along with it.
|
||||
#[test]
|
||||
fn clicking_a_hot_grip_leaves_hidden_objects_out_of_the_stretch() {
|
||||
let mut app = CadApp::new(cadcraft_engine::Session::new(), crate::Services::default());
|
||||
for y in [0, 30] {
|
||||
app.run("line", serde_json::json!({ "points": [[0, y], [100, y]] })).unwrap();
|
||||
}
|
||||
let handles = app.session.doc().unwrap().model.handles();
|
||||
let (shown, hidden) = (handles[0], handles[1]);
|
||||
app.session.set_selection(vec![shown, hidden]);
|
||||
app.canvas.hot_grips = vec![(shown, 0), (hidden, 0)];
|
||||
app.run("hideobjects", serde_json::json!({ "handles": [hidden.hex()] })).unwrap();
|
||||
app.session.set_selection(vec![shown, hidden]);
|
||||
click_grip(&mut app, (shown, 0), false);
|
||||
assert_eq!(app.canvas.hot_grips, vec![(shown, 0)], "the hidden object's grip must leave the hot set");
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(crate) fn of_selection(app: &mut CadApp) -> Arc<Vec<(Handle, DynGrip)>> {
|
||||
let grips: Vec<(Handle, DynGrip)> = sel
|
||||
.iter()
|
||||
.filter(|h| matches!(st.doc.entity(**h).map(|e| &e.kind), Some(cadcraft_engine::doc::EntityKind::Insert(_))))
|
||||
.filter(|h| st.doc.entity(**h).is_some_and(|e| !st.doc.layer(&e.common.layer).is_some_and(|l| l.locked)))
|
||||
.filter(|h| st.doc.entity(**h).is_some_and(|e| st.doc.is_visible(e) && !st.doc.layer(&e.common.layer).is_some_and(|l| l.locked)))
|
||||
.take(MAX_REFERENCES)
|
||||
.flat_map(|h| cadcraft_engine::doc::dynblock::grips(&st.doc, *h).into_iter().map(move |g| (*h, g)))
|
||||
.collect();
|
||||
@@ -152,3 +152,61 @@ pub(crate) fn menu(app: &mut CadApp, ctx: &egui::Context, xf: &Xf) {
|
||||
app.canvas.dyn_menu = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{Handle, Space};
|
||||
use serde_json::json;
|
||||
|
||||
use super::of_selection;
|
||||
use crate::{CadApp, Services};
|
||||
|
||||
/// DXF text from lines of `code value`.
|
||||
fn dxf(src: &str) -> String {
|
||||
src.lines()
|
||||
.map(str::trim)
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|l| {
|
||||
let (code, value) = l.split_once(' ').unwrap_or((l, ""));
|
||||
format!("{code}\n{value}\n")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A session with the synthetic DOOR block of the io tests (Width's end grip shown in the
|
||||
/// Open state, plus the visibility grip) and a reference to it at (100, 0).
|
||||
fn door() -> (Session, Handle) {
|
||||
let grip =
|
||||
"0 BLOCKLINEARGRIP\n5 68\n330 61\n100 AcDbEvalExpr\n90 9\n98 33\n99 232\n100 AcDbBlockElement\n300 End Grip\n98 33\n99 232\n1071 0\n";
|
||||
let src = include_str!("../../io/tests/data/dynblock_door.txt")
|
||||
.replace("\r\n", "\n")
|
||||
.replace("170 4\n91 0\n91 0\n", "170 4\n91 8\n91 9\n")
|
||||
.replace("1030 0\n93 0\n170 0\n171 0\n100 AcDbBlockVisibilityParameter", "1030 0\n93 10\n170 0\n171 0\n100 AcDbBlockVisibilityParameter")
|
||||
.replace("91 5\n93 32\n95 6\n360 67", "91 5\n93 32\n95 6\n360 67\n92 -1\n92 -1\n92 -1\n92 -1\n91 6\n93 32\n95 9\n360 68")
|
||||
.replace("95 1\n333 63\n", "95 2\n333 63\n333 68\n")
|
||||
.replace("0 BLOCKFLIPGRIP", &format!("{grip}0 BLOCKFLIPGRIP"));
|
||||
let d = cadcraft_io::read_dxf(dxf(&src).as_bytes()).unwrap();
|
||||
let mut s = Session::empty();
|
||||
s.open_drawing(d, "door", None);
|
||||
let r = s.execute("insert", &json!({ "name": "Door", "at": [100, 0] })).unwrap();
|
||||
let h = Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap();
|
||||
assert!(s.doc().unwrap().space_of(h) == Some(Space::Model));
|
||||
(s, h)
|
||||
}
|
||||
|
||||
/// A hidden reference restored to the selection (Undo, or another selection source) shows
|
||||
/// and takes no dynamic grips, as ordinary grips are not shown for it either.
|
||||
#[test]
|
||||
fn a_hidden_reference_has_no_dynamic_grips() {
|
||||
let (session, h) = door();
|
||||
let mut app = CadApp::new(session, Services::default());
|
||||
app.session.set_selection(vec![h]);
|
||||
assert_eq!(of_selection(&mut app).len(), 2, "a visible reference shows its two grips");
|
||||
app.run("hideobjects", json!({ "handles": [h.hex()] })).unwrap();
|
||||
app.session.set_selection(vec![h]);
|
||||
let st = app.session.state().unwrap();
|
||||
assert!(!st.doc.is_visible(st.doc.entity(h).unwrap()), "the reference must be hidden");
|
||||
assert!(of_selection(&mut app).is_empty(), "a hidden reference must not offer dynamic grips");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//! An ordinary hidden object restored to the selection must not paint grips.
|
||||
|
||||
use cadcraft_engine::doc::Handle;
|
||||
use cadcraft_engine::geom::Vec2;
|
||||
use cadcraft_engine::{Session, View};
|
||||
use cadcraft_ui_egui::{CadApp, Services, theme::Tokens};
|
||||
use serde_json::json;
|
||||
|
||||
/// An app with two lines, the second hidden by HIDEOBJECTS, and an empty selection.
|
||||
fn drawing_with_hidden_object() -> (CadApp, Handle, Handle) {
|
||||
let mut app = CadApp::new(Session::new(), Services::default());
|
||||
app.ui.start_tab = false;
|
||||
app.ui.in_window_menu = false;
|
||||
app.integrated_titlebar = true;
|
||||
for y in [0, 30] {
|
||||
app.run("line", json!({ "points": [[0, y], [100, y]] })).unwrap();
|
||||
}
|
||||
let handles = app.session.doc().unwrap().model.handles();
|
||||
assert_eq!(handles.len(), 2);
|
||||
let (regular, hidden) = (handles[0], handles[1]);
|
||||
app.run("hideobjects", json!({ "handles": [hidden.hex()] })).unwrap();
|
||||
assert!(app.session.doc().unwrap().is_hidden_object(hidden));
|
||||
app.session.set_selection(Vec::new());
|
||||
(app, regular, hidden)
|
||||
}
|
||||
|
||||
/// The grip rectangles (filled with `color`) inside `canvas` among the painted shapes.
|
||||
fn count_grips(shape: &egui::Shape, canvas: egui::Rect, color: egui::Color32) -> usize {
|
||||
match shape {
|
||||
egui::Shape::Rect(rect) => usize::from(rect.fill == color && canvas.contains(rect.rect.center())),
|
||||
egui::Shape::Vec(shapes) => shapes.iter().map(|shape| count_grips(shape, canvas, color)).sum(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one frame of the app and counts the grip rectangles painted on its canvas.
|
||||
fn painted_grips(app: &mut CadApp, ctx: &egui::Context) -> usize {
|
||||
app.logic(ctx);
|
||||
let input = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0))),
|
||||
events: vec![egui::Event::PointerGone],
|
||||
..Default::default()
|
||||
};
|
||||
let mut output = ctx.run_ui(input, |ui| app.ui(ui));
|
||||
output.textures_delta.clear();
|
||||
let canvas = app.canvas.rect.unwrap();
|
||||
let color = Tokens::get().grip;
|
||||
output.shapes.iter().map(|shape| count_grips(&shape.shape, canvas, color)).sum()
|
||||
}
|
||||
|
||||
/// A selected ordinary line paints its grips; the same selection of a hidden line paints none.
|
||||
#[test]
|
||||
fn a_hidden_selection_paints_no_grips_while_an_ordinary_selection_does() {
|
||||
let (mut app, regular, hidden) = drawing_with_hidden_object();
|
||||
let ctx = egui::Context::default();
|
||||
// Settle the dock layout, then keep both objects comfortably within the canvas.
|
||||
painted_grips(&mut app, &ctx);
|
||||
painted_grips(&mut app, &ctx);
|
||||
assert!(app.session.state_mut().unwrap().set_view(View { center: Vec2::new(50.0, 15.0), height: 120.0, twist: 0.0 }));
|
||||
app.session.set_selection(vec![regular]);
|
||||
painted_grips(&mut app, &ctx);
|
||||
let ordinary_grips = painted_grips(&mut app, &ctx);
|
||||
let state = app.session.state().unwrap();
|
||||
let xf = app.canvas.xf.unwrap();
|
||||
assert!(state.doc.entity(regular).unwrap().kind.grips().iter().all(|point| xf.rect.contains(xf.to_screen(*point))));
|
||||
assert!(ordinary_grips >= 2, "the counter must find ordinary selected-line grips, got {ordinary_grips}");
|
||||
|
||||
// Undo/Redo or another public selection source can restore a still-hidden handle.
|
||||
app.session.set_selection(vec![hidden]);
|
||||
painted_grips(&mut app, &ctx);
|
||||
let hidden_grips = painted_grips(&mut app, &ctx);
|
||||
assert!(app.session.selection().contains(&hidden), "the forced hidden selection must reach the paint loop");
|
||||
let state = app.session.state().unwrap();
|
||||
assert!(!state.doc.is_visible(state.doc.entity(hidden).unwrap()));
|
||||
let xf = app.canvas.xf.unwrap();
|
||||
assert!(state.doc.entity(hidden).unwrap().kind.grips().iter().all(|point| xf.rect.contains(xf.to_screen(*point))));
|
||||
assert_eq!(hidden_grips, 0, "a hidden object leaked blue grip rectangles into the canvas");
|
||||
}
|
||||
@@ -20,7 +20,7 @@ About 57,000 lines of Rust in 12 library crates, 3 apps and `xtask` (counted 202
|
||||
| L2 | `render` | 3,300 | Display lists (polylines, triangles, points tagged with entity handles): linetypes, hatches, gradients, text, block expansion, dimension geometry, paper-space clipping; CPU rasteriser (`tiny-skia`) for PNG, plot preview and tests |
|
||||
| L2 | `constraints` | 2,504 | Own parametric solver: damped Gauss–Newton / Levenberg–Marquardt (`lm.rs`), 12 geometric and the dimensional constraints, expressions and parameters, AutoConstrain inference |
|
||||
| L3 | `io` | 5,124 | DXF ⇄ document mapping (`dxf_read.rs`, `dxf_write.rs`, CADCraft xdata in `dxf_ext.rs`), PDF plotting, SVG and PNG export |
|
||||
| L3 | `dwg` | 149 | DWG through a DXF bridge: the `acadrust` crate (MPL-2.0, unmodified) converts DWG ⇄ DXF bytes; nothing else sees its types. Native only |
|
||||
| L3 | `craft-dwg` | shared | DWG through the shared byte bridge: `acadrust` (MPL-2.0, unmodified) converts DWG ⇄ DXF bytes; the app keeps process isolation and maps DXF into its document model. Native only |
|
||||
| L4 | `engine` | 25,090 | `Session`, the command registry (`cmd/*`), command-line parser and prompt machines, object snaps and polar/ortho (`snap.rs`), selection (`select.rs`), grips (`grips.rs`), associativity (`assoc.rs`), system variables, units, undo, R-tree spatial index (`spatial.rs`) |
|
||||
| L5 | `ui-egui` | 9,849 | The swappable egui front end: GPU canvas (`gpu.rs`, wgpu), menus and shortcuts (`menus.rs`), command line, palettes, Layer Properties Manager and other dialogs, ViewCube, icons drawn in code, themes, the interface string catalog (`i18n/`, English, Ukrainian and German), JSON control channel (`control.rs`) |
|
||||
| L5 | `mcp` | 559 | MCP server (JSON-RPC 2.0 over stdio) on a headless session or bridged to a running app |
|
||||
@@ -64,7 +64,7 @@ SVG and plot preview reuse the same display list through the CPU rasteriser or v
|
||||
## Files
|
||||
|
||||
DXF is the native path: `dxf` parses tags, `io::dxf_read` maps them to the document, and
|
||||
`io::dxf_write` writes an R2000 (AC1015) DXF. DWG is converted to DXF in memory by `acadrust` and
|
||||
`io::dxf_write` writes an R2000 (AC1015) DXF. DWG is converted to DXF in memory by shared `craft-dwg` and
|
||||
read through the same path; saving DWG writes our DXF and converts it back. CADCraft-only data
|
||||
(constraints, associativity details, table flags) travels in xdata under our own application
|
||||
name. Detail and gaps: [file-format-parity.md](file-format-parity.md).
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde_json::Value;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn dwg_test_dependency_matches_the_bridge_targets() {
|
||||
fn dwg_test_dependency_matches_the_shared_bridge_targets() {
|
||||
let output = Command::new(env!("CARGO"))
|
||||
.args(["metadata", "--format-version", "1", "--no-deps", "--locked"])
|
||||
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||
@@ -17,9 +17,11 @@ fn dwg_test_dependency_matches_the_bridge_targets() {
|
||||
let package = packages.iter().find(|p| p["name"] == name).expect("workspace crate");
|
||||
package["dependencies"].as_array().expect("crate dependencies").iter().find(|d| d["name"] == "acadrust").expect("DWG decoder dependency")
|
||||
};
|
||||
let bridge = dependency("cadcraft-dwg");
|
||||
let tests = dependency("cadcraft-io");
|
||||
assert_eq!(tests["kind"], "dev", "the IO crate only inspects acadrust output in tests");
|
||||
assert_eq!(tests["target"], bridge["target"], "--all-targets must not add acadrust where the native DWG bridge is unavailable");
|
||||
assert_eq!(tests["req"], bridge["req"], "tests must inspect the same decoder version as the bridge");
|
||||
assert_eq!(
|
||||
tests["target"], "cfg(not(any(target_arch = \"wasm32\", all(target_os = \"freebsd\", target_arch = \"aarch64\"))))",
|
||||
"--all-targets must not add acadrust where the shared DWG bridge excludes it"
|
||||
);
|
||||
assert_eq!(tests["req"], "^0.6.6", "tests inspect output from the decoder version used by craft-dwg");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user