EXTEND on an open polyline ran a ray along the chord of an arc end segment
and kept the old bulge, so the arc changed radius and centre, or the
boundary was reported missed when only the circle reached it. The arc end
segment now runs on round its own circle to the nearest crossing ahead,
turning the same way, and its bulge becomes that of the total turn, as
EXTEND does with an ARC. The polyline also keeps its constant width,
elevation and linetype generation, which the rebuilt polyline dropped.
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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
* fonts: scan the Android system font directories
System fonts are the only fallback for text whose own font lacks a glyph
(CJK in an SHX big font, for example), and Android was falling through to
the XDG directories it does not have: a drawing came out in the stroke font
with missing-glyph boxes for Chinese. Search /system/fonts and the product
and vendor partitions too.
The directory list moves out of scan() so the platform's main location is
tested directly.
* fonts: scan the HarmonyOS system font directories
HarmonyOS targets are target_os = "linux" with target_env = "ohos", so they
fell into the XDG branch and found no fonts at all: Chinese text in a drawing
came out as missing-glyph boxes while the stroke font drew the ASCII around
it. Search /system/fonts there too, as on Android.
* contributors: add qinshah
Credits them by the name on their public GitHub profile.
* fonts: document the platform font directories
* constraints: evaluate a named expression without copying its text
Env::value cloned the stored expression before parse rejected it as too
long, and failures are not cached, so an oversized imported expression
referenced from many dimensional constraints was copied on every
reference. Parse the stored text in place, and keep only a bounded
prefix of an over-long expression when it is inserted (it still fails
as too long).
* constraints: test repeated evaluation of an oversized named expression
The tag list for a counted ASCII file is reserved once with a fallible
allocation, so where the system refuses the memory, opening fails with an error
instead of ending the app. Growing it would also take half as much again for a
moment. 100 million rather than 150 bounds a crafted 512 MB file to about 4 GB
of tags; CADCRAFT_MAX_DXF_TAGS raises it, and the error says so.
* 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
From review of #2670. Changing SAVEFILEPATH released the old folder's lock while
files remained there, so another app could offer a running app's drawings. A
write failing while the app was idle waited for the next input to be shown. A
second drawing's failure stayed hidden behind a dismissed notice.
Endpoint residuals rebuilt a fit spline through a dense interpolation
solve on every evaluation, so many Tangent/Smooth constraints on one
spline, or numerical differentiation along other objects' parameters,
repeated identical O(n^3) rebuilds; solving had no budget for them.
purge_invalid built a fresh model per constraint, re-sampling the same
block for every reference, and an Insert naming no block scanned every
block definition case-insensitively.
- Models built for one operation share a Prep: block samples are taken
once per block (in block coordinates, transformed per Insert), block
names resolve through a lazily built case-insensitive index, and the
last rebuild of each spline is reused at the same defining points.
- Comp::run and the Fix redundancy check refuse a system whose
per-iteration residual work, including spline rebuilds, exceeds the
limit DOF analysis already applied (MAX_EVAL_WORK).
- Spline templates keep only what a rebuild reads: no fit points for a
control spline, no control net for a fit spline.
- purge_invalid validates every constraint with one Prep.
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).
next_name tested up to 100,000 candidate names, each against every
parameter and constraint name, so a drawing with many occupied names
made adding one dimensional constraint quadratic. Collect the numeric
suffixes in use once and take the first free one.
* DWG reader and writer run in a child process (E7)
An allocation failure or stack overflow in the third-party code ends its
process, which catch_unwind can't stop, so one bad file took every open drawing
with it. The app and CLI now run them in a copy of themselves over pipes. A
worker that dies or runs past 30 minutes costs that one open or save. If no
worker can be started, the work stays in the process, as before.
* DWG worker: keep the reply's bytes instead of copying them; skip the test without a bridge
The frame now runs under a guard. A panic autosaves unsaved drawings at once,
even with SAVETIME 0, and shows a notice that doesn't block saving. A panic
hook writes a local report for every panic. Panics that keep coming stop the
interface and leave a window that closes, with the recovery copies kept.
* Autosave: a failed write is shown until one works (E6)
A failure only reached the log, so someone could work for hours with no
recovery copy. The autosaver keeps why each drawing's write failed; the app
says it once at the command line and shows a notice that doesn't block saving.
It ends when a write works or the drawing no longer needs a copy.
* Crash recovery: offered at the first relaunch, not 5 minutes later (E5)
A crashed app's lock file looked alive until it went stale, so a relaunch
within 5 minutes skipped its autosaves, and a suspended app's looked dead.
A running app now holds a system lock on the file, which the OS drops however
the process ends. File age still decides where locks don't work and for lock
files of older versions, which don't record holding one.
The folders are made by a new writer only user-named targets use, so autosave
and a dangling symlink's target keep the strict write_atomic. Save, Save As,
Export, Plot, WBLOCK, layout templates, render and the CLI report the topmost
new folder at the command line and in createdFolder.
* DWG: a table cell's text height is an override only when the file flags it (#2501)
acadrust's DXF writer writes group 140 for any cell with a style record, even a
default 0.18 with no override flag, and CADCraft read it as a per-cell height,
so cells ignored the table style's height. The bridge now clears the height of a
cell style that has neither the TEXT_HEIGHT property flag nor the content's
height override bit (0 reads as no override).
* Parity checklist: regenerated after merging main
* Tests: inherited table cell heights must stay unset (review on #2588)
A cell that inherits its height must read back as None, not an explicit 0.0625 override.
* Parity checklist: regenerated after merging main
* Test: a cell style with the TEXT_HEIGHT flag, content override bit 128 and a cell with no style record keep the right height (review on #2588)
* DWG: a table cell's content height override (group 144) is kept (review on #2588)
* Parity checklist: regenerated after merging main
* DWG: table cell height takes the first content's override and the style's override flags (review on #2588)
Only the first content's bit-128 height stands for the cell (one height for its text); an
invalid content height no longer keeps the writer's default 0.18; a height marked only in
the style's override flags (newer files) is kept.
A damaged DWG opens partially since #859 and the drawing's read notes
say how many object records were left out. OPEN echoes them and MCP
`open` returns them as `warnings`, but `cadcraft-cli info` and
`convert` stayed silent: a corpus round trip wrote thousands of skipped
records away without a word.
- convert: each read note goes to stderr as `IN: <note>`, next to the
skipped-records warning (#872). The exit code stays 0.
- drawing.inspect (and so info, MCP inspect_drawing and the control
channel): new field `readNotes`, next to `skipped`. Existing fields
are unchanged.
- run already prints OPEN's notes with the session log.
- README and docs/control-protocol.md say where the notes appear.
Part of #1039.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* render: a display list past the block-expansion budget says what it left out
* Report viewport truncation only after the expansion budget is exceeded
* fmt: keep ui-egui control.rs rustfmt-clean
Found while testing crates/io/src/dxf_write.rs:1964-1970 (xrecords written) and crates/io/src/dxf_read.rs:2049-2052 (apply)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* DWG: scale actions keep the byte AutoCAD reads after their connections (acadrust 0.6.5)
AutoCAD's DWG scale action (BLOCKSCALEACTION) ends with one byte after its
three connections that DXF doesn't carry. acadrust 0.6.4 neither read nor
wrote it, so AutoCAD read past the object and refused every DWG CADCraft
wrote with a dynamic block that scales ("Error Decrypting Data"), even an
unchanged one. acadrust 0.6.5 keeps the byte (dwg_tail_byte).
0.6.5 also maps the dimension flags byte's bit 0 to text_user_positioned
both ways, so the bridge no longer inverts it (it would now double-invert);
it still sets bit 1, which acadrust keeps no DXF source for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DWG: acadrust 0.6.6; multi-layout DWGs get unique paper space names
0.6.6 keeps the scale action byte and the dimension flag mapping this branch
relies on, and writes only the active layout as *Paper_Space (others get
unique *Paper_SpaceN names; hakanaktt/acadrust#143), so ACadSharp opens a
DWG CADCraft saves with more than one layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* io tests: a scale action survives CADCraft's own DWG save
The cadcraft-dwg test builds its DXF with acadrust's writer; this one goes
through cadcraft_io::write (the bridge DXF, then acadrust) with a scale
action added to the synthetic Door fixture, and checks the DWG holds it
with its connections and reads back as the same action.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ui: block description field ids from Ui::scope_id (egui 0.37 deprecates Ui::id)
main's gate fails clippy -D warnings on the one Ui::id() call (#2113);
Ui::scope_id is its new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DWG: the scale action byte doc names acadrust 0.6.6, the version in use
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Docs: leave the parity docs to the maintainers (AGENTS.md: PRs don't edit them)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
* OVERKILL: objects within the tolerance are duplicates wherever their numbers fall
OVERKILL compared objects by rounding every number to a multiple of the
tolerance. Two numbers closer than the tolerance on either side of a
rounding step round apart, so the objects were kept: at 0.01, circles at
x = 80.001 and 80.003 were merged but circles at 50.004 and 50.006 were
not. Arcs on one circle were bucketed the same way.
Duplicates are now found by shape (the JSON without its floating-point
numbers; counts and flags must match exactly) and a weighted sum of the
numbers on a grid of the tolerance, looking in the neighbouring cells too,
then compared pairwise to within the tolerance. Arcs and circles look in
the neighbouring cells of centre and radius and are grouped with a
union-find. Lines were already grouped by sorting and are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* OVERKILL: within the tolerance doesn't chain
Arcs and circles were grouped with a union-find over pairwise matches,
so A ~ B and B ~ C put A and C in one group even when A and C are
farther apart than the tolerance: a circle C was deleted for matching
only the arc between it and the kept circle A, and arcs were combined
across the chain. Each object now joins the first group whose every
member is within the tolerance of it, or starts its own.
In the duplicates pass, a followed object displaced an earlier keeper
that later objects had already matched, leaving them deleted up to twice
the tolerance from what stays. Followed objects are now looked at first,
so every deletion is within the tolerance of an object that stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pt-BR): translate the rows the sibling catalogs cover
The Brazilian Portuguese catalog covered the reference catalog and every marked UI literal, but 191
strings that the German, French, Swedish and Ukrainian catalogs already translate still fell back to
English: the Hatch and Gradient dialog, the Block Editor and dynamic-block grips, the plot and drawing
units dialogs, multileader editing, selection preview settings and the unit names.
Rows follow the catalog's vocabulary (dimension = cota, hatch = hachura, grip = pinça, layout =
apresentação, multileader = multilinha, xref = referência externa), keep the prompt keywords, SPDX
tokens and technology names in English, and are appended after the existing entries.
* docs: leave localization parity updates to maintainers
* feat(inquiry): AREA, PERIMETER and DISTANCE system variables, as AutoCAD sets them
AREA, DIST and MEASUREGEOM now leave AREA, PERIMETER and DISTANCE for
getvar, read-only and not saved: the area just measured (the running
total in Add and Subtract mode) with the last area's perimeter, and the
distance (the total of Multiple points). After DIST's two points
LASTPOINT stays at the first point, as measured. AREA's Object option
measures splines, closed by their chord like open polylines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: spline area over narrow spans, finite distances, list order
- AREA of a spline samples it span by span (Spline::tessellate_spans), so
a narrow knot span no longer drops out of the area. The spread over the
spans is one helper shared with Spline::bounds, whose boxes are
unchanged.
- DIST refuses a distance that isn't finite on all three paths and leaves
DISTANCE as it was.
- sysvars::list puts the session's values after the drawing's header, so
it shows what get returns (AREA, ORTHOMODE, LWDISPLAY...).
- LASTPOINT after DIST's Multiple points is pinned to AutoCAD 2024's
measured behaviour: the last point entered, also after Length and Undo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: measure every knot span of a spline in AREA
A spline with more knot spans than half the sample cap fell back to an
even grid over the domain, which can step over a narrow span and measure
the wrong area: a 100 x 100 square as a degree-1 spline, its right and
top edges two spans a millionth wide after 4,500 spans along the bottom,
measured 4,998.75.
Spread the samples over the spans at any span count: at least one
segment per span, within the cap up to MAX_SAMPLES spans and one each
past it, as Spline::length integrates every span. One evaluation per
span costs 85-285 ms on a million spans in a release build, against
32-52 s for the length AREA already takes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: share AREA's spline samples by how much each span bends
Past the sample cap, tessellate_spans gave each knot span one segment, so
a bent span among many straight ones was measured by its chord. Example:
a quadratic spline along the x-axis, one control point 1000 above it,
past 4,500 spans. It measured 750; the area is 1750/3.
Each span now keeps one segment. A span whose control points are
collinear is straight, so its chord is exact. tessellate's count is
shared among the spans that bend, by the cube root of a measure of their
control points' convex hull, which holds the span and its chord. The
shares are rounded on the running total, so they use the whole count
however many spans share it. The example now measures 583.33343, below
the cap and past it.
The spreading by parameter width that this replaces goes, and
span_params with it: bounds is main's code again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DIST: a measurement refused as too large leaves LASTPOINT at the first point
Session::feed sets LASTPOINT to the second point before DIST checks the
distance, and the refusal returned before DIST put the first point back,
as it does after a measurement. It now does so before refusing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* AREA: a spline too long to measure is too large, not a perimeter of 0
Spline::length reads a length past f64's range as 0, and AREA took that
as the spline's perimeter. Spline::checked_length returns None there,
and AREA reports the perimeter too large, as it does a polyline's.
The overflow was hidden twice: in the sum, and before it in the
derivative, which read as zero speed once past f64's range. The length
integral now takes the derivative before that (Spline::velocity), so an
overflowing speed counts too. param_at_length keeps the speed it had,
and finite lengths are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Say "or" once in prompts that offer options
Twelve prompt messages ended in "or", and the command line adds "or"
before a prompt's options, so they read "or or": HELIX height, XLINE angle
and offset, BLEND, four LENGTHEN prompts, two NCOPY prompts, PEDIT and
table breaks. They now read as AutoCAD's do.
The interface catalogs are keyed by these messages: each key loses its
"or" and each translation its trailing word for "or"; where the shorter
key already had a translation, the longer row is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Say "or" once in HELIX's radius prompts; rename the Vietnamese keys
HELIX's base and top radius prompts ended in "or" before their Diameter
option too, so they read "Specify base radius or or [Diameter]". They
now read "Specify base radius or [Diameter] <1.0000>:", and their keys
lose "or" in all 17 catalogs, as the other twelve did.
The Vietnamese catalog, merged from main, gets the same renames as the
other catalogs; where the shorter key already had a translation, the
longer row goes. The unused "Specify rotation angle or" row goes from
every catalog too: RECTANG has asked "Specify rotation angle" since
#2554.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Write option prompts without "or", as AutoCAD does
AutoCAD writes " or [" only when a prompt asks for something besides its keywords ("Enter block
name or [?]"); a prompt asking for one of them has none ("Enter an option [...]", "Enter layout
option [...]", "New color [Truecolor/COlorbook]"). The 27 messages read without "or" from
AutoCAD 2024's command line are listed in NO_OR and displayed that way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: the window's command line drops "or" as the prompt does
The command line in the window draws a prompt's keywords itself and
wrote " or [" after every message, so -LAYER still read "Enter an option
or [?/Make/...]" there, and questions such as AUDIT's had "or" too. Only
the prompt text (the control channel, the state JSON, the tests) had
dropped it.
Prompt::keyword_lead gives what goes before the keywords; display() and
the command line both use it, and the command line translates only
" or [". A render test reads what the command line draws, in English and
German, for -LAYER, LINE and AUDIT. Suggested in review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The interactive save only knew about DWG, so overwriting a 2013+ DXF lost SAT-text
solids on a command-line note. loss_on_save now lists them, and EXPORT reports them
with the DXF wording.
The Chinese catalog lacked 74 interface strings: the block editor
prompts and dialog, the alias editor sentences, the selection visual
effects and preview filter, units of new drawings, and the menu names
Dimension, Draw, Modify and View.
Adds them in the catalog's TSV format with the AutoCAD zh-CN vocabulary
the catalog already uses (夹点, 图案填充, 外部参照, 多行文字, 孤岛).
After this the report lists nothing missing for zh.
Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
`double_clicked_text_takes_ime_composition_and_commit` commits 中文 at the
command line. Without a CJK font (no CRAFT_FONTS_DIR, none installed, as on
a plain Linux CI image) the kittest harness panics on the first missing
glyph: "No glyph for 中 in Proportional". Code and tests must work without
craft-fonts.
The test now asks the harness's own fonts whether they can draw 中文 and,
only when they can't, lets the command line draw boxes for it. The IME
path it checks is unchanged: the pre-edit is shown and not saved, the
commit is inserted once, Enter stores 中文 in the text. With a CJK font
the harness still fails on any missing glyph.
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* plot and exportpdf help name the `device` parameter (#2557)
`cadcraft-cli commands plot` didn't list `device`, so the only way to find it was the `unknown plotter` error.
PLOT's help now lists `device` with the built-in devices (CADCraft PDF, PNG, SVG), PLOTTERMANAGER plotters and "None". It also says the default is the page setup's device and that the device picks the output format and the papers. EXPORTPDF's help names `device` too, and notes that its output stays PDF. A test checks that both help texts name `device` and that PLOT's names every built-in device.
* plot help: say what Model, "None" and an omitted paper do
In model space, and with "None", no device is used and the plot is a PDF. When `paper` is omitted, a layout keeps its page setup's paper and model space takes the device's default.
* 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>
SVG 1.1 §7.5 lets every graphics element carry a transform. Only groups'
transforms were applied, so a rect, circle, path or other shape with its
own transform came in where it would be without one. on_element now
composes the element's transform after its groups' for that element
only, the way a group around it would.
https://www.w3.org/TR/SVG11/coords.html#TransformAttribute
Co-authored-by: ashokram-si <347905169+ashokram-si@users.noreply.github.com>