- #2587: a frame turned by anything but a quarter turn keeps its size about its turned
centre; the box of its two mapped corners collapsed it (a 10 x 10 square turned 45° became
14.14 x 0, and the OLE header with it) for EXPLODE, REFEDIT, paste and array placement
(austin62jake, CodeRabbit). Moves, scales, mirrors and quarter turns keep the exact
corner mapping. `recase` reads only the rewritten digits and a few after them, not the
whole 310 line (austin62jake). The PR's rotated-frame test expects the kept size.
- #2585: an `output` that contradicts the path's extension is refused, as a device that
does is; it lost silently to the extension.
- #2608: the window's state is dropped when another dialog took its place (it missed a UI
pass), so reopening reads the drawing instead of an old unsent edit (CodeRabbit); TEXT and
ATTDEF get a one-line field (Enter commits), so a line break can't turn into a space on
save; the module comment says the shortcut menu opens it.
main added view twist. Kept from both: a drawing opens on the view it was saved at, with its saved twist; one that stores none fits its extents through main's twisted box. The VPORT reader takes main's twisted view centre and keeps this PR's first-*ACTIVE view; the writer writes the saved view, else main's twisted fit, with the fallback still fitting the width through the record's aspect.
The renderer draws a zero-width frame as a line, so leaving it out of the hit geometry made a
visible object unpickable. Also corrects where the corner tolerance switches to ulps (4 ulps
pass 1e-6 near 1.1e9).
The ulp term takes over from CORNER_EPS from about 1.1e9, and the doc now says
so. The case of the nearest letter for each hex digit is found in two linear
passes instead of a search per digit; same result, covered by
moving_an_ole_frame_keeps_each_hex_digits_case.
Putting the frame in prims() also made it a TRIM/EXTEND edge, an object snap
target and dimension geometry. prims() is back to nothing for it, the extents
arm is restored, and select::hit_polylines adds the frame's outline for picking
(none for a frame with no width or height or a bad corner).
Tests: clicking_an_ole_frame_edge_selects_it (now also a frame with no width),
ole_frame_is_not_a_trim_edge_or_snap_target.
The corner match was capped at 1e-6, under one ulp above about 8.6e9, so a frame
there never matched its header; a few ulps of the larger value now also match.
The rotation bound had a floor of 1 on the scale, so a small real turn at a tiny
scale passed; it is now purely relative. Hex digits are re-encoded in the case of
the digit at the same place, so untouched bytes keep their text.
Tests: ole_frame_moves_at_2e10_with_a_one_ulp_text_corner, ole_frame_refuses_a_small_turn_at_a_tiny_scale, moving_an_ole_frame_keeps_each_hex_digits_case.
An OLE2FRAME is drawn but prims() gave nothing for it, so hit testing never saw
it and a click on its outline picked nothing. prims() now returns the frame's four
edges from the same corner helper the extents use, so the extents arm is gone.
The renderer keeps its closed-polyline arm (one joined outline, not four segments).
Test: clicking_an_ole_frame_edge_selects_it.
ALIGN builds its matrix from two angles that agree only to rounding, so a large
scale leaves a rotation part of about scale * 1e-16 that an absolute 1e-12 bound
refused as a turn. The bound now grows with the scale; a real turn is still refused.
Test: ole_frame_accepts_a_large_scale_with_rounding_noise_in_its_rotation.
The extents took the first copy and the far corner copy only. Turned, the grid's
other two corner copies lie outside that box, so ZOOM Extents cut them off. All
four corner copies count now, and the counts are clamped to the 10,000 columns
and rows the renderer draws. Found by CodeRabbit on #2756.
main's #2673 runs the DWG writer in a child process. The kept version now goes with the request
(encode_kept_noted, worker protocol 2), so the worker reads the bridge DXF as that version too.
The proxies proxy_classes leaves out are now counted in Unwritten, as ACAD_PROXY_OBJECT and
ACAD_PROXY_ENTITY.
* MINSERT: a block reference repeated in rows and columns
MINSERT asks INSERT's questions, then the grid's as AutoCAD 2024 does
(measured with accoreconsole): the numbers of rows and columns (1 to
32767), the distance between rows or a unit cell when there is more than
one row, the distance between columns when there is more than one column,
then the attribute values once. A distance along an axis with one copy is
0, and one row by one column is a plain reference.
EXPLODE leaves a grid whole ("The object is minserted.") instead of
exploding its first copy and dropping the rest. MIRROR mirrors a block
reference as AutoCAD does, negating the X scale factor and a grid's
column distance, so a mirrored grid's rows stay on the right side. An
attribute with no default is asked without "<>".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MINSERT JSON: refuse a distance that is not a number
`rowSpacing` and `columnSpacing` went through `f64_or`, which takes any
value that is not a number as the default, so `"rowSpacing": "15"` stored 0
and put every copy on top of the others without an error. They are now
checked as `rows` and `columns` are: absent or null is 0, a finite number is
kept, anything else is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A sign in a hex pair is no longer accepted; only the 310 groups holding the 98-byte header are decoded and rewritten, in place and without cloning the payload; the corner lookup is one allocation-free pass; the io ole_frame test module is not built for wasm like its DWG siblings; Unknown::is_ole_frame replaces five copies of the type check. Tests: a_plus_sign_in_ole_data_is_not_hex, a_large_ole_payload_only_rewrites_its_leading_groups.
EXPLODE, REFEDIT, paste and array placement call EntityKind::transform with rotations and mirrors, which left groups 10/20 not upper-left and the data header not matching. ole_frame_transform now takes the box of the two mapped corners (upper-left, lower-right) and writes the header as UL, UR, LR, LL of that box. Test: a_rotated_frame_stays_axis_aligned_and_consistent (rotate 30 and mirror).
The relative tolerance grew with the coordinate: at 1e9 a payload 0.5 off the frame still matched and was rewritten. It is now capped at CORNER_EPS (1e-6), still above an ulp at 1e9. Tests: a_half_unit_offset_at_large_coordinates_is_not_the_frame, exact_corners_at_large_coordinates_still_move.
ole_frame_accepts took only a translation, so SCALE on a selection with a frame failed as a whole. A uniform positive scale keeps the frame axis-aligned and upper-left, so the corner rewrite handles it; rotation and mirroring are still refused. Tests: scale_carries_an_ole_frame_with_the_rest_of_the_selection, mocoro_scales_an_ole_frame, ole_frame_accepts_translations_and_uniform_positive_scales_only.
* Dynamic blocks: index kept objects once per listing and copy each value record once
Listing blocks rebuilt a handle index of every kept foreign object for each
block with an extension dictionary, multiplying the block count by the object
count. The listing now builds one ObjectIndex and asks is_dynamic_block_in per
block; dynamic_block answers None before indexing when the block has no
extension dictionary.
templates() copied a value record's tags once per dictionary key, so a record
listed under many keys was copied many times, and it looked up each
dictionary entry by scanning every object. It now resolves entries through
its handle index and copies each record at most once (CWE-400, CWE-407).
* Dynamic blocks: list a block as dynamic from its graph object without walking the graph's members
* Multilines: find each segment's usable neighbour direction in two linear passes
MLine::miters searched the direction array forwards and backwards from every
vertex for the nearest non-degenerate segment, so a long run of coincident
vertices made it quadratic. The nearest usable directions ahead and behind
are now computed once, in one backward and one forward pass (CWE-407).
* Multilines: test that a run of coincident vertices at a corner takes the corner's miter
Each top-level bounds call started with an empty block cache, so measuring a
space's extents or building a spatial-index chunk measured a block definition
again for every reference to it: R references to a block of B entities cost
R x B. BlockBoundsCache keeps the per-depth block bounds (separately for drawn
bounds and extents, which differ on layers turned off) across one bulk
operation; Drawing::extents and the spatial index use it (CWE-407).
Region::renest tested every loop's probe point against every other
tessellated loop, scanning all of its edges each time, so the work was the
loop count times the total vertex count. It now skips loops whose bounding
box does not hold the probe, and stops once MAX_RENEST_EDGE_VISITS edge tests
have been spent, leaving the flags unchanged and returning false; UNION,
SUBTRACT and INTERSECT then report the regions as too complex to combine, as
they already do for too many boundary pairs (CWE-407).
A DIMENSION moved by an older AutoCAD keeps its *D block contents where
they were and the move in DXF group 12, and is drawn translated by the
insertion point. Group 12 was never read into Dimension, so the block
rendered at its pre-move position and the extents reached there too.
Read group 12 into a new Dimension::insertion_point, draw the block
translated by it, move its bounds with it in the extents, test the
block's text where it draws, and write group 12 back whenever the block
written is the one read from the file. Regenerated geometry (edits drop
the block link) sits at its definition points, so no offset is kept or
written for it.
Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
* Dynamic blocks: a point on a frame edge of any length is on it
The frame edge test used Line::dist, which takes a segment shorter than
EPS (1e-9) for its first point: a point halfway along a frame edge that
short was neither on the edge nor, by the even-odd test, inside, so its
line moved whole instead of stretching. The distance to an edge is now
the exact one, clamped to the segment.
Tests also cover two rules from #2627 that had none: a point on an
edge's line but past its end is outside the frame, and a polyline
tilted by only 5e-10 (about a unit off at an elevation of 2e9) keeps its
stored vertex list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dynamic blocks: measure a frame edge's residual from its start
segment_dist added the projection back to the edge's start before
measuring, so near x = 36 the residual was rounded to that magnitude's
spacing (7e-15). A point 1.4e-15 off an edge 5e-10 long, outside the
frame, then came out 4e-20 away, within the edge's 5e-19 tolerance, and
was taken as on it. The residual is now measured from the edge's start,
where those digits are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every LAYOUT was written with limits 0,0 to 12,9 (AcDbLayout groups
10/11) and paper image origin 0,0 (groups 148/149), whatever the file
had, so another program drew the sheet elsewhere and a Layout plot came
out shifted. Layout::limits and PageSetup::image_origin keep what the
file had (finite, within 1e12) and a save writes it back; a layout
without them writes the old defaults, which read back as none.
Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
From 2013 the DWG keeps another program's proxy entity with the class its group 91 names, so the
loss report skipped every ACAD_PROXY_ENTITY. Saving drops a proxy whose group 91 names no class of
the file's CLASSES, though, so that one is now counted. The group 91 helpers move to
cadcraft_doc::foreign so the engine can ask Foreign::proxy_class.
* Dynamic blocks: stretch the points inside the frame, as AutoCAD does
A dynamic block made by a third-party tool came out distorted when a
distance changed: its stretch action has distance multiplier 0 with
angle offset 1, and stored vertex lists that don't match its objects.
apply turned the 0 into 1 but kept the angle (a 57 degree slant), and
moved exactly the listed points.
AutoCAD 2024, on a copy of that drawing, moved the block straight along
the parameter, and every point of its lines and polylines exactly when
it was inside the frame, whatever the lists said. So:
- a zero multiplier moves as 1, without its angle offset;
- a line's or lightweight polyline's moving points are those inside
the frame where the dynamic block has them; a selected one wholly
outside the frame moves whole, as in AutoCAD's own stretched copies;
- a frame of fewer than 2 or more than 1,000 points keeps the stored
lists, so a huge frame can't make every point test every edge.
Against AutoCAD's own anonymous copies in two library drawings (556
references set back to their block's distances), 249 come back exactly
on main and 258 with this change; none stops matching.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dynamic blocks: a point on a polygon frame's edge is inside it
in_frame counted a point on a two-corner frame's edge as inside, but
the even-odd test of a polygon frame put one on its right edge outside.
A line whose end lay there, with its other end outside, then had no
point inside the frame and moved whole instead of its end alone.
A polygon frame now counts a point on an edge (within 1e-9, relative
to the edge's length) as inside too, as the two-corner frame does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dynamic blocks: frame edge tolerance by the edge's length; tilted polylines keep their lists
A point counted as on a polygon frame's edge within 1e-9 * (1 + the
edge's length), so never less than 1e-9 drawing units: in a tiny frame,
a point visibly outside was taken as inside. The tolerance is now a
billionth of the edge's length.
A lightweight polyline's vertices are in its own plane. The reader
flips a mirrored one (extrusion along -z) into the block's plane, but
leaves a tilted one's in its OCS, which the frame (block coordinates)
can't be tested against. A tilted polyline keeps the stored vertex
list, as before the frame test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* dynblock: only an exactly untilted polyline is tested against the frame; a tilted one keeps its stored list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* feat(engine): INSERT of a drawing file keeps an attribute definition's field
The attribute definitions of a drawing file read by INSERT name=file lost their
extension dictionary, so the ACAD_FIELD dictionary and the FIELD objects of an attribute
whose value is a field (the file name in a title block) were gone from the block.
adopt() now copies that object tree to the drawing under new handles, with the FIELD
class and a field list if the drawing has none.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* foreign: index the kept objects once when walking and copying a dictionary tree
A file can keep many objects; a linear lookup per pointer grew with their product.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: andrewbrake <6925387+andrewbrake@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Polyface meshes stay one object through DXF and DWG
A polyface mesh (POLYLINE flag 64) was read as loose 3D faces. It is now
read, drawn, exploded into its 3D faces and written back as a POLYLINE.
Hidden edges aren't drawn. A mesh too large for the 16-bit groups 71-74
is saved as 3D faces.
Split from #1005.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* A polyface face ends at its first zero index
A face record with a gap ([1, 2, 0, 3]) has two corners and is dropped,
not read as a triangle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Polyface meshes: write only faces that name their vertices; ID gives their Z
The writer drops the faces the reader would drop, so no record names a
missing vertex, and the 16-bit check only needs the counts. ID and DIST
give a mesh the Z of its nearest visible edge, as they do for 3D faces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
There was no OOPS. It now brings back what the last ERASE removed (the
Delete key too), or BLOCK deleting the objects it makes a block of, as
AutoCAD 2024 does (accoreconsole, the drawing's entnext order read
back):
- Each object keeps its handle and goes back directly above the object
that was under it, or under the one that was above it when that is
gone. An erase records both neighbours (EntityStore::neighbours).
- Only the last erase counts; entdel doesn't. OOPS says nothing and is
one undo step.
- An object stays out when it is already in the drawing, when an undo
took the drawing back to before it was made, when its layer, block or
layout is gone, and, during REFEDIT, when it isn't in the working set.
REFCLOSE forgets the working copies; the Block Editor has an OOPS of
its own.
- The command's label is in the 17 interface catalogs.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* SAVEAS portableFonts: Windows fonts for Linux text style fonts
A text style using a Linux TrueType font (Liberation, DejaVu, Open Sans)
shows in AutoCAD on Windows with a substitute it picks. A save now names
those styles on the command line (and as `nonPortableFonts` in the
result), and SAVEAS {"portableFonts": true} writes the Windows equivalent
in the file (Liberation -> Arial, Times New Roman, Courier New; DejaVu
Sans -> Verdana; Open Sans -> Arial) while the drawing keeps its own
fonts. Narrow and condensed families have no Windows stand-in (Arial
Narrow comes with Office) and are kept and named. QSAVE takes no SAVEAS
options.
Tests: stand_in_tests in cmd/file.rs, tests/save_portable_fonts.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts kept in the file, applied by the writer
Reworked after review. SAVEAS `portableFonts` writes a copy of the drawing
with `Drawing::portable_fonts` set, and the DXF/DWG writer turns each
style's Linux font into its Windows font, keeping the style's own font in
the CADCraft FONT xdata already used for the built-in font. Reading the
file gives the Linux fonts back (unless the font was changed since, in
AutoCAD); the drawing itself is never changed.
It applies to that save: a later QSAVE writes the drawing's fonts and
says so, naming the styles and SAVEAS portableFonts. The family table
moves to `cadcraft_fonts::windows`, used by the writer and the note.
Styles from an xref aren't saved and aren't named. The parity docs are
left to the maintainers (CLAUDE.md).
Tests: crates/io/tests/it/portable_fonts.rs (DXF, DWG, a font changed in
AutoCAD), crates/engine/tests/save_portable_fonts.rs, windows.rs tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts: match the Windows font by file name
A STYLE whose Windows font carries a path (C:\Windows\Fonts\…) still
gets its own font back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts: say the font list again only when it changes
A save still returns nonPortableFonts, but the command line note isn't
repeated on every QSAVE of the same drawing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
main's conformance matrix (#2237) compares page setups across saves; file_paper is the file's own groups 44/45/73, so it compares the setting without it, as tables_objects_roundtrip already does.
The data layout is only known when its corners match the frame's groups; any other
header is left byte-identical while the tags still move. Test: move_leaves_ole_data_that_is_not_the_frames_corners_alone.