Repository navigation
R2007+ DXF, code pages, header, every new model field, and uncad-export (0.3 port, 3/3) - #1
Closed
iujunhyung wants to merge 56 commits into
Closed
iujunhyung wants to merge 56 commits into
iujunhyung wants to merge 56 commits into
Conversation
Without libclang the build died inside bindgen's Builder::generate() with "Unable to find libclang: couldn't find any valid shared libraries matching ...", which names neither this crate, nor LLVM, nor the command that fixes it -- and someone running `cargo install uncad-cli` never reads README.md's "Platform" section. build.rs now probes libclang first, through the same bindgen::clang_version() call generate() would make, and fails with the install command per platform, and with the LIBCLANG_PATH it searched when that is set. bindgen already runs before the C compile here, so the failure came within seconds before too; the difference is the message. Ported-from: 6ea8a3c Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the shim The shim gains the entry points the parser needs to read a drawing whose path LibreDWG cannot open, and to turn the file's bytes into UTF-8: - uncad_dwg_read_bytes / uncad_dxf_read_bytes decode from a buffer. The file readers fopen() a `const char *`, which the MSVC runtime interprets in the ANSI code page, so a non-ASCII path fails with DWG_ERR_IOERROR. Both mirror their file-based counterparts, down to dwg_read_file()'s VIEWPORT id fix-up; on four corpus files (DWG R2000/R2018, DXF R2000 x2) they return the same error code and object count as the file readers. - uncad_dwg_version, uncad_dwg_from_version, uncad_dwg_from_dxf and uncad_codepage_name expose the header fields Dwg_Data hides behind its opaque binding. - uncad_tv_to_utf8, uncad_bytes_to_utf8, their entity-pointer twins, uncad_dwg_string_to_utf8 and uncad_free_string convert a string by the drawing's code page, with the DOS-era double-byte rules LibreDWG's tables get wrong (BIG5 and GB2312 pair only bytes >= 0x80, CP932 is double-byte, GB2312's EUC bytes are masked to the table's 7-bit index), and tell an R2007+ DXF's UTF-16 fields from its 8-bit ones. - build.rs binds them, plus dwg_version_type and dwg_next_object. Where both sides of the port defined the same thing there is one definition. uncad_dwg_codepage returns uint16_t: LibreDWG's header.codepage is a BITCODE_RS, and a second `unsigned int` definition would not compile; uncad_codepage_name takes the same type. The feature branch's uncad_dwg_is_tu is the same test as uncad_dwg_is_wide_string, and no Rust code calls it, so only uncad_dwg_is_wide_string remains, now written as the library's own IS_FROM_TU_DWG macro (bits.h is on the shim's include path) instead of a copy of it. Nothing calls the new entry points yet; the parser moves onto them when it gains parse_bytes and the code-page rules. Ported-from: 2b2c750, 78cfe4b, a37b7ba, c045d6a, 1f08658, efd43cc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vendored copy stops being byte-for-byte upstream: five files carry an `uncad local patch` block, each dated and saying what it changes and why (GPLv3 §5(a)). - src/dwg.c: dwg_find_tablehandle(), dwg_find_dicthandle_objname() and dwg_handle_name() decode a table record's name before comparing it. For an R2007+ DXF the importer stores names as UTF-16 but read them as 8-bit, so every layer, block, style and dimstyle lookup it makes while building the drawing stopped at the first character. - src/common.c: cvt_TIMEBLL() zeroes its struct tm and clamps every field into what strftime() accepts. One changed byte of the corpus Helix.dwg otherwise ends the process with 0xC0000409 inside the C library, where no Rust guard reaches. - src/in_dxf.c and src/dynapi.c: AcDbPolygonMeshVertex, the marker on a polygon mesh's vertices, is known. Without it one polygon mesh is a critical error (2048) for the whole DXF. - src/common_entity_data.spec: an R2004+ entity colour reads its RGB before its transparency, as LibreDWG's own bit_read_ENC() does; with both present the two were swapped. The five files are the feature branch's, byte for byte (same blob hashes). With them, and nothing else changed, all 92 existing tests pass unchanged: golden, corpus_sweep's pinned counts, determinism, second_reader and dxf_pipeline included. The R2007+ DXF refusal stays -- this crate's half of reading those files is not ported yet. tests/vendored_patches.rs pins the two patches parse() can observe today; both tests fail against the unpatched sources (the first by ending the test binary with 0xC0000409, the second with Critical(2048)). The dwg.c patch is not observable while R2007+ DXF is refused, and the colour-order one not while an entity's true colour is read only under the TRUECOLOR method; the documents say so. The documents that called the vendored copy unmodified now say what it carries: the crate description, lib.rs, vendor-config/config.h, README, docs/ARCHITECTURE.md and docs/THIRD_PARTY_NOTICES.md. NOTICE.md at the crate root lists the five changes inside the tarball, where docs/ does not reach; docs/CAVEATS.md gains "Local patches to the vendored LibreDWG" and notes, in the R2007+ DXF section, that the library's half of that fix is now vendored. scripts/sync-libredwg-vendor.sh warns that a re-vendor deletes the patches. Ported-from: 722d568, 596e208, 598fc09, 25cd9e7, 75b0461 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cargo package` never includes a file from outside the crate directory, and no crate here sets `license-file` or an `include` list, so the repository root's LICENSE reached none of the three tarballs. Measured on the published 0.2.0: libredwg-sys carried only LibreDWG's own vendor/libredwg/COPYING, uncad only the OFL text of the font it bundled then, and uncad-cli no licence file at all. Each crate now has its own copy, byte-identical to the root LICENSE. (`license-file.workspace` would avoid the copies, but cargo then warns "only one of `license` or `license-file` is necessary" for every member on every build.) Ported-from: 25cd9e7 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drift detector in build.rs counts .c files, and a re-vendor through scripts/sync-libredwg-vendor.sh replaces the five patched files with upstream's without changing that count: the build then compiles upstream's code, and polygon-mesh DXFs, R2007+ DXF names, the colour order and the strftime abort come back without a word. build.rs now also counts the `uncad local patch` markers in every file under vendor/libredwg and compares them, file by file, with the table it carries (LOCAL_PATCHES). A difference in either direction panics with both lists and says what to update. Checked by replacing dynapi.c with upstream's: the build stops, naming the file that lost its marker. Markers are counted as bytes, so a CRLF checkout counts the same. ARCHITECTURE's list of drift detectors, CAVEATS, NOTICE.md and the sync script's warning say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…carry crates/uncad-cli/tests/release_invariants.rs checks, against the tree: - every published crate has its own LICENSE, byte-identical to the root one (cargo package never reaches outside the crate directory); - LibreDWG's own COPYING stays beside the vendored sources; - while any vendored file carries an `uncad local patch` marker (found by walking the tree), nothing calls the vendored LibreDWG unmodified: not the crates.io description, the rustdoc or config.h's header (which must say "patch"), and none of the phrases README, ARCHITECTURE and THIRD_PARTY_NOTICES used before this port (each of which must say "local patch"); and NOTICE.md and CAVEATS name every patched file. The feature branch's version also checked `uncad --version`, the test counts the documents state and the CI triggers; those claims do not hold on this tree yet and come with the ports that make them true. Ported-from: 192d7fd Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shim's new entry points, the per-crate licence files and NOTICE.md, the libclang message, and the five vendored patches with the build check that keeps them. Measured for the last entry: the JSON output of all 208 corpus drawings (172 parsed, the same 36 refused or failed) is byte-identical between this branch and da93fa7, so the patches change no value this crate reports today; what they change is shown by tests/vendored_patches.rs. Ported-from: 25cd9e7, 598fc09 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…were sent upstream CAVEATS said none of the five patches had been sent to LibreDWG, which nothing here can check. What can be checked replaces it: the lib/libredwg submodule (a8ce248) and the vendored copy, compared file by file with line endings aside, differ in exactly the five patched files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uncad-model gained extrusion, justification, polyline, viewport, layer state, dimension-style and layout fields and a POLYLINE_MESH variant; iron-render-cad replaced ToPngOptions::scale with a PngSize. Give every literal the value the model documents for a file that states nothing (the z axis, None, an empty list, an absent reference) and count the mesh with the polylines in the second-reader comparison. The parser does not read the new fields yet; the commits that port each feature do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eighteen R2000 DXFs written from group codes by make_fixtures.py (which reproduces them byte for byte): code-page strings, mirrored object coordinate systems and bulges, dimensions of every kind, viewports and layouts with plot settings, layer states, polyline vertices, meshes, colours, a nested attribute and a sheet whose every string is on paper. Their README states what each file says and what LibreDWG holds after reading it; the renderer and export assertions the feature branch made on them are left to those crates. tests/fixtures.rs reads each one with clean diagnostics and through a JSON round trip, and pins what the model already carries: the CP949 strings, the bound dimension block, the paper-space viewport, the construction lines, block children kept on layer 0, the strings on the sheet. tests/polyline_closed.rs checks every LWPOLYLINE's closed bit in example_2000.dwg and .dxf against the DXF's own group 70. The DXFs are kept out of line-ending normalisation. Ported-from: 2b2c750, be3ff34, a7f9ea5, f80bdfa, 2415979, 47d0b00, 7d4ce8b, 7ff5c70, 72cedb9, bd10f50, b4918ab Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An INSERT converts its own ATTRIBs, the one place the conversion recurses, and every attribute source it reads is data from the file: a damaged handle can point the chain back at the INSERT (endless recursion), make it a ring (an endless loop) or point it at anything at all. The attribs[] array was converted whatever each reference named, and the R2004+ owned-subentity walk and the polyface walk had no bound. Now the conversion carries its depth and stops past two levels (the format has one), each walk converts only ATTRIBs and stops at the first object that is not one, and every walk hands back at most 100,000 subentities. tests/corrupt_dwg.rs pins the three bytes of example_2000.dwg that sent the feature branch's walk around the INSERT until the stack ran out; this crate's chained walk already survived them, and the test keeps it that way. No output changes: the JSON of all 199 drawings the CLI reads from the corpus, the fixtures and the samples is byte-identical before and after. Ported-from: e7d9780 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LibreDWG's dwg_object_polyline_{2,3}d_get_points walk first_vertex ..
last_vertex with a loop whose condition ends before its body sees
last_vertex, so on every file older than R2004 the last vertex was
dropped: a closed square came back a triangle, a two-vertex arc a single
point. The vertices now come from the polyline's own owned-subentity
chain, which stops at last_vertex and, from R2004 on, indexes the same
vertex[] array; a drawing older than R13 fills neither, and there the
VERTEX records that follow the polyline in the object list are read, as
LibreDWG's own pre-R13 branch does. The four accessor allowlist entries
in libredwg-sys go with them.
The walk that lists a block's entities for versions other than
R13..R2000 skips the five VERTEX kinds, as the R13..R2000 chain walk
already did: the DXF importer puts every object between a BLOCK and its
ENDBLK into that list.
Measured against the files themselves: all 22 POLYLINEs whose VERTEX
records can be counted in a corpus DXF or fixture now have exactly that
many vertices (the accessors gave 20 of them one too few), the DWG twins
agree with their DXFs, and all 12,250 POLYLINE_2D/3D vertex lists
(entities and block records over the 235 oracle inputs) equal the
feature branch's; the 69 that differ from upstream's are each one vertex
longer.
corpus_sweep's layer-reference total goes from 64,733 to 64,671: the 62
VERTEX_* Unknown entities seven pre-R13 DXFs listed as block content
(r2.6, r2.10, r9, r10, both r11, r12 Leader) are gone, and nothing else
changed in the sweep.
Ported-from: efd43cc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LayerRecord's off, frozen, locked, plot, lineweight and linetype were the defaults for a file that states nothing. They are read now: - off/frozen/locked from the bits LibreDWG's DWG decoder fills; for a DXF from the raw group 70 (1 frozen, 4 locked) and the colour's sign, because the importer applies the binary layout to group 70 (2 off, 4 frozen in new viewports, 8 locked). A negative colour is "off" for either reader, as the model documents. - plot (290) and lineweight (370) from R2000 on. A DWG states both; the DXF importer leaves an absent group at 0, so a DXF's 290 = 0 and its silence are both None, and a lineweight code of 0 is None. Codes map through the lineweight table to hundredths of a millimetre, 31 to -3; BYLAYER/BYBLOCK and the unused codes are not a layer's weight. - linetype through the same reference resolver as every other table reference, so a pre-R13 drawing's index resolves too. The regenerated golden expectations for G1-G10 (they only add the model's new keys) now pass whole: their layers name CONTINUOUS. tests/layer_state.rs checks the hidden-layers fixture and the example_2000 DWG/DXF twins. A/B over the 717 layers the feature branch also read (corpus, fixtures, samples): off, frozen and locked agree everywhere; plot agrees for every R2000+ DWG layer (557 plotted, 25 not) and is None where the feature branch said "plots" without the file saying so (90 DXF, 45 pre-R2000 DWG layers); lineweights agree (the feature's "none" is the model's -3); the linetype differs on 28 layers of pre-R13 DWGs, which now resolve to CONTINUOUS through the table index where the feature branch had an empty name. Ported-from: c045d6a Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A polygon mesh arrived as Entity::Unknown. It is now Entity::PolylineMesh: the M by N VERTEX_MESH records of its own subentity chain, joined as the model states -- first each row to the next, column by column, then along each row -- with the closing edges where group 70 says the mesh is closed in M (bit 1) or N (bit 32). A mesh whose vertex count is not M times N (a smoothed surface stores its control points beside the approximated ones) keeps no edges and counts every edge its grid implies as skipped, so it reads as not read rather than as empty. A polyface mesh accepts VERTEX_MESH positions too: LibreDWG's DXF importer types a polyface vertex VERTEX_MESH when the vertex names the block record rather than the POLYLINE as its owner (what ezdxf writes), and such a polyface found no positions. Golden case G15 (a 3 by 4 mesh closed in N) reads back exactly. A/B: the four POLYLINE_MESHes (2000/TS1.dwg, the polyface fixture) and the 25 POLYLINE_PFACEs of the oracle inputs have exactly the feature branch's edges; against upstream, the polyface 4E4 of example_2000.dxf, example_r13.dxf and example_r14.dxf now has the six edges its DWG twin already had, and TS1.dwg's two meshes are no longer Unknown. corpus_sweep's pinned numbers do not move. Ported-from: efd43cc, 598fc09, bd10f50 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CIRCLE, ARC, LWPOLYLINE, POLYLINE_2D, TEXT, ATTRIB, ATTDEF, INSERT, SOLID and TRACE state their coordinates in an object coordinate system whose normal they carry (DXF 210), the planar ones at an elevation. The model's extrusion and elevation fields were the defaults; they are read now, as the file states them: the coordinates stay in the OCS (owner decision: the transform is the consumer's; Affine2::from_insert does it for a block reference), the normal is not normalised, and only a zero vector reads as the default (0, 0, 1). An LWPOLYLINE's normal is read only when its flag says one is stored, as the decoder itself does. A/B: all 55,635 normals the feature branch also carried (it normalised them) agree; every coordinate of these kinds is upstream's, apart from the 52 POLYLINE_2D vertex lists the last-vertex fix lengthened -- the feature branch moved the 401 mirrored entities of the samples (240 SOLIDs, 95 LWPOLYLINEs, 66 INSERTs) to world coordinates, which the model deliberately does not. tests/fixtures.rs pins the mirrored fixture: stated coordinates, stated angles, the (0,0,-1) normal. Ported-from: fae3b14, 89d77c5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWPOLYLINE and POLYLINE_2D kept only their vertices, so every arc segment arrived as its chord and every width as none. They now carry: - bulges: LWPOLYLINE's bulges[], a POLYLINE_2D's VERTEX bulges, one per vertex with the sign the file wrote (a mirrored OCS does not change it; the feature branch negated them while moving the vertices to the world, which the model no longer does); - widths: LWPOLYLINE's widths[], a POLYLINE_2D's VERTEX start/end widths; and const_width (DXF 43; a POLYLINE_2D states none, 0). Both lists are empty when every bulge is 0 or every width is the constant one, so a file stating those values and one stating nothing are the same polyline (the model's rule, which G12's last polyline tests). One reading goes beyond the field map: a DXF writes a POLYLINE's default widths once, as its groups 40/41, and omits them from every VERTEX that has them, and the importer reads an absent vertex width as 0 -- so a DXF vertex whose widths read 0 takes the default. Without it the DXF of 2000/PolyLine2D gave its 0.15-wide arrow ticks no width while the DWG twin carried 0.15 on every vertex. Golden cases G11 (a mirrored part) and G12 (curved and wide polylines) read back exactly. tests/polyline_fields.rs holds the DWG/DXF twins of example_2000, example_2004, example_r13 and 2000/PolyLine2D to agree on every polyline field. A/B against the feature branch over the oracle inputs: all 20,195 polyline bulge lists agree once its mirrored-OCS negation is undone, all 7,986 LWPOLYLINE width lists and constant widths agree (the empty-list rule aside). Of the 52 POLYLINE_2Ds it gave a constant width from the polyline's start width, 50 now carry that width on their vertices; the other two (one polyline of AutoCADSamples5.dwg, counted as entity and block content) state 0 on all 553 vertices, and carry no width, as the vertices say. Every twin polyline agrees between DWG and DXF (80). Ported-from: fae3b14, 89d77c5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opens
parse() handed LibreDWG a C path, which dwg_read_file/dxf_read_file open
with fopen(); the MSVC runtime reads that byte string in the process's
ANSI code page, so a drawing under a Korean directory name failed with
critical error 4096 although the file was there. parse() now reads the
bytes with std::fs::read and decodes them with the shim's memory readers
(uncad_dwg_read_bytes / uncad_dxf_read_bytes, ported in the native layer),
and the same decode is public as parse_bytes(bytes, Format) for callers
that never have a path.
- Format { Dwg, Dxf } with Format::from_path: ".dxf" in any case is DXF,
anything else DWG -- what parse() always did.
- ParseError::Io(std::io::Error) for a file that cannot be read, with the
cause as its source(). ParseError::InvalidPath is removed: nothing can
produce it any more (a path is no longer converted for C), and a variant
that is never returned would be a wrong sentence in the API.
- The R2007+ DXF refusal is unchanged; its $ACADVER scan reads the bytes
already in memory instead of opening the file a second time.
tests/read_paths.rs (ported, adapted): a DWG and a DXF under a Hangul
path, parse vs parse_bytes equality, Format::from_path, a missing file as
Io, garbage bytes as Critical. The CLI's parse_input comment now says
what parse() reports.
Ported-from: 2b2c750
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n type
A consumer cannot read the model's numbers without the header: the unit a
coordinate is in ($INSUNITS), the extents the file claims, the dimension
variables a DIMENSION falls back on. The model carries no header variables
by design, so the header is this crate's public type, uncad::Header, read
in the same pass and returned beside the database:
parse_with_header(path) -> Result<(CadDatabase, Header), _>
parse_bytes_with_header(bytes) -> Result<(CadDatabase, Header), _>
parse() and parse_bytes() keep their signatures and call these. A separate
header read was the alternative and was not taken: it would decode the
drawing twice, and a DXF decode is the expensive part (more than quadratic
in LibreDWG's importer). A tuple rather than a wrapper struct keeps the
database the model's own type with nothing to unwrap.
Header: format, acadver (the DWG magic or $ACADVER, as the file states it),
version (LibreDWG's release name), codepage/codepage_name (the one strings
were decoded with), $INSUNITS (Header::units() -> Units { name, to_mm },
the DXF reference's table), $MEASUREMENT, $LUNITS/$LUPREC/$AUNITS/$AUPREC,
$EXTMIN/$EXTMAX/$LIMMIN/$LIMMAX and the paper-space four, the $DIM*
variables (DIMSCALE, DIMLFAC, DIMDEC, DIMLUNIT, DIMPOST, DIMRND, DIMZIN,
DIMFRAC, DIMAUNIT, DIMADEC, DIMTXT, DIMASZ), $LTSCALE, $TEXTSIZE and
$CLAYER as a Ref<String>. Serialize/Deserialize, #[non_exhaustive].
Unknown is None, never a default. LibreDWG's struct holds every variable
whether the file states it or not, so a variable is Some only when the
file is known to: for a DWG by its version's header layout
(header_variables.spec / header_variables_r11.spec: INSUNITS, DIMADEC,
DIMFRAC, DIMLUNIT from R2000; DIMDEC, DIMAUNIT from R13; a pre-R13 header
stops where its numheader_vars says; MEASUREMENT only when the optional
Template section was read), for an ASCII DXF by the variables its HEADER
section names (a scan of the bytes, bounded by the section), and not at
all for a binary DXF, which is not scanned. The feature branch filled all
of these with zero or LibreDWG's value. The same scan now supplies the
R2007+ refusal's $ACADVER.
Two shim accessors make that decidable (new C API, in the shim this crate
owns): uncad_dwg_numheader_vars and uncad_dwg_template_read (whether
Template.description was allocated, which bit_read_T16/TU16 do for any
length). TextDecoder gains header_text, and codepage_name returns None for
a codepage LibreDWG has no name for.
tests/header.rs (ported, adapted to Option): example_2000.dwg against the
values the file states, the DXF twin agreeing, R14 stating no R2000-only
variable, pre-R13 headers cut where their count says, the Chinese R2013
file, $DIMPOST round trips, a DXF stating only what its HEADER names, the
serde shape, parse vs parse_bytes. Unit tests for the INSUNITS table, the
scan and the DWG layout.
Ported-from: 2b2c750, 1f08658
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TextDecoder walked a double-byte code page the way LibreDWG's own converter
does, and inherited three of its table quirks:
- dwg_codepage_is_twobyte answers "always" for Big5 (24) and GB2312 (31),
ASCII included, so "*Model_Space" was consumed as "*M", "od", ...: a
drawing declaring either lost its model space and every entity, and
'中国 AB' decoded as four U+FFFD. Both are EUC-style encodings whose lead
and trail bytes all have the high bit set; only such a byte opens a pair.
- The GB2312 table is indexed by the 7-bit ISO-2022 form of a pair
(0x2121..0x777E) while the file holds EUC-CN bytes; the pair is masked
to 7 bits before the lookup.
- CP932 (DOS Shift-JIS, 22) is left out of dwg_codepage_isasian and went
through the single-byte table one byte at a time; it is double-byte.
And ASCII is never looked up in any code page's table now. The wide lookup
used to be applied to every byte of a double-byte page, which turns 0x5C
into a yen sign (CP932) or a won sign (JOHAB) -- in a drawing it is the
backslash of \P and of every \U+XXXX escape.
Upstream's rules stay as they were: a DXF string that is valid UTF-8 is
UTF-8 first, a byte the declared page has no character for is U+FFFD and a
TEXT_ENCODING warning, and a code page LibreDWG has no table for is
reported and read as UTF-8 -- not guessed as ANSI_1252, which the feature
branch's C conversion did in silence. \U+XXXX escapes are passed through
as written (disposition #8: expansion is a consumer's).
A/B on generated DXFs (bytes from Python's encoders), text value as read:
GB2312 '中国 AB' upstream '����' + TEXT_ENCODING, feature and this '中国 AB'
BIG5 '中文 AB' upstream '中文��' + TEXT_ENCODING, feature and this '中文 AB'
CP932 '日本 AB' upstream '���{ AB' + TEXT_ENCODING, feature and this '日本 AB'
ANSI_1252 + UTF-8 'café': upstream and this 'café', feature 'café'
unknown $DWGCODEPAGE + 'caf\xE9': upstream and this 'caf�' + TEXT_ENCODING,
feature 'café' with no warning
On the 198 corpus/fixture/sample inputs both read, every entity string,
layer and block name and every TEXT_ENCODING diagnostic is the same as
upstream's.
tests/codepage.rs (ported, adapted): single-byte pages keeping every
character, an unmapped byte reported, the DOS-era pages not pairing ASCII
(example_2000.dwg with its declared code page patched) and decoding their
pairs against the Windows twins, and a corrupt code page reported rather
than guessed. Unit tests in text.rs for the same rules at byte level.
Ported-from: a37b7ba
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…op refusing it
LibreDWG's DXF importer stores every string it sets through its field
setter as UTF-16 once the version is R2007 or later, exactly as for a DWG,
but its text accessors convert UTF-16 only for a DWG (IS_FROM_TU_DWG) and
hand an R2007+ DXF's strings out as 8-bit ones, which stop at the first NUL
byte: "*Model_Space" read as "*", no block record matched, and the file
came back empty with no error -- which is why it was refused. The
exceptions are the strings the importer copies byte for byte: MTEXT's text
chunks (groups 1/3) and the HEADER variables, parsed before it knows the
version. Those are the file's UTF-8 and must never be read as UTF-16,
which scans for a 16-bit NUL past their allocation.
TextDecoder now knows which of three storages a drawing is (R2007+ DWG,
R2007+ DXF by the same header.version >= R_2007 rule the library's setter
uses, or 8-bit before R2007) and reads each stored string in its width:
dynapi hands back a RawText (Converted UTF-8 from the library, stored
Bytes, or stored UTF-16 units read in Rust up to their 16-bit NUL), never
decoded there. The 8-bit strings of an R2007+ DXF are checked as UTF-8 and
reported when they are not. stored_field decodes a string field reached by
raw pointer (an embedded struct such as LAYOUT's plotsettings) by the same
rule. The importer's own layer/block lookups are the vendored dwg.c
patch's (ported in the native layer); this pins it.
The refusal is gone. ParseError::UnsupportedDxfVersion stays, for the
failure this used to be: an R2007+ DXF whose entities LibreDWG placed in
model or paper space (entmode, set from each entity's owner handle, no name
lookup of ours involved) while none reached the model is an error, not an
empty drawing. An error rather than a read_diagnostics warning, because
upstream's own ordering (CAVEATS) is that an obviously empty drawing is
worse than an error, and a warning would still return the empty drawing as
Ok. Scoped to R2007+ DXF: the same test on every input would also refuse
two pre-R13 DWGs (r11/ACEB10.dwg, r2.10/block.dwg) that read today with an
empty model space. No corpus file trips it; with the width fix disabled,
exactly the 27 readable R2007+ DXFs do.
Measured:
- corpus_sweep (dxf parsed, refused, critical) (31, 32, 4) -> (58, 0, 9):
27 of the 32 R2007+ DXFs read; 2013/gh109_1, 2018/Constraints,
Dynblocks, LiveSection1 and TS1 fail inside LibreDWG with 2048. Layer
references 64,733 -> 66,064: the 1,331 entities of those 27 files, all
resolved but the 21 of 2010/gh209_1.dxf, whose entities the importer
leaves without a layer handle (absent). Every other pinned number holds.
- 22 of the 24 readable R2007+ DXFs with a DWG twin state the same entity
count, layers and INSERT blocks; the two that do not are pinned as known
deviations with a tripwire (gh209_1 above; 2018/Leader.dxf's own text
puts one entity on a layer "0 @ 1" the DWG does not have).
- 2000/entities-2d.dxf restamped AC1015..AC1032 reads to the same model.
- Against the oracle: all 235 inputs parse or fail exactly as the feature
branch's CLI; on the 28 it reads that upstream refused, every string,
layer and block name matches the feature branch but four MTEXT texts,
where the feature branch expanded "108\U+00B0" to "108°" and this keeps
the escape as written (disposition #8).
- Without the dwg.c patch: r2007_dxf_handles.rs fails all 3 tests
(example_2018.dxf {"0": 7, <absent>: 65}), dxf_pipeline's two R2007+
tests fail.
Tests: tests/r2007_dxf_handles.rs (ported: DWG-twin agreement on layers,
blocks and the layer+invisible tally -- the hidden count is a consumer's
rule now, so what the parser owns is compared), codepage.rs's R2007+ cases
(ported; escapes now kept as written), header.rs's $DIMPOST across all 27
files and at AC1032, dxf_pipeline's two refusal tests turned into
twin-agreement tests, lib unit tests for the guard. vendored_patches.rs's
module doc, CAVEATS (the refused section, the text section, the dwg.c
bullet, the corpus tally, the stale R2000/R2004 entity-count paragraph),
README scope and CHANGELOG say what is true now.
Ported-from: 2b2c750, 78cfe4b, 722d568
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TEXT, ATTRIB and ATTDEF carried only their start point, height, rotation and string, so a centred or right-aligned text was placed as if it started where its alignment point is not, and its style was not named. They now carry the horizontal and vertical justification (DXF 72 and 73, 74 on the attribute kinds; out-of-range values read as the format's left/baseline default), the alignment point (DXF 11, only when a justification is not the default -- otherwise the field holds nothing the file stated), the width factor (DXF 41; a 0, which a hand-written DXF that omits the group reads as, is the default 1), the oblique angle (DXF 51, radians from both readers) and the text style (DXF 7) as a reference; LibreDWG's DXF importer already points an entity with no group 7 at the STANDARD entry when the drawing declares one. An MTEXT carries its reference width (41), its extents (42/43, a zero measures no text and is None) and its style. tests/text_fields.rs writes a DXF from group codes (a centre/middle TEXT at (50, 50) with width 0.8 and a 15-degree slant, a plain TEXT, a rotated MTEXT) and checks example_2000.dwg's attributes. A/B against the feature branch: justification, alignment point, width factor, oblique angle and style agree on all 4,256 TEXTs and ATTRIBs it read (two mirrored texts aside, whose alignment point it moved to the world), and rect width, extents and style on all 3,152 MTEXTs; the goldens G1-G12 still read back exactly. Ported-from: 96abb6d Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ATTRIB and ATTDEF carry AttributeFlags from DXF 70 -- bit 1 invisible, 2 constant, 4 verify, 8 preset -- instead of the no-flag default. An invisible attribute (a title block's hidden field) was indistinguishable from a shown one. Golden case G13 (justified text, text styles and an invisible attribute next to a visible one) now reads back exactly, with one known deviation applied to the expectation and pinned by the existing tripwire test: a TEXT naming the text style GOST, which the file never declares, is pointed by LibreDWG's DXF importer at STANDARD -- the entry an absent group 7 means -- and the name it wrote is lost, the same cause as the undeclared block and dimension-style names G10 and G5 already pin. tests/fixtures.rs pins the nested-attribute fixture from the parser's side of disposition #12: the INSERT inside DOOR carries D-101 as its own attribute and DOOR's entity list does not repeat it; the top level lists the model-space INSERT's D-TOP once after it. A/B: no attribute of the corpus or the samples sets any of the four bits, and all 1,401 ATTRIBs agree with the feature branch's invisible flag. Ported-from: c045d6a, 47d0b00 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native layer carried the feature branch's C string conversion into the shim (uncad_tv_to_utf8, uncad_bytes_to_utf8, their uncad_entity_* twins, uncad_dwg_string_to_utf8, uncad_free_string, uncad_codepage_name and the static convert_codepage/bytes_to_utf8 behind them). No Rust code calls any of them: TextDecoder decodes every string itself, with the DOS-era rules and the R2007+ DXF width split that were the reason for the C version, and unlike it reports what it cannot decode instead of falling back to ANSI_1252 in silence. Keeping a second implementation of the same rules would let the two drift, so they go, with their bindgen allowlist entries. What R2007+ DXF still needs from C is kept: the file-header accessors (version, from_version, from_dxf) and uncad_dwg_is_wide_string, which TextDecoder reads to tell the three storages apart. UTF-16 is read in Rust (dynapi::read_stored), so bit_convert_TU is not needed either; codepages.h is no longer included by the shim (wrapper.h still gives bindgen the table functions TextDecoder calls). The comment in the vendored dwg.c patch named uncad_tv_to_utf8 as the rule its predicate matches; it names crates/uncad/src/text.rs now (a comment only, the marker count is unchanged). The CHANGELOG entry for the shim and ARCHITECTURE's shim line no longer list the conversions. No output changes: the whole workspace suite passes unchanged. Ported-from: a37b7ba, 78cfe4b Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A DXF's decode grows faster than the square of its entity count inside LibreDWG's importer, so a drawing of a few megabytes takes a minute. The feature branch recorded this in CAVEATS (75b0461) with numbers from its own machine and generator; they did not reproduce here, so the paragraph carries this branch's measurement instead (release CLI, LINE-only DXFs with a handle and subclass markers per entity, summary only): 25 000 entities, 2.3 MB: 2.8 s 50 000 entities, 4.6 MB: 16.4 s 100 000 entities, 9.2 MB: 72.0 s The 0.2 CLI, whose conversion code differs, takes the same on the same files (3.0 s, 14.0 s), which is what places the cost in the importer. The feature branch's instrumentation of dwg_read_dxf (nearly all of it in dxf_entities_read, re-resolving the object-reference vector on every pool reallocation) is cited as its measurement, not repeated as this one. Its claim that a DWG of the same drawing parses in linear time was not measured here and is not carried over. parse()'s rustdoc and README's DXF scope say the same in a clause. Ported-from: 75b0461 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An entity's true colour was read when its colour's method said TRUECOLOR, which is wrong for both readers: an R2004+ DWG states the RGB under the colour's ENC flag 0x80 and never sets the method, so no DWG entity reported its true colour; the DXF importer answers a plain group 62 with the TRUECOLOR method and an RGB synthesised from its own ACI palette, so a DXF entity with only an index reported an RGB the file never wrote (example_2000.dxf's LINE 513, ACI 8, came back 0x414141). split_entity_color reads the flag first -- 0x80 an inline RGB, 0x40 a colour-book reference, which is not converted -- and for the DXF shapes drops an RGB that is the one the library synthesises for the entity's index. That RGB comes from the library itself: libredwg-sys now binds dwg_rgb_palette_index, so LibreDWG's table stays private to the parser and the model's display palette (upstream's pinned table, which differs on 222 of 256 indices) is untouched (owner decision on the palette). This makes the vendored common_entity_data.spec patch visible, and tests/vendored_patches.rs now pins it: HatchG.dwg's HATCH 29F (RGB and transparency) and the LWPOLYLINE 28D around it (RGB alone) both carry 0x1ae464, where the HATCH's colour read 0x0000e5 without the patch. Its module doc, the CAVEATS bullet and the CHANGELOG line that said the patch was not visible yet are rewritten. tests/fixtures.rs pins the four DXF spellings of entity_truecolor_r2000.dxf. A/B: the true colour of all 206,652 entities compared agrees with the feature branch; against upstream, 23 differ -- the example_*/sample_* DXFs' synthesised ACI RGBs are gone (10), HatchG.dwg and 2013/gh44-error.dwg report the colours they state (7), and the fixture (6). Ported-from: efd43cc, 598fc09, 0c07b95 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DimStyleRecord's arrow size (DIMASZ), linear unit format (DIMLUNIT), zero suppression (DIMZIN), rounding (DIMRND), angular unit format (DIMAUNIT), angular decimal places (DIMADEC) and fraction format (DIMFRAC) were None. They are read like the style's other variables: every number Some, an enumeration only for a value the format defines (DIMLUNIT 1..6, DIMAUNIT 0..4, DIMFRAC 0..2), the 16-bit ones read signed so DIMADEC's -1 survives. The rules for a style that states next to nothing -- the feature branch fell back to the header and capped DIMDEC -- are the export's, not the parser's. The golden test's DIMSTYLE deviation, which takes the library's starting value for every variable a DXF case does not state (the library cannot tell an omitted group from a written one), now covers the seven new fields too; every stated variable is still compared exactly. A/B: all eleven compared variables agree with the feature branch on all 335 styles of the oracle inputs. Ported-from: 74bb835, 837fb7d Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ps 10 and 16 Two parts of disposition #19, checked against upstream's latest mapping (9039b63) on the corpus and the fixtures: - ordinate_axis: bit 64 of group 70 says whether an ordinate measures its feature's x or y distance from the datum. A DXF, and a drawing older than R13, state it in `flag`; from R13 on a DWG states it as bit 1 of the stream-only `flag2`, and the decoder rebuilds `flag` from it wrongly (it sets bit 128 and clears bit 64), so `flag2` is read there. None on every other kind. - 9039b63 maps a two-line angular dimension's group 10 to xline2end_pt and group 16 to def_pt, which is right for a DWG only: the DXF importer fills those two fields by group code. Read from a DXF, every such dimension had its definition point and arc point exchanged -- example_2000.dxf's 43B gave (3.54, 4128.87) for group 10, which the file states as (490.62, 4118.24). The mapping now depends on the reader; the rest of 9039b63's mapping was already right and is kept. corpus_sweep's angular test pins all five points for example_2000.dxf too. Golden case G16 (ordinate dimensions, a style stating every variable and one stating only its unit formats) reads back exactly; the fixtures angular_ordinate and radial pin every point by its group. A/B: the axis of all 75 ordinates and all five points of all 33 two-line angular dimensions of the oracle inputs agree with the feature branch; against upstream, only the 13 two-line angular dimensions of DXF inputs differ, each in exactly those two points. Ported-from: a7f9ea5, 74bb835 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Group 42 of -1 is what writers leave for a dimension they did not measure; no length or angle is negative. Like 0 before it, it is now None instead of -1.0. Every other value stays the file's, radians for an angular dimension, whether or not it agrees with the dimension's points: the feature branch dropped a stored value more than 1% away from the one its points give and converted angles to degrees, and both are a consumer's steps now (owner decision on disposition #20; G5's 133.5 keeps reading back). A/B over the top-level dimensions of the oracle inputs: against upstream, exactly the 63 sentinels change from -1.0 to None (AutoCADSamples9.dwg 58, AutoCADSamples8.dwg 5); against the feature branch, all 1,120 agree once its degrees are taken back to radians, apart from four in 2018/Dynblocks.dwg (3.048 and 3048.0) that it had dropped for disagreeing with their points and the model carries. Ported-from: 74bb835 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MLineEntity::scale was None. It is DXF 40, which the format requires: the factor the MLINESTYLE's offsets are drawn at, carried as the file states it. The feature branch turned a 0 or a missing value into 1.0; that fallback is a consumer's (the model's own doc says so). A/B: the 37 MLINEs of the oracle inputs (20.0 on 23, 1.0 on 14) agree with the feature branch. Ported-from: 00414f4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ViewportEntity carried only its frame. It now carries: - view (DXF 12, 45, 17, 16, 51, 42): the view's centre and height in its own coordinates, target, direction (a zero one reads as the plan view (0,0,1)), twist (radians from both readers) and lens length -- from R2000 on; an older viewport keeps its view in extended data, which is not read, so it is None (the feature branch reported zeros there); - on: a DXF's group 68; a DWG does not store that group (LibreDWG makes one up) and says "off" as status-flag bit 0x20000, which exists from R2000 on; - viewport_id: a DXF's group 69 only, for the same reason; - frozen_layers: every layer frozen in this viewport alone, in file order, through the reference resolver (an unresolved one keeps its handle; the feature branch dropped those). tests/fixtures.rs pins the twisted viewport's stated view and the four states of the viewport-states fixture. A/B over the 168 viewports of the oracle inputs: every view, state, number and frozen-layer list present on both sides agrees with the feature branch; the model says None for the 12 pre-R2000 views, the 8 pre-R2000 DWG on-states and the 118 DWG numbers it made up values for. Ported-from: 1f08658 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… side The two fixtures whose feature-branch assertions were all about drawing and exporting get the parser-side facts those rested on: every LINE of hidden_layers_r2000.dxf keeps the layer it names whatever that layer's state, and only the one with group 60 = 1 is invisible; each of hatched_viewport_r2000.dxf's pattern HATCHes is owned by its own space's block and carries the definition line its groups state. With this every fixture has a parser-side test; what the renderer and the export do with them is left to those crates. Ported-from: c045d6a, 72cedb9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ARCHITECTURE listed LAYER, BLOCK_RECORD and MLINESTYLE as the tables table_convert.rs reads and the model's `tables` carries. It also reads every DIMSTYLE, and the LAYOUTs with their embedded plot settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every new model field read from the file, the polyline vertex chain, VERTEX subentities kept out, POLYLINE_MESH, true colour, layer state, the DIMSTYLE union, layouts and plot settings, viewport views, bounded subentity walks, the fixtures and the regenerated golden cases. Resolved against port/text: a plot setting's strings are read through TextDecoder::stored_field, which knows the width each drawing stores them in, instead of a second byte reader keyed on a single wide flag (removed); the layer-reference pin adds the text layer's 1,331 R2007+ DXF references to the entity layer's 62 fewer VERTEX ones (66,002, measured); the vendored-patch notes say both library patches are now pinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…E edges Brings in upstream's f768c5d (7 commits) and moves the parser onto the model's synced shape (uncad-model sync/model 39c20e8): a polyline vertex is PolylineVertex { point, bulge, start_width, end_width }, SegmentWidth and the bulges/widths lists are gone, a 3DFACE carries invisible_edges. Where upstream now does what this branch did, upstream's shape is kept and ours dropped; ours stays only for a case upstream's does not cover. Overlaps and how each was resolved: - libredwg-sys binding of dwg_next_object: both sides added it. Adopted upstream's entry and comment; ours dropped. The allowlist is the union (upstream's removal of the four dwg_object_polyline_{2,3}d_get_* accessors matches ours; dwg_rgb_palette_index and the shim entries stay), and the local-patch marker check is kept. - Walking a POLYLINE's vertex records: adopted upstream's polyline_vertex_records/polyline_vertices (pre-R13 through the object stream up to SEQEND, R13+ through the owned-subentity walk); ours (polyline_subentities and its polyline_vertices) dropped. Combined, with our cases on top of upstream's walk: the owned walk is bounded by MAX_OWNED_SUBENTITIES; an R13+ polyline that owns none of its records (a DXF whose VERTEX records name the block record, as ezdxf writes a polyface and a polygon mesh) reads them from the object stream, as before R13 -- the polyface_mesh_r2000.dxf fixture and golden g15 fail without it; and the same walk serves POLYLINE_PFACE and POLYLINE_MESH, so the stream accepts every VERTEX kind and the POLYLINE_VERTICES report lives in the walk. Last vertex: upstream's walk keeps it (polyline_vertices_r2000.dxf and 2000/PolyLine3D.dwg, 6 vertices, as on this branch); nothing to fix. - POLYLINE_VERTICES for a pre-R13 polyline cut short at a JUMP: adopted upstream's (r9 and r10 report one each). - LWPOLYLINE bulges: adopted upstream's with_bulges and its POLYLINE_BULGE report; ours (stated_bulges, the empty list for all-zero bulges) dropped -- every vertex now carries its bulge, 0 when straight. - LWPOLYLINE widths and const_width: upstream has none; ours kept on upstream's shape. with_widths gives the vertices their widths on the terms with_bulges has (one per vertex or none; another length is reported as POLYLINE_WIDTH and not used), and still zeroes them when every one repeats const_width. - POLYLINE_2D vertices: adopted upstream's per-VERTEX_2D read (position and bulge); ours kept on top for the widths it lacks, including a DXF POLYLINE's default 40/41 for the vertices that state none. - HATCH polyline boundary bulges: adopted upstream's (widths 0). - The extrusion helper: combined. Upstream's extrusion() (name, doc) kept, with this branch's rule that a zero vector is the default (0, 0, 1): the library leaves the field zero when the record stores no normal, and read as it stands it gave (0, 0, 0) for 4,289 DWG LWPOLYLINEs of the A/B inputs and for all 49 pre-R13 CIRCLEs, ARCs, SOLIDs, TRACEs and 2D POLYLINEs, where the model says an absent group is (0, 0, 1). Our LWPOLYLINE flag-bit-1 check is dropped: the zero rule covers it. - CIRCLE/ARC extrusion and SOLID/TRACE elevation/extrusion: the same lines on both sides. - LWPOLYLINE/POLYLINE_2D elevation and extrusion: adopted upstream's lines. - 3DFACE invisible_edges and TextDecoder::warn made pub(crate): upstream's. - Goldens: g1, g7, g8, g9 changed on both sides; every case is replaced by the synced model's writer output, byte-exact (g1, g7, g8, g9, g11, g12, g14, g16 .expected.json; g7, g11, g12 .dxf -- the writer no longer writes 42 for a straight segment). g11-g16 stay in the case list. - corpus_sweep (clean DWGs 100 -> 98, two POLYLINE_VERTICES), diagnostics and second_reader: upstream's tests, unchanged. Every pin of this branch was re-measured and none moved. - CHANGELOG: upstream's entries and heading order kept; ours that overlapped (the OCS entry, bulges/widths, the last-vertex fix) rewritten to state only what upstream's do not. - CAVEATS: upstream's "Where a polyline's bulges come from" and "How a 2D or 3D POLYLINE's vertices are found" kept; ours ("Polyline vertices come from the polyline's own chain", "Polyline bulges and widths are carried as stated") dropped, with what only they said moved into upstream's sections and a new "Where a polyline's widths come from". The paragraph saying a HATCH boundary's bulges are dropped is gone with the defect. A/B over the 235 oracle inputs, integrate/0.3 against this merge (the old JSON normalised to the vertex shape): every value is unchanged except the new 3DFACE invisible_edges, the HATCH boundary bulges now read, and the two POLYLINE_VERTICES reports. Against upstream f768c5d: vertex positions and bulges, elevations, 3DFACE edges and diagnostics agree wherever both read a file; the extrusions differ only by the zero rule above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three cases the merge kept on top of upstream's reading code, each now shown by a test that fails without it: - tests/extrusion.rs: a normal the record does not store is the world Z axis. LibreDWG leaves the field zero then -- a DWG LWPOLYLINE whose flag bit 1 is clear, a pre-R13 entity whose options leave it out -- and reading it as it stands gives (0, 0, 0) for every LWPOLYLINE of example_2000.dwg and every CIRCLE, ARC, SOLID, TRACE and 2D POLYLINE of r11/entities-2d.dwg and r10/entities.dwg, where their DXF twins (and the model's own default for an absent group) say (0, 0, 1). - convert.rs unit tests for with_widths: an LWPOLYLINE's width array goes on its vertices one to one; widths that all repeat const_width are no widths of the vertices; an array of another length is reported as POLYLINE_WIDTH and not used, the rule upstream gave the bulge array. TextDecoder gains a test-only constructor for them. - polyline_fields.rs: the corpus's R2000 3D polyline has six vertices in both of its formats, the last one included. Upstream's vertex walk keeps it as this branch's did; the library's point accessors gave the DWG five. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The entry carried two versions of itself, one after the other, from an earlier merge: the second began mid-sentence and each said the other's patch was "not visible yet". Both are visible now -- the name-lookup patch since R2007+ DXF is read, the colour-order one since true colours are read as stated -- and the entry says so once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iron-render-cad's render-once/assemble-many proposal (port/export-api 6e33818) replaces ToSvgOptions::outlier_trim with a Crop choice. The CLI keeps its behaviour: the default stays the cluster trim (Crop::Cluster), and --no-trim keeps everything (Crop::Everything). This is the only caller of the option in the workspace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package the feature branch wrote from inside `uncad` has no place in the split: `uncad` parses and draws nothing, and the renderer knows nothing of who reads its pictures and bundles no font. It becomes a crate of its own in this workspace, `uncad-export` 0.1.0, depending on `uncad` (path and version, like `uncad-cli`) and on `uncad-model` and `iron-render-cad` by version only. This first step brings the font the package draws and measures text with: `Uncad Sans`, the feature branch's Noto Sans KR subset (Latin, Greek, the 2350 KS X 1001 Hangul syllables, the CAD symbols), byte for byte, with its OFL text, its reproduction recipe and the two scripts that produce it. `fonts::bundled()` hands it to the renderer as `Fonts::Custom`, built once per process, and `fonts::UNCAD_SANS_CAP_HEIGHT` (0.733, OS/2 sCapHeight over unitsPerEm, checked against the embedded bytes) is the value the renderer needs as `ToSvgOptions::cap_height` for a CAD height to be the capitals' height. The README says what the renderer cannot do that the feature branch did: there is no "bundled face first, then the host's" choice, and the renderer's `%%c` (U+2300) is outside the subset. Licences: the crate is `GPL-3.0-or-later AND OFL-1.1`; `deny.toml` grants both to this crate by name (the fourth copyleft grant, which the file says is a decision), `docs/THIRD_PARTY_NOTICES.md` names the font, and `uncad-cli`'s release invariants now cover four crates' `LICENSE` and the font's licence beside the font and in the manifest. Ported-from: 2572d99, ea24fdc, 815ffda, 26bdd8d Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model carries a polyline's vertices with their bulges and computes nothing but a block reference's placement; a length, an area or whether an outline crosses itself is a consumer's derivation. These are the feature branch's, on the model's `PolylineVertex` (a vertex's bulge is the segment leaving it) instead of parallel point and bulge lists: - `bulge_arc` and `polyline_segments`, `polyline_length`, `polyline_signed_area` and `polyline_area`: arcs included; an open polyline's area closes with a straight segment, so the bulge AutoCAD leaves on its last vertex bends nothing; a closed pair of arcs (a DONUT) encloses its real area; a repeated closing vertex adds no segment. `polyline_area` is `None` where the vertices cannot enclose anything. - `is_simple` (quadratic; the caller bounds it), `polygon_centroid`, `point_in_polygon`. - New for the split: `plane_to_world(extrusion, elevation)`. The model now keeps a planar entity's coordinates in its own plane and leaves the map to the world to its consumers; the package takes it from the model's own `Affine2::from_insert` for a unit reference at the plane's origin, so it and the renderer run the one arbitrary axis algorithm the model owns. `map_polyline` takes vertices through a similarity -- a mirror turns a bulge's sign -- and refuses a tilted plane, where an arc is no longer one. The feature's unit tests come along on the new shapes, and the revision cloud of example_2000.dwg is measured against an independent sum of its arcs (tests/geometry.rs). Ported-from: fae3b14, c5ccaf0, 7210517, f34985c Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model keeps every string as the file states it -- MTEXT format codes, `\S` stacks, `%%` symbols, `\U+` escapes -- and what a reader sees is a consumer's derivation, so the feature branch's decoder comes to the export as `text::decode_mtext` / `text::decode_text` (the feature's `text_plain`): paragraphs as newlines, a stack as `a/b` with a space after a digit so `3 1/2"` cannot read as thirty-one halves (`a^b` for a tolerance stack), `%%c` as `∅`, `%%nnn` by code, decorations and fractions recorded, and a character XML forbids replaced by U+FFFD. `\U+XXXX` is expanded here, which is where an R2007+ DXF's strings, which the model keeps raw, get their characters. The renderer has a decoder of its own for what it draws, and does a few things differently (`%%c` as U+2300, which the bundled font lacks; a tolerance stack with `/`). The records keep this one -- the package's own string for its string index and its dimension labels -- and the module says where the two part. The feature's text-box estimates are not ported: the renderer estimates and measures every text it draws now (`Scene::text_boxes`). Ported-from: 96abb6d, 888caf6, b182912 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model keeps what a DIMENSION's file states -- DXF 42 as written
(radians for an angle; 0 and -1 read as not stated), the points by DXF
group (10, 13-16), the text override folded to one value per meaning, the
style's name -- and says nothing about which number to believe or what the
label reads. That is the package's, and the feature branch's rules come
here as `dimension`:
- `measurement_from_points` on the model's points, per kind: a rotated
dimension 13-14 along DXF 50, aligned 13-14, a three-point angle the
sector at 15 between 13 and 14 holding 10, a two-line angle between
13-14 and 15-10 holding 16, radius 10-15, diameter 10-15, an ordinate
13's offset from 10 along its stated axis, an arc length about 15.
Angles in degrees.
- `usable_stored_measurement`: the file's value unless it is not finite,
-1, impossible for the kind (<= 0 except an ordinate) or more than 1 %
from the points (the corpus's worst honest disagreement is 2.3e-7), in
degrees for an angle -- the model keeps the file's value (owner decision
#20); this decides the package's confidence in it.
- `EffectiveStyle::resolve` over the model's all-`Option` DIMSTYLE: a
stated value is taken at its word (DIMDEC 0 included), DIMLUNIT and
DIMLFAC keep their > 0 guard, the husk a file leaves when it only names a
style (no text height and no arrow size) says nothing, the header's
`$DIM*` (`DimDefaults::from_header`, since `uncad::Header` is outside the
model) stand in, then the format's documented defaults; DIMDEC/DIMADEC
are capped at 8, and DIMADEC -1 means "as DIMDEC".
- `format_measurement`, `display_text` and `cached_label`: suppressed,
override (`<>` replaced), the cached *D block's texts, then formatted.
New over the feature (#21): DIMPOST (prefix/suffix, `<>` for the value),
DIMRND rounding and DIMZIN's leading-zero bit, which the model now
carries.
- `tolerance_text_height`: the renderer's rule (stated, the style's text
height, 1), so a TOLERANCE record's height is the one it is drawn at.
Tests: the feature's unit tests on the model's shapes, and on real files
example_2000.dwg's ten dimensions against their points and AutoCAD's
labels ("1504,68", "108°"), the R13/R14 twins recomputed to example_2007's
stored values, 37E's believable disagreement kept, and dimlfac12's "120".
Ported-from: 74bb835, 9c05987, 837fb7d, 80009b4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feature branch decided its crop itself. On the split the renderer does (its proposal's `Crop` and `CropReport`), and the export keeps only what a reader counting patches needs, as `frame`: - `CropMode` (Auto, Raw, Header, Fixed) and `renderer_crop`: Auto is `Crop::Guarded` with the header's `$EXTMIN/$EXTMAX` as the stated extent the renderer may take; Raw is `Crop::Everything`; Header is a `Crop::Window` of the header's extents when they are sane, else Auto; Fixed a `Crop::Window`. `header_extents` reads them from `uncad::Header` (the model carries no header), and `crop_source` names the rule the manifest reports from the renderer's `stated_taken` and content. - `auto_padding` (2 % of the longer side, 24 px at least, half a unit for a point) and `snap_to_lattice` (the pixel size up to the patch lattice, the world rectangle grown to match), unchanged. - `detached_groups`, the grouping that makes a detail beside the plan a frame of its own, on a `BTreeMap` of grid cells instead of a `HashMap` (the workspace forbids std's hash collections: same input, same bytes). The grid is the crop's diagonal times `frame_gap`, so it holds a few hundred cells; the corner-only rule for a runaway extent stays. - `Rect`, the package's rectangle, still serialized as `[x0, y0, x1, y1]` and read from the old object form, converting to and from the renderer's. The feature's guard, header-candidate and cluster code is not ported: it is the renderer's now. Ported-from: 89ac6de, 583882e, 6395009, 0194c0e Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feature branch's export.rs, on the renderer's render-once surface and the model's shapes, as `export_package(db, header, dir, options)` and `export_file(path, dir, options)` (which parses with the header and names the source). This first part writes everything but the tile pyramid and the sheets: - One walk: `Scene::new` with padding 0, the crop mode as `Crop`, the bundled font's cap height 0.733. The overview is `fit_overview` (the profile's edge and patch budget, 2 %/24 px padding, the lattice snap) of the renderer's `crop.content`, drawn as `Scene::png` of exactly that View, every part but the ones the crop left out, re-encoded 8-bit RGB. On example_2000.dwg it is the feature's rectangle to the bit. - Records of what the picture shows (not hidden, not left out). Texts come from `Scene::text_boxes`: every text the renderer drew, found back along its path of IDs (top-level entity, block references, a nested reference's attribute) to its entity, placed through the model's `Affine2::from_insert` and its own plane (`plane_to_world`), with the renderer's measured glyph box -- a text inside a block, a table cell and a TOLERANCE included, a hidden one and a dimension's label not. Dimensions, geometry (a mirrored arc, circle, polyline or solid is reported where the picture draws it), regions with labels, block instances; ids are the model's reference IDs (`293`, `293/270`), with the file's `handle` beside them, and a record's confidence never above the model's. - strings.json (NFKC), shards with `first_key`/`last_key` on the ids' numbers, drawing.json (the `uncad::Header`, layers with their state, block definitions, the reader's diagnostics), drawing.svg as a window of the same scene, entities.json, report.json (left out -- `scale_outlier`, `far_outlier`, `outside_view` -- plus every renderer bound with its cap, hidden counts and ids, unsupported types, empty blocks, unresolved references, the limits), README.txt and the manifest, written last. - Same input, same bytes: no hash collection anywhere, and report.json carries no timings (the feature's `timings_ms` is gone, and the determinism test now compares every file). Tests: the feature's package tests on this API, on example_2000, the fixtures and built drawings, plus a mirrored polyline and a mirrored block placed where the picture draws them. Ported-from: 1d17ff8, d4cddd6, 2572d99, 8d73ab4, 47d0b00, c6f9539, f0bdaae, 775b514, 00414f4, 7c9cb91, 643765e, 2b0f92f Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feature branch's frames and tiles, on the scene the package already walks once: - Frames: the drawn extents -- each part's grown by the measured boxes of the texts it draws, so a long Hangul note is culled by its glyphs and not by the 0.6-em estimate -- grouped by `frame::detached_groups` at `frame_gap` of the crop's diagonal. `f0` is the largest group; a detached one becomes `f1`, `f2`, ... when it holds `min_frame_entities` entities or a text, up to `max_frames`; every other group is in `frames_dropped` with its reason and a warning. One frame reuses the overview; several get `frames/fN/overview.png` each. - The pyramid: levels 2x apart until the dominant text class reaches `target_text_px`, within `max_levels` and the `max_tiles` budget (a level that would overrun it is dropped whole, with a warning, and the frame's `pyramid_complete` says so); SAHI-style tiles with the profile's overlap, the last row and column shifted inward; a tile nothing visible reaches is planned but not written, and `tiles.json` gives its reason. - Drawing: every frame overview and tile is `Scene::png` of its exact View with only the parts that reach it (plus construction lines), on up to 16 threads sharing the scene and the fonts; a thread that panics is an error, not a dead process. Records now list their tiles and carry a clipped pixel box on every image they are on. - Sidecars: position, affines, written neighbours, parent and children, records as positional rows with their `columns`, exact `counts` and `geometry_by_kind`, every layer on the tile; cut to 32 KB compact (geometry first, then the other rows, then the layers, each with its flag). `tiles.json` lists every tile with its bytes and SHA-256. The manifest gains `frames_dropped`, the legibility legend and a guidance that quotes the profile's own tile and overlap. Tests: the feature's tile, frame, sidecar and acceptance tests, including the pixel checks (the far- from-origin grid rasterizing alike, a Hangul note's second half on its tile, a nested attribute's ink on the tile its record names). Ported-from: 583882e, d4cddd6, 8d73ab4, 2afef3a, 14eaeae, 1f49e4d, b28c8a2, 0d8d299, 30c02d3, 0194c0e Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feature branch composited the model through each viewport itself; on the split that is the renderer's (`Scene::layout`, its layout proposal), and the package writes what a reader of sheets needs: - One sheet per LAYOUT with a tab order above 0 whose block the model holds, in tab order, drawn with `Crop::Everything` and no padding, and imaged whole: the renderer's sheet (`SheetSource::Limits` -> `layout_limits`, `PlotSettings` -> `paper_size`), else the paper entities' bounds (`entities`), fitted to the profile's lattice. A file with no layout (R13/R14, a DXF without OBJECTS) has every non-empty `*Paper_Space*` block drawn as a sheet through a layout stated on a copy of the model, one stating no sheet, so it is framed on its entities. A sheet with no usable rectangle, or one the renderer refuses, is a warning, not a failed export. - `sheets.json` / `SheetReport`: every VIEWPORT of the sheet with its ID and handle, the file's number, on, overall, composited, frame, scale and twist -- from the map the renderer drew the model through when composited, from the model's view otherwise -- the model window, and the map itself as `model_to_paper` [a, b, c, d, e, f] in the package's own row-major form, which the file's prose now states. - The sheets' own texts -- the title, the title block through its INSERT -- are text records with `space: "paper"`, their sheet, and a pixel box on that sheet's image: the renderer's texts whose path does not start at a viewport, placed on the paper. They are in texts.json, the string index, `counts.texts_paper` and `capabilities.paper_text`. - Each sheet render's bounds are summed into the report's limits. - Sheet directories: the layout's name sanitised, cut to 100 characters, suffixed when two collide. Tests: the feature's sheet tests on this API -- example_2000's two Letter layouts, colliding Hangul names, the twisted viewport's map and its ink, the plot origin, a paper hatch keeping its own pattern, one viewport per state, construction lines staying inside their viewport, the title block's texts on their sheet -- and a paper block without a layout. Ported-from: 1f08658, f9af522, 7ff5c70, 72cedb9, a4a137a, 69b2edf, 7761c75 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`uncad export <input> -o <dir>` parses the drawing with its header and hands it to uncad-export, with the profile, the pyramid depth and tile budget, the sheet images and the SVG copy as options. An option the subcommand does not know is refused by name instead of ignored. Ported-from: 22d6677, 44b131e Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not know The plain command used to skip an unknown option, a second input and an option missing its value in silence; each is now an error that names it. `--version` answers on both commands, and `--include-hidden` passes the renderer's include_hidden through. Ported-from: 22d6677, 44b131e Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: the package as a third output, the export entry point beside the parser and the renderer, the export subcommand, --include-hidden and --version, and the crate in the repository layout. ARCHITECTURE: the parser's current modules (text.rs, header.rs; hatch_color.rs is gone), the tests the parser now carries, and the export crate as the consumer that holds the derived values the model does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package format uncad-export writes is specified in the design the feature work followed. Bring it over with a note on where each part now lives (uncad-export, the model, the renderer) and that its measured numbers were taken on the earlier monolithic layout. Ported-from: 85e66d1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CAVEATS section for uncad-export: the crop decided before text is laid out, glyphs outside the bundled subset, tile cost, the sheet compositing rule, record ids from the model, and how little of the corpus the port has been compared on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release build, four drawings, the new export against the earlier monolithic one: 17 to 41 per cent slower. Not profiled yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PORT_STATUS.md says what the port is, how to build the three repositories together without committing the local patch, what is done and what remains in order. UPSTREAM_PR_DRAFTS.md carries the three pull request descriptions; the corpus comparison script comes along. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Merged into main as 85108ae (absorbed with follow-up adjustments). The export crate is not part of this merge — it will move into a separate permissively licensed component, so the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge order: 1. iyulab/uncad-model#1 2. iyulab/iron-render-cad#1 3. #1
What this adds.
crates/libredwg-sys/NOTICE.md.build.rscounts their markers so a re-vendor cannot drop one silently:come out empty into an error. The 27 readable corpus files match their DWG twins.
parse_bytes,FormatandParseError::Ioare added, and a non-ASCII pathnow opens.
uncad::Header, returned beside the model byparse_with_header. Unstatedvalues are
None.POLYLINE_MESH, true colour, layer state, layouts and viewports are read.
Subentity walks are bounded.
crates/uncad-exportis new. It writes the LLM/VLM package: overview, tilepyramid, sheets, records, strings index, manifest. It bundles a Noto Sans KR
subset under OFL-1.1, granted in
deny.toml.uncad export,--versionand--include-hidden. Unknown optionsare now errors.
Needs your decision. Three reverse documented positions.
measured and pinned by a test.
twin comparison as evidence.
Tests. 304 in the workspace, the CLI's 23 included. The corpus sweep pins were
re-measured, with the reasons in the commit messages. On five drawings the export's
counts match the earlier monolithic implementation. The two differences come from
your attribute walk reading more ATTDEF/ATTRIB records, which is intended.
Release order when you are ready. uncad-model 0.2.0, then iron-render-cad 0.2.0,
then libredwg-sys 0.3.0, uncad 0.3.0, uncad-export 0.1.0 and uncad-cli 0.3.0. Publish
from a clean checkout without any local
[patch].🤖 Generated with Claude Code